补充:synchronized(务必会读(辛可肉耐子)会写),要搭配一个对象的时候,不一定非要是访问的this成员
synchronized(锁对象){ 代码块}
public synchronized static void func(){} 静态方法和具体对象无关,和类有关了
相当于synchronized(类.class)
在synchronized()里面没有必要去纠结里main写普通对象还是类对象之类的,synchronized不关心对象是什么,只关心两个线程是否针对同一个对象加锁
static:相当于加上了一个类属性/类方法
一、 💛
内存可见性引起的问题,如下图,你在线程1设定假如他不是等于0就终止,然后你在线程2中给他把值改变了,让他的循环结束,但是我么可以看到,当前我们输入了一个不是0的数字,然后试图改变它,却改变不了,这就是内存可见性的问题
程序在运行的时候,java编译器和jvm可能会对代码进行优化,程序猿们写代码,然后java编译器把你代码改了,保持原有逻辑不变的情况下提高代码效率——编译器优化后
并且优化的效果特别好:服务器的启动步骤非常复杂,启动一个差不多10分钟左右(但是假如我们吧优化关了,可能1个小时打底)
我们想要知道他是如何优化的,就要先清楚while循环的本质,两个指令
1.load读取内存
2.jcmp(比较,并且跳转,寄存器操作,速度极快)
此时编译器发现,代码反复的,快速的读取同一个内存值,并且这个内存值每次读出来的结果还是一样的,此时编译器决定,直接把load优化掉了,只是第一次执行load,后续并不执行load,直接拿寄存器中的数据进行比较了。
但是在另一个线程修改t2线程会不会执行,什么时候去执行,因此产生了误判,导致虽然最后t2的isQuit改动了,但是t1线程中,并未重复load也就会导致出现上述问题了。
import java.util.Scanner; public class Demo { public static int isQuit=0; //静态变量 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { //创立一个线程 while(isQuit==0){ ; } System.out.println("t1 结束了"); }); Thread t2 = new Thread(() -> { //我们去用t2来改变t1的值 Scanner scanner=new Scanner(System.in); System.out.println("请输入isQuit的数字"); isQuit=scanner.nextInt(); }); t1.start(); t2.start(); } }
volatile(会读会写 (wao(平🐍)里太哦)弥补上述缺口:意思易变的,修饰一个变量之后,编译器就明白,这个变量是一边倒,就不能按照上述方式处理代码(把读操作优化到寄存器中),让编译器禁止优化,于是保证t1在循环的过程中,始终都能读取内存中的数据
volatile本质是保持变量的内存可见性
见下面代码用法:
下面得到的结果就是正确的,我就暂时省略结果了。
import java.util.Scanner; public class Demo { public volatile static int isQuit=0; //变量static前面 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(isQuit==0){ ; } System.out.println("t1 结束了"); }); Thread t2 = new Thread(() -> { Scanner scanner=new Scanner(System.in); System.out.println("请输入isQuit的数字"); isQuit=scanner.nextInt(); }); t1.start(); t2.start(); } }
编译器的优化是一个“玄学问题”,就比如说,我在新代码里面,写了个sleep,同时把volatile取消了,但是这样也正确
可以理解为,加上sleep之后,sleep就会大幅度的影响到while循环的速度,速度慢了,编译器也不打算继续优化了~此时即使不加volatile,也能够及时感知到内存变化了,sleep到底间隔多久,会触发优化,只有那些码届远古大能才知道。
二、💜
另一种解释方式
Java的内存模型(JMM)
其实这个理解和上面那个编译器解释原理是一样的,从主内存读,但反复读都是一样的,所以直接就去工作内存读,但是工作内存又是什么鸟东西捏?
工作内存:不是我们平时说的内存,而是cpu的寄存器和cpu缓存统称为工作内存,有人可能会好奇那为啥不叫cpu寄存器+cpu缓存呢(猜:JAVA宣称是跨平台,但是cpu的话又是有一部分硬件知识,他们希望java程序猿可以不用掌握太多的硬件知识。
内存可见性和加锁描述了线程安全问题的典型情况和处理方式。
三、 💙
wait(等待)和notify(通知):用来协调线程顺序的重要工具,多线程调度是随机的~很多时候希望多个线程按照咱们规定的顺序来执行,完成线程之间的配合工作。
上面两个都是object提供的方法,也就是说任意对象,当wait引起线程阻塞之后,可以用interrupt方法,把线程给唤醒,打断当前线程的阻塞状态的。
wait执行程序的时候会干三件事:
1.解锁
2.阻塞等待
3.当被其他线程唤醒之后,就会尝试重新加锁,加锁成功,wait执行完毕,继续往下执行其他逻辑。
也就是说wait(需要先加锁)
核心思路:先加锁,在synchronized里面进行wait(加锁要加同一个对象上面),这里的线程会一直阻塞到其他线程notify。
其中最典型的场景就是有效的避免线程饥饿/线程饿汉
几个注意
1.要想notify能顺利唤醒wait,就需要确保wait和notify都是同一个对象调用的,
2.wait和notify都需要放到synchronzied之中的,虽然notify不涉及到解锁操作
3.如果进行notify的时候,另一个线程没有处于wait状态,此时notify也没有任何副作用。
t2可以理解成辅助t1的线程,使用notify线程对其他线程统筹安排作用。
import java.util.Scanner; public class Demo { public static Object locker=new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(true){ synchronized (locker) { System.out.println("t1 开启"); //第一步执行的 try { locker.wait(); //第二步执行,t2陷入阻塞状态 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 over"); //第五步,t1被唤醒 } } }); Thread t2 = new Thread(() -> { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (locker) { System.out.println("t2 开启"); //第三步执行的 locker.notify(); //第四部执行,唤醒t1 System.out.println("t2 over"); } } }); t1.start(); t2.start(); } }
线程可能有多个~
几个线程wait,一个线程复制notify,notify只会唤醒一个线程,具体哪个随机,notifyAll会唤醒全部线程(但是不推荐去用),这种也是全随机,就和wait的初心违背了。
如果要唤醒某个特定的线程,就要让不同的线程,使用不同的对象来进行wait,想要唤醒谁,就可以使用对应的对象notify
wait和sleep的区别
sleep有明确的使用是假,到达时间自动被唤醒,也能提前用interrupt
wait:死等,一直等到其他线程notify(但是,是正常唤醒,可继续工作,还会进入wait 状态),wait也可以被interrupt提前唤醒(但是这个是来通知线程要结束了,线程要收尾了)
,当然他也有带时间版的和join差不多,因此协调多个线程执行顺序wait比notify更牛一些