当前位置: 首页 > news >正文

JUC并发编程——wait-notify

目录

    • 一、wait / notify
      • 1.1 wait / notify 原理
      • 1.2 wait / notify API介绍
    • 二、wait VS sleep
    • 三、wait / notify —代码改进

一、wait / notify

1.1 wait / notify 原理

在这里插入图片描述

● Owner线程发现条件不满足,调用wait( )方法即可进入WaitSet变为 WAITING状态

● BLOCKED 和 WAITING的线程都处于阻塞状态,不占用CPU时间片(相同点)

● BLOCKED 线程会在 Owner线程释放锁时唤醒

● WAITING 线程会在 Owner线程调用 notifynotifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

1.2 wait / notify API介绍

obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。无论是wait还是notify 必须获得此对象的锁,才能调用这几个方法

示例
在这里插入图片描述

正常运行:

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test18")
public class Test18 {
    static final Object lock = new Object();
    public static void main(String[] args) {

        synchronized (lock) {
            try {
            /* 需先获取对象锁,成为Owner后才能调wait();
            这时才能进入lock所关联的Monitor对象中的WaitSet中WAITING
             */
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

notify():挑一个唤醒

import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        // 进入同一个对象中的Monitor
        synchronized (obj) {
            // 唤醒obj上一个线程(挑一个线程唤醒)
           obj.notify();
           

            //     obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

运行结果:

在这里插入图片描述

notifyAll():全部唤醒

在这里插入图片描述

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止(wait(0)也会无限制等待)

wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    // 让线程t1在obj上等待1s
                    obj.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();
     }
  }      

运行结果:即使未唤醒也会结束

在这里插入图片描述

若在等待期间被其他线程唤醒,则会恢复,不会等够时间才才向下运行

import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    // 让线程t1在obj上等待1s
                    obj.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();


        // 主线程0.5秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        // 进入同一个对象中的Monitor
        synchronized (obj) {
            obj.notifyAll();
        }
    }
}

运行结果:

在这里插入图片描述

二、wait VS sleep

sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 的静态方法,而 wait 是 Object 的方法(所有的对象都有的方法)
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态都为 TIMED_WAITING(有时限的等待)

sleep(0)触发操作系统立刻重新进行一次CPU的竞争。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权

tip作为锁的对象使用final修饰,final意味引用不可变(若引用发生变化,synchronized锁住的为不同对象)

sleep演示
在这里插入图片描述

wait演示:(1s后主线程便成功获得锁)

在这里插入图片描述

三、wait / notify —代码改进

问题背景:模拟线程使用共享的room来达到线程安全

//  共享变量(线程安全的操作)
static final Object room = new Object();
static boolean hasCigarette = false;    // 是否有烟
static boolean hasTakeout = false;      // 外卖是否送到

思考下面的解决方案是否较好,为什么?

new Thread(() -> {
    synchronized (room) {
      log.debug("有烟没?[{}]", hasCigarette);
      if (!hasCigarette) {
           log.debug("没烟,先歇会!");
           sleep(2);
      }
      log.debug("有烟没?[{}]", hasCigarette);
      if (hasCigarette) {
            log.debug("可以开始干活了");
      }
   }
}, "小南").start();

// 其他线程
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
      synchronized (room) {
          log.debug("可以开始干活了");
      }
    }, "其它人").start();
 }
 
 // 主线程等待1秒
sleep(1);
new Thread(() -> {
    // 这里能不能加 synchronized (room)?
    hasCigarette = true;
   log.debug("烟到了噢!");
}, "送烟的").start();

观察7个线程的工作流程:
在这里插入图片描述
出现的问题(缺点):

  1. 其它干活的线程,都要一直阻塞,效率太低
  2. 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  3. 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没synchronized 就好像 main 线程是翻窗户进来的

● 解决方法:使用 wait - notify 机制

import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
    static final Object room = new Object();
    static boolean hasCigarette = false; // 有没有烟
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        sleep(1);
        // 主线程等待1s后启动睡眠线程
        new Thread(() -> {
             synchronized (room) {
                 hasCigarette = true;
                 log.debug("烟到了噢!");
                 // 唤醒正在睡眠的线程
                room.notify();
             }
        }, "送烟的").start();
    }
}

运行结果:(并发效率得到大大提升)
在这里插入图片描述
深度思考

如果有其他线程也在等待条件呢?(送烟线程会不会错误唤醒其他线程)

