写点什么

JUC 并发—volatile 和 synchronized 原理

  • 2025-06-06
    福建
  • 本文字数:10036 字

    阅读完需:约 33 分钟

1.volatile 关键字的使用例子


(1)volatile 关键字的使用场景


如果多个线程共用一个共享变量,有的线程写、有的线程读,那么可能会导致有的线程没法及时读到其他线程修改的变量值。volatile 关键字可让某线程修改变量后,其他线程立即看到该变量的修改值。

 

(2)volatile 关键字的使用例子


public class VolatileDemo {    //如果没有volatile,那么读取的线程可能一直读到旧值而不打印,CPU缓存模型的数据一致性导致的    static volatile int flag = 0;
public static void main(String[] args) { new Thread() { public void run() { int localFlag = flag; while(true) { if (localFlag != flag) { System.out.println("读取到了修改后的标志位:" + flag); localFlag = flag; } } }; }.start(); new Thread() { public void run() { int localFlag = flag; while(true) { System.out.println("标志位被修改为了:" + ++localFlag); flag = localFlag; try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } } }; }.start(); }}
复制代码


(3)volatile 关键字的理解路径


一.CPU 缓存模型

二.Java 内存模型 JMM

三.原子性、可见性、有序性

四.volatile 的作用

五.volatile 的底层原理

六.volatile 案例

 

2.主内存和 CPU 的缓存模型


CPU 如果频繁读写主内存,那么就会导致 CPU 的计算性能比较差。所以现代的计算机,一般都会在 CPU 和内存之间加几层高速缓存。这样每个 CPU 就可以直接操作自己对应的高速缓存,从而不需要直接和主内存进行频繁的通信,保证了 CPU 的计算效率。


 

3.CPU 高速缓存的数据不一致问题


主内存的数据会被加载到 CPU 高速缓存里,CPU 后续会读写自己的高速缓存。但多线程并发运行时,就可能引发各个 CPU 高速缓存里的数据不一致问题。

 

上面 volatile 的例子,在没有 volatile 修饰 flag 的时候:负责执行线程 0 的 CPU 一开始会将主内存的 flag 值读到其高速缓存。之后在执行线程 0 的指令时,便会在该 CPU 的高速缓存里读取 flag 的值。此时该 CPU 无法感知负责执行线程 1 的其他 CPU 对其高速缓存的 flag 的修改。因为负责执行线程 1 的 CPU 可能没有将其修改的缓存值及时刷新回到主内存,或者负责执行线程 0 的 CPU 可能没有主动更新主内存的最新值到其高速缓存中。

 

所以 CPU 的缓存模型,在多线程并发运行时可能存在数据不一致的问题。也就是各个 CPU 的高速缓存和主内存没有及时同步,同一个数据在各 CPU 可能都不一样,从而导致数据的不一致。


 

4.总线锁和缓存锁及 MESI 缓存一致性协议


(1)总线锁和缓存锁机制


一.什么是总线


所谓总线,就是 CPU 与内存和输入输出设备传输信息的公共通道。当 CPU 和内存进行数据交互时,必须经过总线来传输。

 

二.总线锁


所谓总线锁就是:如果某个 CPU 要修改主内存的某数据,那么就往总线发出一个 Lock#信号。这个信号能够确保主内存只有该 CPU 可以访问,其他 CPU 的请求会被阻塞。这就使得同一时刻只有一个 CPU 能够访问主内存,从而解决缓存不一致问题。

 

所以总线锁可以理解为:当一个 CPU 往总线发出一个 Lock#信号时,其他 CPU 的的请求将会被阻塞,于是该 CPU 就能独占主内存(共享内存)了。

 

总线锁把 CPU 和内存之间的通信锁住了,从而使得锁定期间,其他 CPU 不能操作其他内存地址的数据。所以总线锁虽然解决了缓存不一致的问题,但却大幅降低了 CPU 的利用率,于是 CPU 使用缓存锁来替代总线锁。

 

三.缓存锁


如果当前 CPU 访问的数据已经缓存在其他 CPU 的高速缓存中,那么当前 CPU 便不会在总线上发出一个 Lock#信号,而是采用 MESI 缓存一致性协议来保证多个 CPU 的缓存一致性。

 

(2)MESI 缓存一致性协议


