JavaEE中volatile、wait和notify详解
一.volatile 关键字.
1.volatile 能保证内存可见性问题
什么是内存可见性?
可见性指 , 一个线程对内存的修改 , 能够及时的被其他线程看到.
Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型 , 目的是屏蔽一切硬件和操作系统的内存访问差异 , 以实现Java程序在各种平台下都能达到一致的并发效果.
- 线程之间的共享变量存在主内存(Main Memory)
- 每一个线程都有自己的"工作内存"(寄存器)
- 当线程要读取一个共享变量时 , 会把共享变量从主内存拷贝到工作内存, 再从工作内存中读取数据.
- 当线程要修改共享变量时 , 也先修改工作内存中的副本 , 最后同步到主内存中.
由于每个线程都有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的副本 , 此时修改线程 t1 的工作内存中的值 , 线程 t2 的工作内存不一定及时发生变化.这时代码就容易发生问题.
此时引出两个问题:
- 为什么要这么多内存
- 为什么要拷贝多次
1) 为什么要这么多内存?
实际并没有这么多的内存 , 这只是Java规范中的一个术语 , 是术语抽象的叫法.
所谓主内存才是真正硬件角度的内存 , 而所谓工作内存 , 则是指CPU的寄存器和高速缓存(cache).至于为什么起名工作内存 , 一方面是为了表述简单 , 另一方面也是避免涉及到硬件的细节和差异 , 例如有的CPU可能没有cache , 有的还存在很多个 , 因此Java就使用工作内存一言蔽之了.
2) 为什么要多次拷贝?
因为CPU访问寄存器和高速缓存的速度 , 比访问寄存器快了3-4个数量级.
如果要连续10次读取同一个数据 , 不断从内存中访问就很慢 , 那么如果第一次从内存中读取到寄存器 , 后面9次从寄存器中读取就会快很多.
- volatile 修饰的变量 , 能够保证内存可见性.
代码示例:
创建两个线程 t1 和 t2 , t1 线程循环重复快速读取flag , t2 线程对 flag 进行修改.按照预期结构 , 如果我们修改 t2 线程中的 flag 变为非0 , t1 线程就会循环结束.
class MyCounter{
public int flag = 0;
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyCounter myCounter = new MyCounter();
Thread t1 = new Thread(()->{
while (myCounter.flag == 0){
//循环重复快速读取
}
System.out.println("循环结束");
});
Thread t2 = new Thread(()->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个数");
myCounter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
结果与我们预期并不相符 , 对 flag 作出修改后 , t1 线程并没有循环结束.
通过 jconsole 查看 t1 线程还在执行 , 而 t2 线程已执行完毕.
结合内存可见性问题 , 答案显而易见. 一个线程读 , 一个线程改 , 会产生线程不安全问题.从汇编的角度来理解 , 执行下面这段代码分为两个步骤:
- load 把内存中的值读到寄存器中.
- cmp 把寄存器的值和0进行比较 , 根据比较结果决定下一步往哪执行(条件循环指令)
上述循环操作在寄存器中 , 执行速度极快(1秒钟执行百万次以上) , 循环这么多次 , 在 t2 真正修改前 , load 得到的执行结果都一样.另一方面 load 相比于 cmp 操作速度慢非常多 , 再加上反复 load 的结果都一样 , JVM 就会认为没有人改 flag 的值 , 从此不再从内存中 load flag 的值 , 直接读取寄存器中保存的 flag , 这时JVM/编译器的一种优化方式 , 但由于多线程的复杂性 , 判定可能存在误差.
解决方式:
此时为了避免上述情况 , 就需要程序员手动干预 , 可以给 flag 这个变量加上 volatile 关键字.意思是告诉编译器这个变量是"易变" , 一定要每次都从内存中重新 load 这个变量 , 不能再进行激进的优化了.
class MyCounter{
public volatile int flag = 0;
}
2.volatile 不能保证原子性
volatile 与 synchronized 有本质的区别 , synchronized 保证原子性 , volatile 保证的是内存可见性.
代码示例:
这是最初演示线程安全的代码 , 两个线程分别对 count 自增5万次.
- 去掉修饰 add 方法的 synchronized 关键字.
- 给 count 变量加上 volatile 关键字.
最终代码执行结果并不是预期的10w次.
class Counter{
public volatile int count;
public void add(){
count++;
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("count = "+counter.count);
}
二.wait和notify
由于线程的特性是抢占式执行随机调度 , 因此线程之间的先后执行顺序难易预知 , 但实际开发中我们希望合理的协调多个线程之间的先后执行顺序.
完成这个协调工作主要涉及三个方法:
- wait()/wait(long timeout).让当前线程进入等待状态.
- notify()/notifyAll().唤醒当前在对象上等待的方法.
Tips:wait(),notify(),notifyAll()都是Object类的方法.
通过上述介绍可以发现 , wait 和 notify 与 join 和 sleep 在功能上有极大的重合之处 , 那么为什么还要开发 wait 和 notify 呢?
因为 , 使用 join 就必须等待一个线程彻底执行完才能换另一个线程. 如果我们想让线程1执行50% , 然后立即执行线程2 , 显然 join 达不到这个效果. 而且使用 sleep 必须指定休眠多长时间 , 但线程1执行完毕需要花费多少时间并不好估计.所以使用 wait 和 notify 可以更好的解决上述问题.
1.wait方法
wait做的事情:
- 先释放锁
- 进行阻塞等待
- 收到通知后 , 重新尝试获取获取这个锁 , 并且在获取这个锁后 , 继续往下执行.
代码示例:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
object.wait();
}
运行该代码出现异常 , 这是因为执行 wait 操作 , 需先获取当前线程的锁 , 而当前线程并没加锁 , 所以会出现非法锁状态异常.这就好比 , 我的一个朋友还没收到offer就已经开始挑选公司.
修改后代码:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("wait 之前");
object.wait();
System.out.println("wait 之后");
}
}
通过运行结果可以得知 , 代码执行到object.wait()就进入阻塞.实际上在阻塞状态之前 , wait 已经释放了锁 , 此时其他线程可以获取到object对象的锁 , 等到 wait 被唤醒后再尝试获取这个锁.
举个例子就是滑稽老铁去ATM机取钱 , 当他进入银行网点后锁上门开始操作ATM机 , 结果发现ATM机没钱 , 由于银行外还有排队等待办理其他业务的人 , 他只能打开锁后出去(相当于 wait 释放锁的操作) , 等待运钞车来存钱(相当于 wait 的阻塞等待) , 当运钞车把钱存进银行 , 站在外面排队等待的滑稽老铁 , 又要和其他竞争进入银行的机会.(重新尝试获取这个锁) , 进入银行后执行取钱操作(重新加锁后继续执行其他操作).
wait结束等待的条件
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时.(wait 有一个带参方法 , 可以指定等待时间)
- 其他线程调用该等待线程的 Interrupted 方法 , 导致 wait 抛出InterruptException异常.
2.notify方法
notify 方法是唤醒等待的线程.
- notifty 方法同样需要在加锁的方法和加锁的代码块中调用 , 该方法是用来唤醒那些因调用 wait方法而阻塞等待的线程 , 通知它们重新获取对象锁.
- 如果有多个线程调用同一对象处于等待 , 则由线程调度器 , 随机挑选一个呈 wait 状态的线程唤醒.
- 在 notify 方法执行完毕后 , 当前线程不会立即释放该对象锁 , 要等待执行 notify 方法的线程彻底退出加锁代码块后才会释放锁对象.
代码示例:
public class ThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(() -> {
//这个线程负责进行等待
System.out.println("t1: wait 之前");
try {
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1: wait 之后");
});
Thread t2 = new Thread(() -> {
System.out.println("t2: notify 之前");
//notify务必获取锁才能通知
synchronized (object) {
object.notify();
}
System.out.println("t2: notify 之后");
});
t1.start();
//此时让 wait 先执行,防止 notify 空打一炮.
Thread.sleep(100);
t2.start();
}
}
观察代码执行结果明显符合预期.
为什么 notify 方法也要在同步方法或同步代码块中?
同步方法或同步代码块指的是 , 加锁的方法或加锁的代码块.
代码示例:
假设我们要实现一个阻塞队列 , 如果不加同步代码块实现方法如下:
class BlockingQueen{
Queue<String> queue = new LinkedList<>();
Object lock = new Object();
public void add(String data){
queue.add(data);
lock.notify();
}
public String take() throws InterruptedException {
while (queue.isEmpty()){
lock.wait();
}
//返回队列的头结点
return queue.remove();
}
}
这段代码的核心思想是 , 当队列为空时使用lock.wait()阻塞 , 如果调用add()方法添加元素时再采用lock.notify()唤醒.这段代码可能产生以下问题:
- 一个消费者调用 take() 方法获取数据 , 但queue.isEmpty() , 于是反馈给生产者.
- 在消费者调用 wait 之前 , 由于CPU的调度 , 消费者线程被挂起 , 生产者调用add() , 然后notify().
- 之后消费者调用wait().由于错误的条件判断导致 wait 调用在 notify 之后.
- 在这种情况下 , 消费者就会一直被挂起 , 生产者也不再生产 , 这个阻塞队列就有问题.
由此看来 , 在调用 wait 和 notify 这种会挂起的操作时 , 需要一种同步机制保证
3.wait和sleep的对比
理论上 wait 和 sleep 没有可比性 , 因为 wait 常用于线程间通信 , sleep 则是让线程阻塞一段时间 , 唯一的相同点是都可以让线程放弃执行一段时间.
- 1.wait 需要搭配 synchronized 关键字使用 , 而sleep则不需要.
- 2.wait 是object 方法 , sleep则是Thread类的静态方法.
- 3.wait 被notify 唤醒属于正常的业务范畴 , sleep 被Interrupt 唤醒需要报异常.
总结
到此这篇关于JavaEE中volatile、wait和notify的文章就介绍到这了,更多相关JavaEE volatile、wait和notify内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341