import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
    static final Object room = new Object();
    static boolean hasCigarette = false; // 有没有烟
    static boolean hasTakeout = false;   // 外卖是否送到

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();


        // 小女线程等待外卖
        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到了没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会");
                }
                try {
                    room.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("外卖送到了没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以干活了");
                } else {
                    log.debug("没干成活");
                }
            }
        }, "小女").start();

        sleep(1);
        // 主线程等待1s后启动睡眠线程
        new Thread(() -> {
            synchronized (room) {
                hasCigarette = true;
                log.debug("外卖到了噢!");
                room.notify();     // 调用notify()时,只能在room中等待的线程中随机挑一个唤醒
            }
        }, "送外卖的").start();
    }
}

运行结果:
在这里插入图片描述

出现的问题(缺点):notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为虚假唤醒

● 解决方法:使用 notifyAll将所有线程唤醒

运行结果:
在这里插入图片描述

出现的问题(缺点):用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了

● 解决方法:使用 while + wait,当条件不成立,再次 wait

import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                // 线程还可以进入下一轮的等待
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }else {
                    log.debug("没干成活......");
                }
            }
        }, "小南").start();


        // 小女线程等待外卖
        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到了没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会");
                }
                try {
                    room.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("外卖送到了没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以干活了");
                } else {
                    log.debug("没干成活");
                }
            }
        }, "小女").start();

        sleep(1);
        // 主线程等待1s后启动睡眠线程
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notifyAll();     // 调用notifyAll()时,将所有在room中等待的线程全部唤醒
            }
        }, "送外卖的").start();
    }
}

运行结果:
在这里插入图片描述

总结

● 正确使用wait-notify的格式:

synchronized (lock) {
   while(条件不成立){
       lock.wait();
   }
  // 条件成立,继续向下运行
}

// 另一个线程
synchronized (lock) {
    lock.notifyAll();
  }

相关文章:

  • PPQ库中KLD算法实现代码解析
  • 我就不信你还不懂HashSet/HashMap的底层原理
  • pytorch学习之pytorch构建模型的流程
  • react-swipeable-views轮播图实现下方的切换点控制组件
  • Java线程知识点总结
  • Android Compose——一个简单的Bilibili APP
  • 世界顶级五大女程序媛,不仅技术强还都是美女
  • 2023年再不会Redis,就要被淘汰了
  • 【学习笔记】深入理解JVM之垃圾回收机制
  • 【数据结构】链式二叉树
  • 自学大数据第三天~终于轮到hadoop了
  • 应用层协议 HTTP HTTPS
  • Linux内核学习笔记——页表的那些事。
  • 一文带你入门,领略angular风采(上)!!!
  • 嵌入式学习笔记——STM32硬件基础知识
  • $translatePartialLoader加载失败及解决方式
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • 【个人向】《HTTP图解》阅后小结
  • 【剑指offer】让抽象问题具体化
  • Angular Elements 及其运作原理
  • angular2开源库收集
  • Brief introduction of how to 'Call, Apply and Bind'
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • CentOS6 编译安装 redis-3.2.3
  • classpath对获取配置文件的影响
  • Docker 1.12实践:Docker Service、Stack与分布式应用捆绑包
  • Java 11 发布计划来了,已确定 3个 新特性!!
  • javascript数组去重/查找/插入/删除
  • JAVA多线程机制解析-volatilesynchronized
  • leetcode46 Permutation 排列组合
  • tensorflow学习笔记3——MNIST应用篇
  • TypeScript实现数据结构(一)栈,队列,链表
  • Yeoman_Bower_Grunt
  • 给新手的新浪微博 SDK 集成教程【一】
  • 汉诺塔算法
  • 排序算法学习笔记
  • 深入 Nginx 之配置篇
  • 微信公众号开发小记——5.python微信红包
  • 小而合理的前端理论:rscss和rsjs
  • 浅谈sql中的in与not in,exists与not exists的区别
  • !!【OpenCV学习】计算两幅图像的重叠区域
  • #微信小程序(布局、渲染层基础知识)
  • (delphi11最新学习资料) Object Pascal 学习笔记---第5章第5节(delphi中的指针)
  • (补)B+树一些思想
  • (独孤九剑)--文件系统
  • (剑指Offer)面试题41:和为s的连续正数序列
  • (企业 / 公司项目)前端使用pingyin-pro将汉字转成拼音
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (转)C语言家族扩展收藏 (转)C语言家族扩展
  • .form文件_SSM框架文件上传篇
  • .Net 6.0 处理跨域的方式
  • .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
  • .NET单元测试
  • [ CTF ] WriteUp- 2022年第三届“网鼎杯”网络安全大赛(白虎组)