Java允许线程访问共享变量。为了确保共享变量能被一致、可靠地更新,线程必须确保 它是排他性地使用此共享变量,通常都是获得对这些共享变量强制排他性的同步锁。Java编程语言提供了另一种机制,volatile域变量,对于某些场景的使用 这会更加的方便。可以把变量声明为volatile,以让Java内存模型来保证所有线程都能看到这个变量的同一个值。
volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或同步方法) 和 volatile变量,相比于synchronized(synchronized通常被称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
注:Volatile只能修饰变量。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
还记得并发编程中的可见性、原子性、有序性吗?如果忘记可以到这里重温复习【并发编程】1 synchronized底层实现原理、Java内存模型JMM;可重入、不可中断、monitor、CAS、乐观锁和悲观锁;对象内存结构、Mark Word、synchronized锁升级
和Java内存结构不同,Java内存模型是一套规范、是标准化的,屏蔽掉了底层不同计算机的区别(Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则)。
JMM描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。细节如下。
对于普通的共享变量x,线程1将其修改为某个值 发生在线程1的工作内存中,此时还未同步到主内存中去;而线程2已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程中的不可见问题,较粗暴的方式自然就是加锁,但此处使用synchronized或Lock这些方式 太重量级了,比较合理的方式是 volatile。
注意:JMM是抽象的内存模型,所谓的主内存、工作内存都是抽象概念,不一定就真实地对应cpu缓存、物理内存。
具体细节见【并发编程】1 synchronized底层实现原理、Java内存模型JMM;可重入、不可中断、monitor、CAS、乐观锁和悲观锁;对象内存结构、Mark Word、synchronized锁升级
用 volatile 修饰共享变量,当一个线程对共享变量进行了修改,另外的线程可以立即看到修改后的最新值。CPU都是有行缓存的,volatile能让行缓存无效,因此能读到内存中最新的值。
详解:
当一个线程把主内存中的共享变量读取到自己的本地内存中,然后做了更新。在还没有把共享变量刷新的主内存的时候,另外一个线程是看不到的。如何把修改后的值刷新到主内存中?
现代的处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,较少对内存总线的占用。但是什么时候写入到内存是不知道的。
所以就引入了volatile,volatile是如何保证可见性的呢?
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。
首先看下不加volatile的情况
1)修改pom.xml文件,添加依赖
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.14</version>
</dependency>
2)编写代码
//为什么使用此工具,直接运行代码不行吗? 只有使用大量的线程去执行,才能测试出指令重排序的效果
@JCStressTest //用并发压缩工具jcstress 测试类方法
@Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ok") //@Outcome 对输出结果进行处理, "0, 0", "1, 1", "0, 1" 打印ok
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestOrdering {
int x;
int y;
@Actor
public void actor1() { //写
x = 1;
y = 1;
}
@Actor
public void actor2(II_Result r) { //读
r.r1 = y;
r.r2 = x;
}
}
mvn clean install,java -jar xxx\target\jcstress.jar
3)查看项目对应路径results目录下的html文件,用浏览器打开,观察运行结果
根据运行结果,出现了”1,0“这种情况,说明actor1()
方法先给y赋值、再给x赋值,确实发生指令重排序(指令重排序是为了实现指令流水线,属于计算机组成原理的内容)
如何解决:在变量上添加volatile,禁止指令重排序,则可以解决问题(volatile原理就是加了一些屏障,使屏障后的代码一定不会比屏障前的代码先执行,从而实现有序性)
//省略部分代码
public class TestOrdering {
int x;
volatile int y;
//其他不变,省略
}
volatile的底层原理是,给共享变量加上不同的屏障,保证指令不会发生重排序
//省略部分代码
public class TestOrdering {
int x;
volatile int y;
//其他不变,省略
}
写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下,但无法阻止下方的指令往上走,y=1可以往上走、r.r1=y可以往下走。
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为如下三种:
1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。
当变量声明为volatile时,Java编译器在生成指令序列时,会插入内存屏障指令,通过内存屏障指令来禁止重排序。
JMM内存屏障插入策略如下:
Volatile写插入内存屏障后生成指令序列示意图:
Volatile读插入内存屏障后生成指令序列示意图:
通过上面这些我们可以得出如下结论:编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。
从一个最经典的例子来分析重排序问题,以单例模式的双重检查锁实现(线程安全)为例
public class Singleton {
public volatile static Singleton uniqueInstance;
/**
* 构造函数私有,禁止外部实例化
*/
private Singleton() {}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
为什么使用volatile关键字修饰uniqueInstance实例变量?
因为uniqueInstance = new Singleton()
这段代码执行时分为三步:
正常的执行顺序当然是 1-->2-->3,但由于JVM具有指令重排的特性,执行顺序可能变成 1-->3-->2。单线程环境时,指令重排没有什么问题;多线程环境下,会导致有些线程可能会获取到还没初始化的实例。如线程A只执行了1和3,此时线程B来调用getUniqueInstance()
,发现 uniqueInstance
不为空,便获取 uniqueInstance
实例,但是其实此时的 uniqueInstance
还没有初始化。
解决方法就是 加一个 volatile
关键字修饰 uniqueInstance
,volatile
会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。
volatile可以保证可见性、有序性,无法保证原子性,所以是线程不安全的(它对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性,因为本质上i++是读、写两次操作)。要保证多线程的原子性, 可以通过AtomicInteger或者Synchronized来实现,本质上就是CAS操作(见下文5.2)
【synchronized可以保证可见性、原子性、有序性,因为锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性】
java.util.concurrent.里面的高级线程安全数据结构像ConcurrentHashMap以及java.util.concurrent.atomic.等的实现都用到了volatile。可以多看看这些类的实现,以加深对volatile的理解和运用。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用"内存屏障"来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
计算机科学里面,为了解决复杂性,都会分层。正如一个名人所说:"计算机的任何问题都可以通过增加一个虚拟层来解决"("All problems in computer science can be solved by another level of indirection")。volatile虚拟机层引入的,解决语言层面的问题,那么它的实现,必然是靠下一层的支持,也就是需要汇编或者说处理器指令的支持来实现,volatile是靠内存屏障和MESI(缓存一致性协议)来达成的它的作用的。
内存屏障(Memory Barriers)是处理器提供的一组内存操作指令,它的作用是限制内存操作的顺序,也就是说内存屏障像一个栅栏一样,它前面的指令要在它后面的指令之前完成;还能强制把缓存写入到主存;再有的就是触发缓存一致性,就是当有写变量时,会把其他CPU核心的缓存变为无效。
对变量可见性有要求、对读取顺序没要求的情况下。如 多线程情况下的标志位
例如 number++ 不是一个原子性操作,由读取、加、赋值3步组成,下列程序可能返回多种结果,2677、4202、4722、4910、5000
public class Test02Atomicity {
private volatile static int number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
number++;
}
};
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread thread : list) {
//让主线程等待自己创建的线程执行完毕,5个线程执行完 再取number的值
thread.join();
}
System.out.println("number=" + number); //值有多种可能,2677、4202、4722、4910、5000
}
}
1)采用synchronized
public class Test02Atomicity {
private static int number = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (obj) {
number++; //synchronized保证 number++ 为原子操作。线程获取不到锁 就会等待
}
}
};
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread thread : list) {
//让主线程等待自己创建的线程执行完毕
thread.join();
}
System.out.println("number=" + number); //5000
}
}
2)采用Lock
public class Test02Atomicity {
private static int number = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
try {
lock.lock();
number++;
} finally {
lock.unlock();
}
}
};
List<Thread> list = new ArrayList<>();
//使用5个线程来进行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread thread : list) {
//让主线程等待自己创建的线程执行完毕,5个线程执行完 再取number的值
thread.join();
}
System.out.println("number=" + number); //5000
}
}
3)采用java并发包中的原子操作类,原子操作类是通过CAS的方式来保证其原子性的
public class Test02Atomicity {
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet(); //AtomicInteger的incrementAndGet()可保证变量赋值的原子性
}
};
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread thread : list) {
//让主线程等待自己创建的线程执行完毕
thread.join();
}
System.out.println("atomicInteger=" + atomicInteger.get()); //5000
}
}
参考 https://zhuanlan.zhihu.com/p/633426082、https://blog.csdn.net/u012723673/article/details/80682208、https://blog.csdn.net/xinghui_liu/article/details/124379221
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。