多线程---内存可见性,wait()和notify()
(一).内存可见性(volatile)
1.概念
在多线程的场景下,一个线程读取,另一个线程修改,修改线程修改的值,但是并没有被读线程读到,此时就称这个问题为“内存可见性”问题。
2.示例
public static int flag=0;
public static void main(String[] args) {
Thread thread1=new Thread(()->{
while (flag==0){
}
System.out.println("thread1线程结束");
});
Thread thread2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入flag的值");
flag=scanner.nextInt();
});
thread1.start();
thread2.start();
}

通过上面的代码和运行图片看到 ,即使我输入了1,但是thread1线程也没有结束,这又是为什么?
这是由于“编译器优化”在捣鬼,“编译器优化”就是 “编译器” / “JVM” 会将我们写的代码在逻辑不变的情况下,将代码进行调整。但是在多线程的程序中,编译器的判断会有错误,会导致优化后的逻辑与优化前的逻辑出现细节上的偏差。

对于 “flag==0”这个操作,类似于compare操作,但是在JVM中短时间会进行很多次这样的操作,首先,会先从内存中读入flag这个变量,即load,读取到寄存器,然后再进行cmp,对于load,是读内存的操作,cmp是纯cpu寄存器操作,所以load消耗的时间是cmp的几千倍
在执行了很多次这个循环之后,JVM发现flag一直是0,所以JVM就会觉得既然都是0,就没必要修改这么多次了,于是就将读内存的操作优化成了读寄存器的操作,即把内存中的值读取到了寄存器,后续再load的时候就不再重新读取内存,直接从寄存器中取。最终,等到用户真正输入新的值的时候,真正修改flag的时候,thread1已经感受不到了
优化
public static int flag=0;
public static void main(String[] args) {
Thread thread1=new Thread(()->{
while (flag==0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread1线程结束");
});
Thread thread2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入flag的值");
flag=scanner.nextInt();
});
thread1.start();
thread2.start();
}

当我在while()循环中加入了一个sleep(1)之后,程序运行起来的时候,发现thread1线程就结束了
本来一个循环一秒执行了几千万那次,但是加上sleep(1)之后,循环的次数就大大降低了,当引入sleep(1)这个操作,相对于load和cmp来说,消耗的时间又快了很多。假设读取flag的操作是1ns,通过之前的优化, 1ns =>0.5ns,提升了50%,但是引入sleep之后,sleep直接占用1ms,所以优不优化就无所谓了
引入volatile关键字
public volatile static int flag=0;
public static void main(String[] args) {
Thread thread1=new Thread(()->{
while (flag==0){
}
System.out.println("thread1线程结束");
});
Thread thread2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入flag的值");
flag=scanner.nextInt();
});
thread1.start();
thread2.start();
}

上述的代码,我在flag成员变量上加入了一个volatile关键字,通过volatile关键字来修饰flag,此时编译器在对这个变量的读取操作时,就不会优化成“读寄存器”,这样,thread2修改了,thread1就能及时看到了,从而避免了“内存可见性”的问题
注意:volatile关键字只能修饰变量,同时volatile关键字只能解决 “内存可见性”的问题,如果想要解决“原子性”的问题,还是需要加锁
3.JMM(Java内存模型)
谈到 “内存可见性”的问题,就不得不了解JMM了,我们上述说的读内存的操作优化成读寄存器的操作,在Java官方文档的术语中有原话:每个线程都有自己的“工作内存”,同时这些线程共享同一个“主内存”。当一个线程循环进行上述读取变量操作的时候,就会把“主内存”中的数据,拷贝到该线程的 “工作内存”中,后续另一个线程修改,也是先修改自己的“工作内存”,拷贝到“主内存”里,由于第一个线程仍然在读自己的“工作内存”,因此感知不到“主内存”的变化
注意:这里说的“工作内存”其实是指的“cpu寄存器”,“主内存”是指我们说的“内存”
(二).wait() 和 notify()
1.概念
wait(),等待。notify(),通知。这两个方法的作用就是协调线程之间的执行逻辑的顺序的。wait()和notify()方法可以让后执行的逻辑等待先执行的逻辑先跑。虽然无法直接干预调度器的调度顺序,但是可以让后执行的逻辑等待,等待到先执行的逻辑跑完了,通知一下当前的线程让他继续执行。
注意:wait()和notify()都是Object的方法,所以java中任意对象都提供了wait()和notify()方法
2.join(),“锁” 和 wait()的区别
①.join是等待另一个线程彻底执行完才能继续走
②.“锁”的等待是不受控制的,某个线程的代码执行到加锁,不一定触发等待,因为不确定其
他的线程是否是“加锁”的状态
③.wait()是等到另一个线程执行notify()之后,才继续走,不需要另一个线程彻底执行完
3.线程饿死(线程饥饿)
站在操作系统的角度,操作系统的调度是随机的,其他线程都属于在锁上阻塞等待,对于当前释放锁的这个线程,是就绪状态,那么这个线程有很大的概率能再次获取到这把锁,从而导致后面的线程一直等待,无法获取到cpu资源,即“线程饿死”(汤老师ATM)
对于线程饿死,不是每个线程一直获取到这把锁,导致后面的线程饿死,可能获取到几十次之后,其他的线程有机会获取到当前这把锁,使用wait()和notify()的目的就是为了优化掉“获取几十次”这个操作,从避免程序效率变低。
4.示例1
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
System.out.println("wait()之前");
object.wait();
System.out.println("wait()之后");
}

