一、volatile定义
volatile是Java虚拟机提供的轻量级的同步机制,其中具有的特征为:
- 保证可见性
- 不保证原子性
- 禁止指令重排
二、JMM
在验证volatile特征之前,需要先了解另外一个知识点—JMM
2.1. JMM的定义
JMM(Java Memory Model):Java内存模型,它是一种抽象的概念,
并不是真实存在的
,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。其具有一下特征:
可见性
原子性
有序性
由于JMM保证了三个特性,而volatile只能保证两个,所以volatile是低配版的同步机制。
2.2. JMM关于同步的规定
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
我们知道JVM中运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而JMM(Java内存模型)中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回内存,不能直接操作内存中的变量,各个线程中的工作内存中存储着内存中的变量副本拷贝,因此不同的线程之间无法访问对方的工作内存,线程间的通信必须通过主内存来完成,访问过程如下图:
三、验证volatile的可见性
我们先来看看当没有加volatile时的代码:
@Slf4j
public class VisibilityVolatile {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(()->{
log.info(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.add();
},"AAA").start();
while (myData.number == 0 ){
//main线程就一直在这里循环,直到number不再等于0
}
}
}
class MyData{
int number = 0;
public void add(){
this.number = 60;
}
}
/**
* 运行结果:
* [AAA] INFO com.glw.myVolatile.atomicity.AtomicityVolatile - AAA come in
*/
在此我们得到以上结果,并且一直处于循环未结束状态。那么为什么会有以上的结果???
当我们启动一个线程AAA,线程AAA将数据拷贝到自己的工作内存,在其本地内存去修改myData.number的值后,再写回主内存,但是并没有通知主线程,以至于主线程并不知道内存中的值已经被修改,一直处于循环状态…
那么我们再来看看加上volatile后的运行结果
@Slf4j
public class VisibilityVolatile {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(()->{
log.info(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.add();
},"AAA").start();
while (myData.number == 0 ){
//main线程就一直在这里循环,直到number不再等于0
}
log.info("main线程结束...");
}
}
class MyData{
volatile int number = 0;
public void add(){
this.number = 60;
}
}
/**
* 运行结果:
* [AAA] INFO com.glw.myVolatile.atomicity.AtomicityVolatile - AAA come in
* [main] INFO com.glw.myVolatile.atomicity.AtomicityVolatile - main线程结束...
*/
由此我们能证明volatile可以保证当数据修改后可以通知其他线程,即—可见性
四、验证volatile的不保证原子性
4.1 不保证原子性的案例演示:
@Slf4j
public class AtomicityVolatile {
public static void main(String[] args) {
MyData2 myData2 = new MyData2();
for(int i = 1; i <= 20; i++){
new Thread(()->{
for(int j = 1; j <= 1000; j++){
myData2.addPlus();
}
},String.valueOf(i)).start();
}
//线程默认有两个线程,分别为:main线程和GC线程,等以上20个线程计算完成
while(Thread.activeCount()>2){
Thread.yield();
}
log.info(Thread.currentThread().getName()+"\t finally number value:" + myData2.number);
}
}
class MyData2{
volatile int number = 0;
public void add(){
this.number = 60;
}
public void addPlus(){
number++;
}
}
/**
* 运行结果:
* [main] INFO com.glw.myVolatile.atomicity.AtomicityVolatile - main finally number value:19500
* 每次运行结果都会不同
*/
此时,我们创建了20个线程来计算number的值,如果可以保证原子性的话,最后的结果应该为20000,但是我们看到上面的结果为19500,计算结果是不对的。那么为什么结果是这个样子呢???
因为number++在多线程下是非线程安全的
,那么为什么number++在多线程下是非线程安全的呢,首先需要了解一下number++底层所做的工作,我们以简单的Test为例子:
public class Test {
volatile int n = 0;
public void add() {
n++;
}
}
/**
* public void add();
* Code:
* 0: aload_0
* 1: dup
* 2: getfield #2 // Field n:I
* 5: iconst_1
* 6: iadd
* 7: putfield #2 // Field n:I
* 10: return
*/
各阶段的解释为:
-
aload_0:从局部变量0中装载引用类型值;
-
dup:复制栈顶部一个字长的内容;
在此处,number++被分为了3个指令:
- 执行getfield拿到原始number;
- 执行iadd进行加1操作;
- 执行putfield写把累加后的值写回
因为很多值在执行putfield这一步操作写回去的时候,可能线程的调度被挂起了,刚好没有收到最新值的通知,有近乎纳秒级别的时间差,一写就出现了写覆盖,就把别人的值覆盖掉了,就出现了丢失写值的情况。
解决原子性问题我们知道可以利用加synchronized解决,但是对于简单的number++操作来说,使用synchronized就好比杀鸡用牛刀,小材大用,那么如何不加synchronized解决???
答案是:使用JUC下的AtomicInteger
。下面我们来看代码:
@Slf4j
public class AtomicityVolatile {
public static void main(String[] args) {
MyData2 myData2 = new MyData2();
for(int i = 1; i <= 20; i++){
new Thread(()->{
for(int j = 1; j <= 1000; j++){
myData2.addPlus();
myData2.addMyAtomic();
}
},String.valueOf(i)).start();
}
//线程默认有两个线程,分别为:main线程和GC线程,等以上20个线程计算完成
while(Thread.activeCount()>2){
Thread.yield();
}
log.info(Thread.currentThread().getName()+"\t int type, finally number value:" + myData2.number);
log.info(Thread.currentThread().getName()+"\t AtomicInteger type, finally number value:" + myData2.atomicInteger);
}
}
class MyData2{
volatile int number = 0;
public void add(){
this.number = 60;
}
public void addPlus(){
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement(); //调用一次就会+1
}
}
/**
* [main] INFO com.glw.myVolatile.atomicity.AtomicityVolatile - main int type, finally number value:19428
* [main] INFO com.glw.myVolatile.atomicity.AtomicityVolatile - main AtomicInteger type, finally number value:20000
*/
从以上结果我们可以看出使用AtomicInteger是可以达到预期的效果的,即是可以解决原子性问题。其主要内部原因为CAS自旋锁的原因,后面我们会讲到什么是自旋锁,以及AtomicInteger为什么使用的是CAS而不是synchronized。
五、禁止指令重排
5.1 JMM的有序性
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分一下3种:
5.2 指令重排会出现的情况:
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致;
- 处理器在进行重排时必须要考虑指令之间的
数据依赖性
; - 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
我们具体来看一下优化重排会出现的结果,我们以代码为例:
public class ResortSeq {
int a = 0;
boolean flag = false;
public void method01(){
a = 1;
flag = false;
}
public void method02(){
if(flag){
a = a+5;
}
}
}
以上的代码会因为指令重排的原因,可能会先执行flag=true,后执行a=1;这时还未来得及执行a=1,导致method02执行的结果错误,对于这种,我们就应该禁止指令重排。这时,volatile就实现了禁止指令重排的优化
,从而避免多线程环境下程序出现乱序执行的情况。
5.3 内存屏障
这时我们就需要出现内存屏障这个词,内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:
-
保证特定操作的执行顺序
-
保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier则会告诉编译器
通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。下面我们来看一下大概的结构:
左半部分为对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存;
右半部分为对volatile变量进行读操作时,会在读操作前加入一条Load屏障指令,从主内存中读取共享变量。
那么线程安全性获得保证的做法:
- 工作内存与主内存同步延迟现象导致的可见性问题,可以使用volatile或synchronized关键字解决,他们都可以使一个线程修改后的变量立即对其他线程可见;
- 对于指令重排导致的可见性问题和有序性问题,可以利用volatile关键字解决,因为volatile的另一个作用就是禁止重排序优化。
5.4 volatile的使用情况
单例模式下的DCL模式:
@Slf4j
public class SingletonVolatile {
private static SingletonVolatile instance = null;
private SingletonVolatile(){
log.info(Thread.currentThread().getName() + "我是单例构造方法");
}
public static SingletonVolatile getInstance(){
if(instance == null){
instance = new SingletonVolatile();
}
return instance;
}
public static void main(String[] args) {
for(int i = 1; i <= 10; i++){
new Thread(() -> {
SingletonVolatile.getInstance();
},String.valueOf(i)).start();
}
}
}
/**
* [4] INFO com.glw.myVolatile.SingletonVolatile - 4我是单例构造方法
* [3] INFO com.glw.myVolatile.SingletonVolatile - 3我是单例构造方法
* [5] INFO com.glw.myVolatile.SingletonVolatile - 5我是单例构造方法
* [6] INFO com.glw.myVolatile.SingletonVolatile - 6我是单例构造方法
* [9] INFO com.glw.myVolatile.SingletonVolatile - 9我是单例构造方法
* [7] INFO com.glw.myVolatile.SingletonVolatile - 7我是单例构造方法
* [10] INFO com.glw.myVolatile.SingletonVolatile - 10我是单例构造方法
* [2] INFO com.glw.myVolatile.SingletonVolatile - 2我是单例构造方法
* [1] INFO com.glw.myVolatile.SingletonVolatile - 1我是单例构造方法
* [8] INFO com.glw.myVolatile.SingletonVolatile - 8我是单例构造方法
*/
从上面看出,当我们不使用关键字时时,单例模式的构造方法被调用了10次,这是违反单例模式的规则的。。。
使用synchronized时:
@Slf4j
public class SingletonVolatile {
private static SingletonVolatile instance = null;
private SingletonVolatile(){
log.info(Thread.currentThread().getName() + "我是单例构造方法");
}
//DCL (Double Check Lock双端检锁机制) 也就是在进来之前和进来之后判断两次
public static SingletonVolatile getInstance(){
synchronized (SingletonVolatile.class) {
if (instance == null) {
instance = new SingletonVolatile();
}
}
return instance;
}
public static void main(String[] args) {
for(int i = 1; i <= 10; i++){
new Thread(() -> {
SingletonVolatile.getInstance();
},String.valueOf(i)).start();
}
}
}
/**
*[1] INFO com.glw.myVolatile.SingletonVolatile - 1我是单例构造方法
*/
当加入synchronized时,从上面的结果看是可以达到效果的,但是其正确性不能百分之百保证,原因是因为底层有指令重排,,,在于某个线程执行到第一次检测,读到的instance不为null时,instance的引用对象可能没有完成初始化。指令重排只会保证串行予以的执行的一致性(单线程),但并不会关心多线程间的语义一致性
。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全的问题,所以加入volatile的原因就是为了禁止底层的指令重排,从而保证线程的安全。
所以上面的代码需要修改为:
private static volatile SingletonVolatile instance = null;
六、CAS
6.1 CAS(CompareAndSet):比较并交换
CAS,它是一条CPU并发原语。
功能:判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
以一个实例为例:
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2023) + "\t" + atomicInteger.get());
}
}
/**
* true 2019
* false 2019
*/
简单解释为如果拿到的值与期望值一致,就修改值。即当拿到的值如果为5,就修改为2019。
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法,调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题
。
6.2 AtomicInteger为什么使用的是CAS而不是synchronized
使用synchronized的话,是多个线程的抢用一份资源,只允许一个线程运行,虽然一致性保证了,但是会导致并发量下降,而CAS底层原理是Unsafe,并且不加锁,保证一致性,允许多个线程同时操作,并发量得到保障,但是循环比较。
6.3 CAS底层原理
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int getAndIncrement() {
// 在内存中的偏移地址
return unsafe.getAndAddInt(this, valueOffset, 1);
}
什么是Unsafe类?
Unsafe:是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务。
6.4 CAS缺点
- 循环时间长开销很大 (执行时间的do…while,导致自旋)
- 只能保证一个共享变量的原子操作
- 引出来的ABA问题
七、ABA问题
7.1 ABA原理
ABA问题就是所说的狸猫换太子。比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一写操作将值变成了B,然后线程two又将位置V的数据变成A,这时线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
7.2 ABA问题的解决
7.2.1 AtomicReference原子引用
@Slf4j
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zs = new User("zs",18);
User li = new User("li",22);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(zs);
log.info(atomicReference.compareAndSet(zs,li) + "\t" + atomicReference.get().toString());
}
}
@Data
@AllArgsConstructor
class User{
String userName;
Integer age;
}
/**
* [main] INFO com.glw.myVolatile.aba.AtomicReferenceDemo - true User(userName=li, age=22)
*/
7.2.2 ABA问题的解决(理解原子引用+新增一种机制,就是修改版本号(类似时间戳))
@Slf4j
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
//理解原子引用+新增一种机制,就是修改版本号(类似时间戳)
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
//暂停1秒钟t2线程,保证上面的t1线程完成了一次ABA操作
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
/**
* [t2] INFO com.glw.myVolatile.aba.ABADemo - true 2019
*/
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//以下为ABA问题的解决
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
log.info(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
log.info(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
log.info(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
log.info(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
log.info(Thread.currentThread().getName() + "\t修改成功否:" + result + "\t当前最新版本号:" + atomicStampedReference.getStamp());
log.info("当前实际最新值:" + atomicStampedReference.getReference());
}, "t4").start();
}
}
/**
* [t3] INFO com.glw.myVolatile.aba.ABADemo - t3 第1次版本号:1
* [t4] INFO com.glw.myVolatile.aba.ABADemo - t4 第1次版本号:1
* [t3] INFO com.glw.myVolatile.aba.ABADemo - t3 第2次版本号:2
* [t3] INFO com.glw.myVolatile.aba.ABADemo - t3 第3次版本号:3
* [t4] INFO com.glw.myVolatile.aba.ABADemo - t4 修改成功否:false 当前最新版本号:3
* [t4] INFO com.glw.myVolatile.aba.ABADemo - 当前实际最新值:100
*/