文章目录
👶一、JMM(Java内存模型)
JMM即Java Memory Model,它定义了内存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
-原子性-保证指令不会受到线程上下文切换的影响。(synchornized,前面的文章有介绍到)
-可见性-保证指令不会受到CPU缓存的影响。
-有序性-保证指令不会受到CPU指令并行优化的影响。
👧二、可见性
1、退不出的循环
我们先来看一下这个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止。
public class CanNotBreakOutCircle {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
}
});
t.start();
TimeUnit.SECONDS.sleep(1);
run = false;
}
}
执行结果:
接下来我们来分析一下:
解决方案1:
加上volatile关键字
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
volatile static boolean run = true;
解决方案2:
加上synchronized
public class CanNotBreakOutCircle {
static boolean run = true;
final static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
synchronized (lock) {
if (!run) {
break;
}
}
}
});
t.start();
TimeUnit.SECONDS.sleep(1);
synchronized (lock) {
run = false;
}
}
}
2、可见性和原子性
synchronized语句块既可以保证代码的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。
🧒三、有序性
现在这样看来似乎是没有什么影响,但这仅局限在单线程下是没有影响的,当多线程的情况下,却是有影响的。
1、鱼罐头的故事
2、指令重排序优化
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来 实现指令集并行,这一技术在80年代中叶到90年代中叶占据了计算架构的重要地位。
👦四、volatile原理
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对volatile变量的写指令后会加入写屏障
对volatile变量的读指令前会加入读屏障
1、volatile保证可见性
2、volatile保证有序性
👩五、double-checked locking问题(dcl)
1、懒汉式实例化
这段代码在多线程模式下是没有问题的,只要每一次我们都去获取锁,然后判断这个对象是否被创建了,如果被创建了我们就直接返回对象,如果还没有被创建,我们就创建对象。因为这段获取对象的代码都在synchronized下进行保护,所以这里是没有线程安全的问题的。但是这样子对于性能会比较不友好,每一次我们都去争抢锁,其实仔细观察这里的代码,你会发现我们只有第一次创建对象的时候需要加锁,而后面我们去获取对象的时候根本就不用加锁,所以我们还可以继续进行改进。
于是就带来了double-checked locking
这里当t1线程检测到对象为null的时候,会占用锁然后进行新建对象,与此同时t2线程也会检测到对象为null,并且试图去获取锁,而这里t2线程发现锁已经被t1所拥有了,于是这里t2线程会继续等待锁的释放。当t1线程创建了对象释放了锁,t2线程获取到锁之后会再次进行判断对象是否已经被创建了,如果被创建了,就直接返回对象,这就是双重检查的关键所在。这样子就实现了首次访问会加锁,而之后的使用就不需要加锁了。但是这样看上去似乎无懈可击的代码依然存在着问题
分析原因:
当这里进行了指令重排之后,先调用了24行,给INSTANCE附上了值,但是还没有调用构造方法,意味着t2线程获取到的INSTANCE确实不为null,但是这时候t2线程去使用对象的时候,这个对象的构造方法还没有完成。
synchronized能保证原子性、可见性,但要根据实际情况来看是否能够保证有序性,当变量完全受synchronized保护的时候,对于指令的重排序,是不会受影响的。但如果变量存在不受synchronized保护的情况,那么当指令发生重排序的时候,可能会存在问题。
2、duoble-checked locking解决
加上了写屏障以后,写屏障之前的代码不能够重排序到写屏障之后了,这里的21行必然会在24行之前。 所以这里t2线程不会拿到不为null但是未执行构造方法的对象。
🧑六、happens-before规则
happens-before规定了对共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结。
以下的这些例子共享变量之间都是可见的