JUC - volatile
1. volatile
volatile 是轻量级的 synchronized,在多处理器环境下,可以保证共享变量的可见性。它不会引起线程上下文的切换和调度,正确的使用 volatile,比 synchronized 的使用和执行成本更低。
可见性:
- 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改一个共享变量时,另一个线程马上就能看到。比如:用volatile修饰的变量, 就会具有可见性。
- volatile 修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如
volatile int a=0;
之后有一个操作a++;
这个变量a具有可见性,但是a++
依然是一个非原子操作,也就是这个操作同样存在线程安全问题。 - 在Java中,volatile、synchronized 和 final 实现可见性。
原子性:
- 原子是世界上的最小单位,具有不可分割性。比如
a=0; (a非long和double类型)
,这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++;
这个操作实际是a=a+ 1;
是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(synchronized)
来让它变成一个原子操作。 一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类, 我们可以通过阅读API来了解这些原子类的用法。比如:AtomiclInteger、 AtomicLong、 AtomicReference等。 - 在Java中,synchronized 和在 lock、unlock 中操作保证原子性。
有序性:
- Java语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含"禁止指令重排序”的语义,synchronized 是由”一个变量在同一一个时刻只允许一条线程对其进行 lock 操作"这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
- Java语言提供了一种稍弱的同步机制,即 volatile 变量, 用来确保将变量的更新操作通知到其他线程。当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
- 在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 synchronized 关键字更轻量级的同步机制。
当一个变量定义为 volatile 之后,将具备两种特性:
- 保证此变量对所有的线程的可见性,这里的"可见性”, 如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
- 禁止指令重排序优化。有volatile修饰的变量, 赋值后多执行了一个
load addl $0x0, (%esp)
操作, 这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
1.1 可见性
可见性测试
/**
* Volatile关键字的使用
*/
class MyTest {
int number = 0;
//volatile int number = 0;
public void setTo60(){
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
volatileVisibilityDemo();
}
/**
* volatile 可以保证可见性, 及时通知其他线程 主物理内存的值 已被修改
*/
private static void volatileVisibilityDemo() {
System.out.println("===可见性测试====");
MyTest myTest = new MyTest();
// 启动一个线程 操作主内存中的共享数据 number
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 执行");
// 更新number的值
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myTest.setTo60();
System.out.println(Thread.currentThread().getName()+"\t 更新number值"+ myTest.number);
}).start();
// 保证上面的线程已经执行完毕, 再进行main线程的执行
while (myTest.number == 0){
// main线程中number的数据出现死循环,说明读取到的内容一直为0
// System.out.println(Thread.currentThread().getName());
}
// main线程
System.out.println(Thread.currentThread().getName()+"\t main线程中 获取number值"+ myTest.number);
}
}
MyTest类是资源类,一开始 number 变量没有用 volatile 修饰,所以程序运行的结果是:
可⻅性测试
ThreadA 执行
ThreadA 更新number值: 60
虽然一个线程把 number 修改成了 60 ,但是main线程持有的仍然是最开始的 0,所以一直循环,程序不会结束。
如果对 number 添加了volatile
修饰,即volatile int number = 0;
,其运行结果是:
可⻅性测试
ThreadA 执行
ThreadA 更新number值: 60
main main获取number值: 60
可⻅某个线程对 number 的修改,会立刻反映到主内存上。
1.2 原子性
原子性指的是?
不和分割,完整性,也即某个线程正则做某个具体业务时,中间不可以被加塞或者被分割。
需要整体完整,要么同时成功,要么同时失败。
/**
* Volatile关键字的使用
*/
class MyTest{
//int number = 0;
volatile int number = 0;
public void setTo60(){
this.number = 60;
}
public void addPlusPlus(){
number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
//volatileVisibilityDemo();
atomicDemo();
}
/**
* 原子性测试
* 需求: 启动20个线程, 每个线程执行1000次 number++操作, 问最终20个线程执行完毕后, number值是多少?
*/
private static void atomicDemo() {
System.out.println("===原子性测试====");
MyTest myTest = new MyTest();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myTest.addPlusPlus(); // number++;
}
}, String.valueOf(i)).start();
}
//main线程
//保证上面的线程已经执行完毕, 再进行main线程的执行, 执行线程礼让操作
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t main线程中获取number值"+ myTest.number);
}
}
volatile并不能保证操作的原子性 。这是因为,比如一条number++
的操作,会形成 3 条指令。
javap -c 包名.类名
javap -c MyTest
public void addPlusPlus();
Code:
0: aload_
1: dup
2: getfield # 2 // Field number:I // 读
5: iconst_1 // ++常量 1
6: iadd // 加操作
7: putfield # 2 // Field number:I // 写操作
10: return
假设有 3 个线程,分别执行number++
,都先从主内存中拿到最开始的值,number=0
,然后三个线程分别进行操作。假设线程 0 执行完毕,number=1
,也立刻通知到了其它线程,但是此时线程 1 、 2 已经拿到了number=0
,所以结果就是写覆盖,线程1、2将 number 变成 1 。
解决的方式就是:
- 对
addPlusPlus()
方法加锁。 - 使用
java.util.concurrent.AtomicInteger
类。
/**
* Volatile关键字的使用
*/
class MyTest{
//int number = 0;
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void setTo60(){
this.number = 60;
}
public void addPlusPlus(){
number++;
}
public void atomicPlusPlus(){
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
//volatileVisibilityDemo();
atomicDemo();
}
/**
* 原子性测试
* 需求: 启动20个线程, 每个线程执行1000次 number++操作, 问最终20个线程执行完毕后, number值是多少?
*/
private static void atomicDemo() {
System.out.println("===原子性测试====");
MyTest myTest = new MyTest();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myTest.addPlusPlus();
myTest.atomicPlusPlus(); // atomicInteger++
}
}, String.valueOf(i)).start();
}
//main线程
//保证上面的线程已经执行完毕, 再进行main线程的执行, 执行线程礼让操作
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t main线程中获取number值"+ myTest.number);
System.out.println("最终20个线程执行完毕后,atomicInteger值为多少 = " + myTest.atomicInteger);
}
}
结果:可⻅,由于volatile
不能保证原子性,出现了线程重复写的问题,最终结果比 20000 小。而AtomicInteger
可以保证原子性。
原子性测试
main int类型最终number值: 17751
main AtomicInteger类型最终number值: 20000
1.3 有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下三种:
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致;
- 处理器在进行重排序时必须要考虑指令之间的数据依赖性;
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
volatile可以保证有序性 ,也就是防止指令重排序 。
所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。
观看下面代码,在多线程场景下,说出最终值a的结果是多少? 5 或者 6
我们采用 volatile 可实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
public class ResortSeqDemo {
int a = 0;
volatile boolean flag = false;
/*
多线程下flag=true可能先执行,还没走到a=1就被挂起。
其它线程进入method02的判断,修改a的值=5,而不是6。
*/
public void method01() {
a = 1;
//code2
flag = true;
//code1
//code3
}
public void method02(){
if (flag){
a += 5;
System.out.println("*****最终值a: " + a);
}
}
public static void main(String[] args) {
ResortSeqDemo resortSeq = new ResortSeqDemo();
new Thread(()->{resortSeq.method01();},"ThreadA").start();
new Thread(()->{resortSeq.method02();},"ThreadB").start();
}
}
为什么 volatile 可实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象?
我们先来了解一个概念, 内存屏障 (Memory Barrier)又称内存栅栏,是一个CPU指令,volatile
底层就是用CPU的内存屏障(Memory Barrier)指令来实现的,它有两个作用
- 一个是保证特定操作的顺序性
- 二是保证变量的可⻅性
由于编译器和处理器都能够执行指令重排优化。所以,如果在指令间插入一条Memory Barrier
则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier
指令重排序,也就是说通过插入内存屏障可以禁止在内存屏障前后的指令进行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本。
1.4 volatile 应用场景
单例模式的安全问题
- 传统场景
/**
* 单例模式的安全问题
*/
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t SingletonDemo构造方法执行了");
}
public static SingletonDemo getInstance(){
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
// main线程操作
public static void main(String[] args) {
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
}
}
测试结果:
- 改为多线程操作测试
/**
* 单例模式的安全问题
*/
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t SingletonDemo构造方法执行了");
}
public static SingletonDemo getInstance(){
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
// main线程操作
public static void main(String[] args) {
// 多线程操作
for (int i = 1; i <= 100000; i++) {
new Thread(()->{
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
测试结果:
main SingletonDemo构造方法执行了
main SingletonDemo构造方法执行了
main SingletonDemo构造方法执行了
显然,打破了单例模式的规则。
- 调整后,采用常⻅的 DCL(Double Check Lock)双端检查模式 加了同步,但是在多线程下依然会有线程安全问题。
/**
* 单例模式的安全问题
*/
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t SingletonDemo构造方法执行了");
}
/**
* DCL Double Check Lock 双端检查锁
* @return
*/
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class){
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//多线程操作
for (int i = 1; i <= 100000; i++) {
new Thread(()->{
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
可能出现抢占问题,这个漏洞比较难捕捉,但是是存在的。
instance=new SingletonDemo();
可以大致分为三步
javap -c SingletonDemo.class
instance = new SingletonDemo();
public static thread.SingletonDemo getInstance();
Code:
0: getstatic #11 // Field instance:Lthread/SingletonDemo;
3: ifnonnull 37
6: ldc #12 // class thread/SingletonDemo
8: dup
9: astore_
10: monitorenter
11: getstatic #11 // Field instance:Lthread/SingletonDemo;
14: ifnonnull 27
17: new #12 // class thread/SingletonDemo 步骤 1
20: dup
21: invokespecial #13 // Method "<init>":()V 步骤 2
24: putstatic #11 // Field instance:Lthread/SingletonDemo;步骤 3
底层 Java Native Interface 中的C语言代码内容,开辟空间的步骤
memory = allocate(); // 步骤1. 分配对象内存空间
instance(memory); // 步骤2. 初始化对象
instance = memory; // 步骤3. 设置instance指向刚分配的内存地址,此时 instance != null
剖析:
在多线程的环境下,由于有指令重排序的存在,DCL(双端检锁)机制不一定线程安全,我们可以加入volatile
可以禁止指令重排。
原因在与某一个线程执行到第一次检测,读取到的instance
不为null
时,instance 的引用对象可能没有完成初始化。
memory = allocate(); // 步骤1. 分配对象内存空间
instance(memory); // 步骤2. 初始化对象
instance = memory; // 步骤3. 设置instance指向刚分配的内存地址,此时 instance != null
步骤2和步骤3 不存在数据依赖关系,而且无论重排前还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); // 步骤1. 分配对象内存空间
instance = memory; // 步骤3. 设置instance指向刚分配的内存地址,此时 instance != null
instance(memory); // 步骤2. 初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程),并不关心多线程的语义一致性。所以,当一条线程访问 instance 不为 null 时,由于 instance 实例未必已初始化完成,也就造成了线程安全问题。
/**
* DCL Double Check Lock 双端检查锁
* @return
*/
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class){
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
如果发生指定重排,那么
- 此时内存已经分配,那么
instance = memory
不为 null。 - 若遇到线程此时挂起,那么
instance(memory)
还未执行,对象还未初始化。 - 导致了
instance!=null
,所以两次判断都跳过,最后返回的instance
没有任何内容,还没初始化。
解决的方法就是对 singletonDemo 对象添加上 volatile 关键字,禁止指令重排。
private volatile static SingletonDemo instance = null;