一文说清楚Java中的volatile修饰符
在 Java 编程领域中,volatile
修饰符具备两大至关重要的功能:其一,它能够确保内存的可见性,使得一个线程对共享变量的修改能够及时被其他线程感知到;其二,volatile
可以有效防止指令重排序,保障程序在多线程环境下的执行顺序与代码逻辑顺序的一致性
部分内容来源:JavaGuide
Volatile关键字有什么作用
1.保证变量对所有线程的可见性:
当一个变量被声明为volatile的时候,它会保证这个变量的写操作立即刷入到主存,然后这个变量的读操作从主存中读取
2.禁止指令重排序优化:
通过读屏障和写屏障来保证我们的特定类型的指令重排序
volatile的使用场景
1.可以作为状态标志变量
例如
我们可以做任务的控制标志
我们可以当变量aaa=50的时候,我们才去往下执行逻辑
使用volatile
可以确保一个线程对变量的修改能够立刻对其他线程可见,而不会出现读到过期数据的情况。
2.单例模式(双重判定锁)
首先我们要知道,我们的JVM会有指令重排序
因为指令重排序,所以我们在多线程执行的时候可能会因为我们的指令重排序从而出问题
对象创建的三个步骤
1.分配内存:为对象分配内存空间
2.初始化对象:调用构造函数,初始化对象的成员变量
3.将对象引用指向分配的内存
重排成如下顺序:
分配内存
将instance指向分配的内存空间
初始化对象
问题所在
我们的这个线程指令重排后,我们这个是先给instance分配内存空间
此时另一个线程用get方法得到这个值,因为我们分配了内存空间不为null,所以另一个线程直接用这个没创建完全的对象来进行操作
但此时对象还没有初始化,就会导致程序抛出异常或不可预测的行为
1.我们用volatile来防止我们的指令重排
2.双重检查锁定:
- 第一次检查
if (instance == null)
是为了避免不必要的同步。 - 第二次检查
if (instance == null)
是为了在同步块内再次确认instance
是否为null
,确保只有一个线程能够初始化instanc
简单说一下对象创建的步骤
对象创建的三个步骤
1.分配内存:为对象分配内存空间
2.初始化对象:调用构造函数,初始化对象的成员变量
3.将对象引用指向分配的内存
是如何保证变量的可见性的?
volatile可以保证变量的可见性
如果我们声明变量为volatile,那么就指示JVM这个变量是共享且不稳定的
每次使用它都到主存内进行读取
volatile这个变量的意义是禁用CPU缓存
它指示这个变量是共享且不稳定的,所以每次使用它都必须到主存中进行读取
volatile可以保证数据的可见性,不能保证数据的原子性
synchronized既能保证可见性,又能保证原子性
volatile能保证线程安全吗
不能,保证内存的可见性和指令有序性(禁止指令重排序)
但是不能保证原子性,所以不能保证线程安全。
如何禁止指令重排序
如何防止JVM的指令重排序
我们对volatile修饰的变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序
例如:
因为JVM的指令重排特性,我们的执行顺序有可能会变成1->3->2
单线程下不会出问题
但是我们多线程的情况下就会出问题了
单例模式(双重判定锁)
首先我们要知道,我们的JVM会有指令重排序
因为指令重排序,所以我们在多线程执行的时候可能会因为我们的指令重排序从而出问题
对象创建的三个步骤
分配内存:为对象分配内存空间
初始化对象:调用构造函数,初始化对象的成员变量
将对象引用指向分配的内存空间
重排成如下顺序
分配内存
将instance指向分配的内存空间
初始化对象
问题所在
我们的这个线程指令重排后,我们这个是先给instance分配内存空间
此时另一个线程用get方法得到这个值,因为我们分配了内存空间不为null,所以另一个线程直接用这个没创建完全的对象来进行操作,但此时对象还没有初始化,就会导致程序抛出异常或不可预测的行为
(内存屏障机制)说一下volatile的实现原理
volatile 关键字在底层的实现主要是通过内存屏障(memory barrier)来实现的。
内存屏障是一种 CPU 指令
用于强制执行 CPU 的内部缓存与主内存之间的数据同步
在 Java 中,当线程读取一个 volatile 变量时,会从主内存中读取变量的最新值,并把它存储到线程的工作内存中。当线程写入一个 volatile 变量时,会把变量的值写入到线程的工作内存中,并强制将这个值刷新到主内存中。这样就保证了 volatile 变量的可见性和有序性。
在 Java 5 之后,volatile 的实现还引入了“内存屏障插入”的机制,内存屏障插入是指在指令序列中插入内存屏障以保证变量的可见性和有序性。
主内存和工作内存
Java 内存模型规定,所有的变量(实例变量和静态变量)都必须存储在主内存中,每个线程也会有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。
线程不能直接读写主内存中的变量,如下图所示:
这样设计的目的主要是为了提升程序的并发性能以及多线程之间的可见性问题。
主内存是 Java 虚拟机中的一块共享内存,所有线程都可以访问。而每个线程还有自己的工作内存,线程的工作内存中存储了主内存中的变量副本的拷贝。这样做的好处是,线程之间不需要同步所有变量的读写操作,只需要同步主内存中的变量即可,这样可以提高程序的执行效率。同时,由于每个线程都有自己的工作内存,因此线程之间的变量操作互相不影响,从而提高了程序的并发性能。
内存屏障
内存屏障是一种硬件机制,用于控制 CPU 缓存和主内存之间的数据同步。在 Java 中,内存屏障通常有两种:读屏障和写屏障
有内存屏障的地方会禁止指令重排序,即屏障下面的代码不能跟屏障上面的代码交换执行顺序。
在有内存屏障的地方,线程修改完共享变量以后会马上把该变量从本地内存写回到主内存,并且让其他线程本地内存中该变量副本失效
volatile和Synchronized比较
解决的问题不同
Synchronized解决了多线程访问共享资源时可能出现的竞态条件和数据不一致的问题,保证了线程安全性,保证同一时间只有一个线程访问资源
Volatile解决了变量在多线程环境下的可见性和有序性问题,确保了变量的修改对其他线程是可见的,保证变量是最新修改的值防止丢失修改
Synchronized
保证了多个线程访问共享资源时的互斥性,即Synchronized是一种排他性的同步机制,同一时刻只允许一个线程访问共享资源。通过对代码块或方法添加Synchronized关键字来实现同步。
Volatile
Volatile是一种轻量级的同步机制,用来保证变量的可见性和禁止指令重排序。当一个变量被声明为Volatile时,线程在读取该变量时会直接从内存中读取,而不会使用缓存,时对该变量的写操作会立即刷回主内存,而不是缓存在本地内存中。