当执行代码的时候,发现程序报错了,报的错误是“非法的锁状态”,那么这里就出问题了,我们又没有加锁,为什么会报一个“非法的锁状态”的错误?
这是因为wait()这个方法,会先释放锁,然后再进行阻塞等待。对于先释放锁的前提是,首先要加锁。所以回到上面的代码中,我们要先释放掉object对象对应的锁,但是释放锁的前提是,object对象要处于加锁的状态,由于object对象没有加锁,所以调用wait()方法的时候才会报错
对于wait()这个方法,最关键的一点就是要先释放锁,然后给其他线程获取锁的机会,既然要释放锁,首先要先加上锁才能谈释放
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
System.out.println("wait()之前");
synchronized (object){ // 重点:synchronized的锁对象要和调用wait()方法的对象一致
//加上锁之后,代码进入wait之后就会先释放锁,并且阻塞等待,如果其他线程做完了必要的工作
//调用notify()唤醒这个wait()线程,wait()线程就会解除阻塞,并且重新获取这把锁,继续执行并返回
object.wait();
}
System.out.println("wait()之后");
}
5.示例2
public static void main(String[] args) {
Object object=new Object();
Thread thread1=new Thread(()->{
System.out.println("wait之前");
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait之后");
});
Thread thread2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("输入任意内容,唤醒thread1");
scanner.next();
synchronized (object){
object.notify();
}
});
thread1.start();
thread2.start();
}

这里我们要注意的点:
①.synchronized()的锁对象要和和调用wait(),notify()的方法的对象是一致的。
②.在thread2线程中,我们是通过Scanner来进行线程阻塞,等待用户控制台输入,只不过这里的阻塞是等待IO进入的阻塞,这样做的目的是确保notify()要在wait()之后
③.notify()和wait()一样,也要先拿到锁,再进行notify()这是Java给出的限制
6.示例3
如果多个线程在同一个对象上wait()那么notify()的时候是随机唤醒其中的一个线程
public static void main(String[] args) {
Object locker=new Object();
Thread thread1=new Thread(()->{
System.out.println("thread1 wait之前");
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread1 wait之后");
});
Thread thread2=new Thread(()->{
System.out.println("thread2 wait之前");
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread2 wait之后");
});
Thread thread3=new Thread(()->{
System.out.println("输入任意内容,唤醒任意一个线程");
Scanner scanner=new Scanner(System.in);
scanner.next();
synchronized (locker){
locker.notify();
}
});
thread1.start();
thread2.start();
thread3.start();
}

通过上面的代码,我们可以知道,thread1线程和thread2线程都在wait(),然后thread3线程负责notify(),当我输入一个值的时候,发现只唤醒了两个线程中的随机一个线程,所以如果多个线程在同一个对象上wait()那么notify()的时候是随机唤醒其中的一个线程
解法方法:有多少个线程wait()就要有多少个notify()进行唤醒
public static void main(String[] args) {
Object locker=new Object();
Thread thread1=new Thread(()->{
System.out.println("thread1 wait之前");
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread1 wait之后");
});
Thread thread2=new Thread(()->{
System.out.println("thread2 wait之前");
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread2 wait之后");
});
Thread thread3=new Thread(()->{
System.out.println("输入任意内容,唤醒任意一个线程");
Scanner scanner=new Scanner(System.in);
scanner.next();
synchronized (locker){
locker.notify();
}
System.out.println("输入任意内容,唤醒另一个线程");
scanner.next();
synchronized (locker){
locker.notify();
}
});
thread1.start();
thread2.start();
thread3.start();
}

7.示例4
public static void main(String[] args) {
Object locker=new Object();
Thread thread1=new Thread(()->{
System.out.println("thread1 wait之前");
synchronized (locker){
try {
locker.wait(10000); //wait()带参数版本,最多等10秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread1 wait之后");
});
Thread thread2=new Thread(()->{
System.out.println("thread2 wait之前");
synchronized (locker){
try {
locker.wait(); //死等
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread2 wait之后");
});
Thread thread3=new Thread(()->{
System.out.println("输入任意内容,唤醒任意所有线程");
Scanner scanner=new Scanner(System.in);
scanner.next();
synchronized (locker){
//一下子唤醒全部的线程
//虽然同时唤醒了thread1和thread2,但是wait唤醒之后会重新加锁,其中某个线程先上了锁,开始执行
//另一个线程因为加锁失败了,再次阻塞等待,等到先走的线程释放了锁,后走的线程才能加上锁,继续执行
locker.notifyAll();
}
});
thread1.start();
thread2.start();
thread3.start();
}

8.wait()和sleep()的相同点和不同点
相同点:
①.wait()有时间等待;sleep()也有时间等待
②.wait()可以使用notify()提前唤醒;sleep()可以通过interrupt提前唤醒,但是interrupt
本身的作用是线程终止
不同点:
①.wait()必须搭配锁进行使用,先加锁,才可以使用;sleep()不需要搭配锁
②.如果都在synchronized内部使用,wait()会先释放锁;sleep()不会释放锁
9.作业题

public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Object locker3=new Object();
Thread thread1=new Thread(()->{
for (int i = 0; i < 10; i++) {
synchronized (locker1){
try {
locker1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("A");
synchronized (locker2){
locker2.notify();
}
}
});
Thread thread2=new Thread(()->{
for (int i = 0; i < 10; i++) {
synchronized (locker2){
try {
locker2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("B");
synchronized (locker3){
locker3.notify();
}
}
});
Thread thread3=new Thread(()->{
for (int i = 0; i < 10; i++) {
synchronized (locker3){
try {
locker3.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
synchronized (locker1){
locker1.notify();
}
}
});
thread1.start();
thread2.start();
thread3.start();
//主线程中,先通知一次 locker1 让上述的逻辑从thread1开始执行
//需要确保到上述三个线程都执行到wait,再进行notify
Thread.sleep(1000);
synchronized (locker1){
locker1.notify();
}
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)