Java线程安全:volatile与wait/notify详解
目录
一. volatile
1.1内存可见性
1.2 volatile的使用
二. wait & notify
2.1 wait()
2.2 notify()
2.3 notifyAll()
2.4 具体流程
一. volatile
volatile 是Java中的一个关键字,主要是解决线程安全问题中内存可见性的问题
在了解volatile之前我们要先了解一下什么是内存可见性
1.1内存可见性
内存可见性是 多线程环境下,一个线程修改共享变量后,其他线程能否“立刻看到”这个修改结果 的特性。
下面来看一个经典的例子:
public class Text {public static int flog=0;public static void main(String[] args) {Thread t1=new Thread(()->{Scanner sc=new Scanner(System.in);flog=sc.nextInt();});Thread t2=new Thread(()->{while (flog==0){//什么也不做}});t1.start();t2.start();}
}
在这个代码中就会出现由于内存可见性而导致输入1后仍然不能退出循环
原因:
站在CPU指令的角度:
1.load操作会从内存中读取flog的值,到寄存器中
2.cmp操作会将flog与寄存器中的值进行比较并且判断跳转
虽然在另一个线程中有flog值的修改,但是编译器无法分析出另一个线程的执行时机,并且load操作的开销远远大于cmp的开销,所以编译器做出了一个大胆的判定:将load操作进行优化,优化为复用寄存器/缓存中的旧值
所以当我们输入1时,而flog读取的仍然是存放在寄存器/缓存中的旧值0,而导致一直陷入循环,不能退出
1.2 volatile的使用
volatile是Java中的关键字,直接修饰可能会触发内存可见性问题的变量后即可
public class Text {public volatile static int flog=0;public static void main(String[] args) {Thread t1=new Thread(()->{Scanner sc=new Scanner(System.in);flog=sc.nextInt();});Thread t2=new Thread(()->{while (flog==0){}});t1.start();t2.start();}
}
注意:
- volatile只能解决内存可见性导致的线程安全问题,并不能保证原子性
- volatile只能应对一个线程读一个线程写的操作,不能应对两个线程写(主要是原子性的问题,而锁不仅能保证原子性还能顺便解决内存可见性问题)
问题:若是在while中加入sleep()能阻止内存可见性问题吗?
答:能 ,因为内存可见性问题本质上是由编译器优化带来的,而由于sleep的引入会抑制编译器对load的优化,从而解决了内存可见性问题(不是sleep影响内存可见性,而是影响编译器的优化)
我们还要知道的一点:在循环体中的各种复杂操作,都可能会引其上述的优化失效
public class Text {public static int flog=0;public static void main(String[] args) {Thread t1=new Thread(()->{Scanner sc=new Scanner(System.in);flog=sc.nextInt();});Thread t2=new Thread(()->{while (flog==0){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}
加入sleep后就不存在线程安全问题了
二. wait & notify
由于线程之间的执行是抢占式的,我们难以预料线程执行的先后顺序。但是在实际开发中我们希望合理的协调多线程之间的执行顺序。就比如假期出行,我们要先订票支付后,平台才会给我们安排座位位置。
在Java中就涉及三个方法能够帮助我们完成整个流程
2.1 wait()
wait()要做的三件事
- 执行到wait后释放当前线程的锁
- 等待其他线程的通知
- 当通知到达后,线程会先从等待状态进入阻塞状态(去竞争锁),等成功获取锁后,才会进入就绪状态
上述的1和2是原子的
wait()被唤醒的条件
- 执行到notify(),被notify唤醒
- wait等待超时【wait(long timeout)/wait(long timeout,int nanos)】---设置超时时间
- 当其它线程调用该线程的 interrupt 方法,wait 会抛出异常使其退出等待状态进入阻塞状态
注意:
- wait必须要在synchronized中使用
- wait()是Object类 的方法
2.2 notify()
notify是唤醒等待的线程
注意:
- notify唤醒的是当前对象锁上,处于wait 等待状态的其中一个线程(随机唤醒其中一个)
- notify必须要在synchronized中使用
- notify()是Object类 的方法
2.3 notifyAll()
与notify()不同的是它唤醒处于wait 等待状态的所有线程,其余与notify一致
2.4 具体流程
注意:
- synchronized 锁定的对象、调用 wait() 方法的对象、调用 notify() / notifyAll() 方法的对象,必须是同一个对象。要配套使用,否则会通知无效
- wait要保证在notify之前执行,不然执行到wait时会出现死等状态(没有notify再唤醒它)
2.5 wait 与 sleep 的区别
- wait 要释放锁,而sleep 只是让线程陷入休眠,不会释放锁
- wait 必须要搭配锁使用,而 sleep 不需要
- 虽然 wait 和 sleep 都能被 interrupt 唤醒,但是 wait 设计的初衷是更希望被notify唤醒