该协议要求每个 CPU 都可以监听到总线上的数据事件并做出相应的处理。当某个 CPU 向总线发出请求时,其他 CPU 便能监听到总线收到的请求,从而可以根据当前缓存行的状态和监听的请求类型来更新缓存行状态。这其实也就是所谓的 CPU 嗅探机制。

 

这样 MESI 缓存一致性协议就能保证,在 CPU 的缓存模型下,就不会出现多线程并发读写变量,各 CPU 没有办法及时感知到的问题,也就是解决了 CPU 缓存的一致性问题。

 

(3)CPU、高速缓存、内存之间的关系总结


一.高速缓存可解决 CPU 与内存的速度矛盾


为了解决 CPU 与内存速度之间的矛盾,引入了高速缓存作为内存与 CPU 之间的缓冲。

 

这里的高速缓存其实就是三级缓存。每个 CPU 可能有多个物理核,每个物理核会有多个逻辑核。每个 CPU 都有自己的三级缓存,每个物理核都有自己的一二级缓存。

 

二.每个 CPU 都有自己的高速缓存


每个 CPU 都有自己的高速缓存,而它们又共享同一个主内存。当多个 CPU 的运算任务都涉及到同一块主内存区域时,将可能导致各自的高速缓存的数据不一致。为了解决这种缓存数据不一致,引入了如 MESI 这些缓存一致性协议(嗅探)。

 

三.CPU 的乱序执行优化


为了使 CPU 内部的运算单元能尽量被充分利用,CPU 可能会对输入代码进行乱序执行优化。与 CPU 的乱序执行优化类似,JVM 的即时编译中也有指令重排序优化。

 

5.Java 的内存模型 JMM


(1)Java 内存模型 JMM 简介


Java 内存模型是用来屏蔽各种硬件和操作系统的内存访问差异的,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

 

Java 内存模型 JMM 的具体内容包括如下:

一.主内存与工作内存的关系

二.主内存与工作内存之间的交互

三.对于 volatile 变量的特殊规则

四.针对 long 和 double 型变量的特殊规则

五.原子性、可见性和有序性

六.Happens-Before 原则(先行发生原则)

 

(2)主内存与工作内存的关系


一.JMM 规定所有共享变量都存储在主内存

这里的共享变量只包括实例字段、静态字段和构成数组对象的元素。但不包括局部变量与方法参数,因为这些是线程私有的,不会共享。

 

二.每个线程都有自己的工作内存

这里的工作内存可以理解为各个 CPU 上的高速缓存,线程的工作内存中保存了被该线程使用的变量的主内存副本。线程对变量的读写操作必须在工作内存中进行,不能直接读写主内存的数据。线程之间无法直接访问对方工作内存中的变量,线程之间变量值的传递均需要通过主内存来完成。

 

三.主内存和工作内存分别存堆栈数据

主内存对应于 Java 堆中对象的实例数据,工作内存对应于虚拟机栈中的部分区域。对象包括对象头、实例数据、对象填充。

 

(3)主内存与工作内存的交互


JMM 定义了 8 种操作来完成:一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存。这 8 种操作都是原子的、不可再分的。

 

一.read:读取主内存的变量值并传输到线程的工作内存中,配合 load 使用

二.load:把 read 操作从主内存读取到的变量值写入工作内存的变量副本中

三.use:从工作内存中读取出数据,然后交给执行引擎进行计算

四.assign:将执行引擎计算好的值赋值给工作内存中的变量

五.store:把工作内存中的变量值传输回主内存中,配合 write 使用

六.write:把 store 操作从工作内存中得到的变量值写入主内存的变量中

七.lock:作用于主内存中的变量,用来标识变量被某个线程独占

八.unlock:作用于主内存中的变量,用来释放锁定状态



在 Java 内存模型下,多线程并发运行依然存在 CPU 高速缓存不一致问题。线程 1 修改了 flag 之后 write 回主内存,线程 2 也还是没法感知到 flag 已修改。

 

(4)对于 volatile 变量的特殊规则


volatile 变量对所有线程是立即可见的,对 volatile 变量的所有写操作都能立刻反映到其他线程之中,volatile 变量在各个线程的工作内存中是不存在一致性问题的。

 

从物理存储角度看,各线程的工作内存的 volatile 变量也可存在不一致的情况。但由于各线程每次使用 volatile 变量前都刷新,执行引擎看不到不一致的情况。因此可以认为不存在不一致的问题。

 

volatile 变量是禁止指令重排序优化的。

 

(5)针对 long 和 double 型变量的特殊规则


long 合 double 的非原子性协定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作,划分为两次 32 位的操作来进行。即允许虚拟机自行选择是否要保证 64 位数据类型的:read、load、store 和 write 四个操作的原子性。

 

如果有多个线程共享一个并未声明位 volatile 的 long 或 double 类型的变量,且同时对它们进行读取和修改,那么某些线程可能会读取到一个既不是原值,也不是其他线程修改值的"半个变量"的值。这种情况很罕见,64 位的 JVM 不会出现,但 32 位的 JVM 有可能出现。

 

6.JMM 如何处理并发中的原子性可见性有序性


(1)并发过程中可能产生的三类问题


一.原子性问题


对于一行代码"n++",只要多个线程并发执行,都不保证该操作是原子性的。如果保证了该自增操作的原子性,那么下图线程 1 的 i++为 1,线程 2 的 i++为 2。



二.可见性问题


线程 1 修改完了主内存的某个变量值,线程 2 一直读取的是 CPU 高速缓存中的该变量值,线程 1 修改完的该变量值对线程 2 不可见。

 

三.有序性问题


有序性指的是程序按照代码的先后顺序执行。而编译器为了优化性能,有时候会改变程序中语句的先后顺序。即编译器和指令器有时为了提高代码执行效率,会将指令进行重排序。比如"a=1;b=2;",编译器优化后可能就变成了"b=7;a=6"。此时编译器虽然调整了语句的顺序,但不会影响最终结果。

 

例子一:


假设有两个线程,线程 1 先执行 init()方法,线程 2 后执行 doxxx()方法。由于 init()方法的操作 1 和操作 2 没有依赖关系,所以编译器可能会对其重排序。经过编译器的重排序后,线程 1 可能会先执行操作 2,然后再执行操作 1。这时线程 2 在 while 循环中就会发现 flag = true,于是执行 execute()方法。但此时 init()方法的 prepare()还没执行完,从而引发代码逻辑异常。


public class ReOrderExample {    boolean flag = false;
//线程1先执行init()方法 public void init() { //准备资源 prepare();//操作1 flag = true;//操作2 } //线程2后执行doxxx()方法 public void doxxx() { while(!flag) { Thread.sleep(1000); } execute();//基于准备好的资源执行操作 } private void prepare() { ... } //prepare()执行完才能保证execute()方法的逻辑执行正确 private void execute() { ... }}
复制代码


例子二:


在下面利用双重检查创建单例的代码中,Singleton.getInstance()方法会先判断 inst 是否为空。如果为空则锁定 Singleton.class 并再次检查 inst 是否为空,如果还为空那么就创建一个 Singleton 的对象实例。

 

其中"inst = new Singleton()"会执行三个指令:

指令 1:分配对象的内存空间

指令 2:初始化对象

指令 3:设置 inst 变量指向指令 1 分配的内存地址

 

如果按照正常顺序来执行,那么是不会有问题的。但是由于指令 2 和指令 3 不存在依赖关系,所以编译器优化后可能进行重排序。于是执行顺序变为:指令 1 -> 指令 3 -> 指令 2。那么就可能发生:在线程 A 刚把 inst 指向对应地址后,线程 B 获取到执行权。然后线程 B 便获取到一个没有初始化的对象,从而产生空指针异常。


public class Singleton {    private static Singleton inst;      private Singleton() {                }      public static Singleton getInstance() {        if (inst == null) {            synchronized(Singleton.class) {                if (inst == null) {                    //开辟空间,inst指向地址,初始化                    inst = new Singleton();                }            }                }        return inst;    }}
复制代码


(2)JMM 如何处理原子性


由 JMM 来直接保证的原子性变量操作包括:read、load、assign、use、store 和 write 六个。

 

基本数据类型的访问、读写都是原子性的,例外就是 long 和 double 的非原子性协定。

 

JMM 还提供了 lock 和 unlock 操作来满足更大范围的原子性保证。这是通过字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作的,这两个字节码指令反映到 Java 代码中就是同步块(synchronized 修饰的代码)。

 

(3)JMM 如何处理可见性


可见性就是指当一个线程修改了共享变量的值,其他线程能立即得知该修改。

 

JMM 是通过在变量被修改后将新值同步回主内存,在变量被读取前从主内存刷新变量值的方式来实现可见性的,无论普通变量还是 volatile 变量都是如此。

 

普通变量和 volatile 变量的区别是:volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。也就是 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证。

 

除了 volatile 关键字,synchronized 和 final 两个关键字也能实现可见性。

 

synchronized 的可见性是通过如下这条规则获得的:对一个变量执行 unlock 操作之前,必须把变量先同步回主内存中。

 

final 的可见性是指被 final 修饰的字段在构造器中一旦被初始化完成,并且构造器没有把引用 this 传递出去,那么其他线程就能看见 final 字段的值。

 

(4)JMM 如何处理有序性


一.volatile 和 synchronized 关键字可保证多线程操作的有序性


volatile 的有序性是由于 volatile 变量禁止了指令重排序优化。

 

synchronized 的有序性则是通过如下这条规则来获得的:一个变量在同一时刻只允许一个线程对其进行 lock 操作。

 

二.通过 Happens-Before 规则来保证多线程操作的有序性


Happens-Before 规则指定了哪些操作是不能进行重排序的。

 

7.volatile 如何保证可见性


(1)volatile 型变量的特殊规则


一.volatile 变量对所有线程都是立即可见的

对 volatile 变量的所有写操作都能立刻反映到其他线程之中,volatile 变量在各个线程的工作内存中是不存在数据不一致性的问题。

 

从物理存储的角度看,各个线程的工作内存中,volatile 变量也可能存在不一致。但由于各个工作线程在每次使用 volatile 变量之前都要先刷新其值,于是执行引擎便看不到不一致的情况,因此可以认为不存在不一致的问题。

 

二.volatile 变量是禁止指令重排序优化的

指令重排序是指 CPU 将多条指令,不按程序规定的顺序,分开发送给各个相应的电路单元进行处理。可见,volatile 型变量的特殊规则就规定了 volatile 变量对所有线程立即可见。

 

(2)volatile 如何保证可见性


普通变量和 volatile 变量的区别是:volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。也就是 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证。



如果 flag 变量是加了 volatile 关键字,那么当线程 1 通过 assign 操作将 flag = 1 写回工作内存时,会立即执行 store 和 write 操作将 flag = 1 同步到主内存。

 

同时还会让线程 2 的工作内存中的 flag 变量的缓存过期,这样当线程 2 后续从工作内存里读取 flag 变量的值时,发现缓存已经过期就会重新从主内存中加载 flag = 1 的值。

 

所以通过 volatile 关键字可以实现这样的效果:当一个线程修改了变量值,其他线程可以马上感知这个变量值。

 

8.volatile 为什么无法保证原子性


(1)volatile 型变量的特殊规则


一.volatile 变量对所有线程都是立即可见的


对 volatile 变量的所有写操作都能立刻反映到其他线程之中,volatile 变量在各个线程的工作内存中是不存在数据不一致性的问题的。

 

从物理存储的角度看,各个线程的工作内存中,volatile 变量也可能存在不一致。但由于各个工作线程在每次使用 volatile 变量之前都要先刷新其值,于是执行引擎便看不到不一致的情况,因此可以认为不存在不一致的问题。

 

二.volatile 变量是禁止指令重排序优化的


指令重排序是指 CPU 将多条指令,不按程序规定的顺序,分开发送给各个相应的电路单元进行处理。可见,volatile 型变量的特殊规则并没有原子性方面的保证。

 

(2)volatile 不能保证原子性的字节码解释


比如 increase()方法只有一行代码,用 javap 反编译可知由 4 条字节码指令构成。当 get_field 指令把 n 的值取到操作栈顶时,volatile 保证了 n 的值此时是最新的。但线程 1 执行 iconst_1、iadd 这些指令时,线程 2 可能已经把 n 的值改变了。于是此时线程 1 的操作栈顶的 n 值,就变成了过期数据,所以线程 1 执行 put_field 指令后就会把较小的 n 值同步回主内存中。


//n定义为初始值为0的静态变量public volatile int n = 0;public void increase() {    n++;}
//javap反编译后的字节码get_fieldiconst_1iaddput_field
复制代码


严格来说,volatile 并不是轻量级的锁或者是轻量级同步机制。因为对于 n++这样的基本操作,加了 volatile 关键字也无法保证原子性。而锁和同步机制,如 synchonized 或者 lock 是可以保证原子性的。

 

9.volatile 如何保证有序性


(1)Happens-Before 规则介绍


Happens-Before 规则指定了两个操作间的执行顺序。如果一个操作 Happens-Before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

 

两个操作之间存在 Happens-Before 关系,并不意味着实际就是按 Happens-Before 规则指定的顺序来执行的。如果重排序后的执行结果与按 Happens-Before 规则执行的结果一致,那么 JMM 是允许这种重排序的。

 

As-If-Serial 保证了单线程内程序的执行结果不被改变(不管怎么重排序),Happens-Before 保证了多线程下的程序执行结果不被改变(不管怎么重排序)。这两者都是为了不改变程序执行结果的前提,尽可能提高程序执行的并行度。

 

总结:

虽然编译器、指令器可能会对代码进行重排序,但要遵守一定的规则。Happens-Before 规则就是限制了不能随便重排序,如果不符合 Happens-Before 规则,那么就可以按编译器、指令器要求重排序。

 

(2)Happens-Before 规则详情


Happens-Before 就是先行发生的意思。


一.程序次序规则

一个线程中的每个操作,先行发生于该线程中的任意后续操作。


二.锁规则

对一个锁的 unlock 操作先行发生于后面对该锁的 lock 操作。


三.volatile 变量规则

对一个 volatile 变量的写操作先行发生于后面对这个 volatile 变量的读操作。


四.传递规则

如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,那么就可以得出操作 A 先行发生于操作 C。


五.start()规则

如果线程 A 执行线程 B 的 start()方法启动线程 B,那么线程 A 执行线程 B 的 start()方法这一操作,先行发生于线程 B 的任意操作。


六.join()规则

如果线程 A 执行线程 B 的 join()方法并成功返回,那么线程 B 的任意操作先行发生于,线程 A 执行线程 B 的 join()方法的这一操作。

 

(3)volatile 如何保证有序性


一.Happens-Before 规则的 volatile 变量规则


程序中的代码如果满足上面这 8 条规则,就一定会保证指令的顺序。但是如果没满足上面的 8 条规则,那么就可能会出现指令重排。

 

如对一个 volatile 变量的写操作先行发生于后面对这个 volatile 变量的读操作。

 

二.volatile 型变量的特殊规则


volatile 型变量会禁止指令重排序优化。在有序性问题的例子一中,使用 volatile 修饰 flag 能禁止重排序避免逻辑异常。在有序性问题的例子二中,使用 volatile 修饰 instance 能禁止重排序避免异常。

 

10.volatile 的原理(Lock 前缀指令 + 内存屏障)


(1)Lock 前缀指令 + MESI 实现可见性


如果对 volatile 关键字修饰的变量执行写操作,那么 JVM 就会向 CPU 发送一条 Lock 前缀指令,将这个变量所在的缓存行数据写回到主内存中。

 

同时根据 MESI 缓存一致性协议,各个 CPU 会通过嗅探在总线上传播的数据,来检查该变量的缓存值是否过期。如果发现过期,CPU 就会将该变量所在的缓存行设置成无效状态。后续当这个 CPU 要读取该变量时,就会从主内存中加载最新的数据。

 

所以 Lock 前缀指令 + MESI 缓存一致性协议实现了 volatile 型变量的可见性。Lock 前缀指令会引起将 volatile 型变量所在的缓存行数据写回到主内存,MESI 缓存一致性协议可让 CPU 检查出哪些缓存被修改,同时令缓存失效。

 

(2)通过内存屏障实现禁止指令重排序


一.通过内存屏障来禁止某些指令重排序


加了 volatile 关键字的变量,可以保证前后的一些代码不会被指令重排。那么这个是如何做到的呢?volatille 是如何保证有序性的呢?

 

为了保证内存可见性,Java 编译器会在生成指令序列的适当位置,插入内存屏障指令来禁止特定类型的指令重排序。

 

二.JMM 的 4 种内存屏障指令


一.LoadLoad 屏障

Load1;LoadLoad;Load2

确保 Load1 数据的装载,先于 Load2 及所有后续装载指令的装载。也就是 Load1 对应的代码和 Load2 对应的代码,是不能指令重排的。

 

二.StoreStore 屏障

Store1;StoreStore;Store2

确保 Store1 数据刷新到主内存,先于 Store2 及所有后续存储指令的存储。

 

三.LoadStore 屏障

Load1;LoadStore;Store2

确保 Load1 数据的装载,先于 Store2 及所有后续的存储指令刷新到主内存。

 

四.StoreLoad 屏障

Store1;StoreLoad;Load2

确保 Store1 数据刷新到主内存,先于 Load2 及所有后续装载指令的装载。

 

三.JMM 制定的 volatile 重排序规则表


举例来说,第三行最后一个单元格的意思是:当第一个操作为普通变量的读或写时,如果第二个操作为 volatile 些,则编译器不能重排序这两个操作。



当第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。该规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。

 

当第二个操作是 volatile 写时,不管第一个操作是什么,都不能重排序。该规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。

 

当第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序。

 

四.JMM 内存屏障的插入策略


volatile 的作用就是对于 volatile 变量的读写操作,都会加入内存屏障。



每个 volatile 写操作前,加 StoreStore 屏障,禁止前面的普通写和它重排;

每个 volatile 写操作后,加 StoreLoad 屏障,禁止后面的 volatile 读/写和它重排;

每个 volatile 读操作后,加 LoadLoad 屏障,禁止后面的普通读和它重排;

每个 volatile 读操作后,加 LoadStore 屏障,禁止后面的普通写和它重排;


public class VolatileStoreStoreExample {    int a = 0;    volatile boolean flag = false;
//线程1执行writer()方法 public void writer() { a = 1;//普通写,操作1 //加入StoreStore屏障,避免操作1和操作2发生重排序,影响线程2的执行结果 flag = true;//volatile写,操作2 }
//线程2执行reader()方法 public void reader() { if (flag) {//操作3 int i = a * a;//操作4 ... } }}
public class VolatileStoreLoadExample { volatile int a = 0; volatile boolean flag = false;
//线程1执行writer()方法 public void writer() { a = 1;//volatile写,操作1 //加入StoreLoad屏障,避免操作1和操作2发生重排序,影响线程2的执行结果 flag = true;//volatile写,操作2 }
//线程2执行reader()方法 public void reader() { if (flag) {//操作3 int i = a * a;//操作4 ... } }}
复制代码


11.双重检查单例模式的 volatile 优化


(1)双重检查单例模式的实现缺陷


在下面利用双重检查创建单例的代码中,Singleton.getInstance()方法会先判断 inst 是否为空,如果为空则锁定 Singleton.class 并再次检查 inst 是否为空,如果还为空就创建一个 Singleton 的对象实例。

 

其中"inst = new Singleton()"会执行三个指令:

指令 1:分配对象的内存空间

指令 2:初始化对象

指令 3:设置 inst 变量指向指令 1 分配的内存地址

 

如果按照正常顺序来执行,那么是不会有问题的。但是由于指令 2 和指令 3 不存在依赖关系,所以编译器优化后可能进行重排序。于是执行顺序变为:指令 1 -> 指令 3 -> 指令 2。那么就可能发生:在线程 A 刚把 inst 指向对应地址后,线程 B 获取到执行权。然后线程 B 便获取到一个没有初始化的对象,从而产生空指针异常。


public class Singleton {    private static Singleton inst;      private Singleton() {                }      public static Singleton getInstance() {        if (inst == null) {            synchronized(Singleton.class) {                if (inst == null) {                    //开辟空间,inst指向地址,初始化                    inst = new Singleton();                }            }                }        return inst;    }}
复制代码


(2)双重检查单例模式的 volatile 优化


public class Singleton {    private static volatile Singleton inst;      private Singleton() {                }      public static Singleton getInstance() {        if (inst == null) {            synchronized(Singleton.class) {                if (inst == null) {                    //开辟空间,inst指向地址,初始化                    inst = new Singleton();                }            }                }        return inst;    }}
复制代码


文章转载自:东阳马生架构

原文链接:https://www.cnblogs.com/mjunz/p/18715765

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
JUC并发—volatile和synchronized原理_Java_不在线第一只蜗牛_InfoQ写作社区