并发编程的艺术-JAVA内存模型
并发编程的艺术-JAVA内存模型
1.JMM
1.1 什么是JMM?
1.2 可见性分析与解决
1.2.1 CAS
1.2.2 ABA
1.3 什么是原子性?
1.4 什么是连续性?
2.Volatile
2.1 什么是Volatile?
2.2 不保证原子性原因及分析
2.3 不保证原子性解决办法
1.1 什么是JMM?
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。
先看看JMM的特性:
可见性
原子性
有序性
1.2 可见性分析与解决
大家都知道,数据是存在主内存中的,线程想要操作主内存,需要先读取主内存中的数据,然后操作后再写入主内存,很简单的流程,那么为什么需要可见性呢?
我们来看这样一种情况,假如线程A和线程B同时操作主内存中的数据N,他们同时读取了N的初始数据,然后线程A操作之后将修改后的数据写入主内存,可是线程B不知道N已经被修改!!!这是很严重的错误,所以我们需要在主内存中的共享变量被修改后,通知其他正在操作这个数据的线程数据已修改,然后其他线程重新读取主内存中的最新数据N,继续操作,这个就是可见性,也是其存在的必要性。
可行性解决方案:
声明共享变量为volatile
使用CAS的原子条件更新来实现线程之间的同步
配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信
1.2.1 CAS
CAS,compare and swap,比较并交换。他要求线程在操作之后,要将数据写入主内存之前,先判断主内存中的数据是不是之前的数据,如果是则写入主内存,如果不是则说明已经被其他线程修改过了,然后就会读取最新数据然后继续操作,这就是CAS。
CAS不是绝对完美的,他有自己的缺陷,比如每次要将数据写入主内存时,主内存中的数据都被其他线程修改过了,他就会一直反复回旋。另外,CAS只能保证一个共享变量的原子操作。
最主要的是,CAS会出现ABA情况。
1.2.2 ABA
什么是ABA?简单理解就是,假如线程A要操作主内存中的某个数据,然后线程A操作时间假如10秒,但是在这10秒内,有另一个线程B操作了该数据,然后又将数据还原,等线程A将数据写入主内存时,发现数据还是之前的数据,就会将数据写入内存,可这个数据已经被线程B修改了两次!!这就是ABA。
ABA解决办法,上锁,或者原子引用。
1.3 什么是原子性?
原子性:一个操作不可分割的,不可分离的。举个简单例子,对于变量x,进行加1,然后取到值,这一个过程尽管简单,但是却不具备原子性,因为我们要先读取x,之后进行计算,然后重新写入,其实是几个步骤。如果仅仅对x进行赋值,那么则可以认为是原子的,JMM是保证其原子性的。
1.4 什么是有序性?
我有一个朋友,他打字打累了,然后他决定随便写写,他说,代码里写的代码,他不是按顺序执行的,他会重新排序,然后执行,原因和细节想了解的请转去源码。有序性就是要求他不重新排序。
2 Volatile
2.1 什么是Volatile?
简单来说,Volatile就是Java虚拟机提供的一个轻量级的同步机制。
为什么说是轻量级,大家先看看Volatitle的三个特点:
可见性
不保证其原子性
禁止指令重排
可见性我在JMM章节已经讲了,禁止指令重排相当于JMM的连续性,在这里也不多叙述了,我们重点分析下不保证其原子性这个特点。
2.2 不保证原子性原因及分析
大家都知道,JMM是保证其原子性的,即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在JMM篇章我们已经说过,为了保证可见性,我们需要用到Volatile去保证其可见性,那么我们考虑下这个情况:
public class TestMain {
public static void main(String[] args) {
TestData testData=new TestData();//有个初始值为0的变量和一个加一的方法
//创建20个线程,每个线程执行1000次给变量加一的方法
for(int i=0;i<20;i++){
new Thread(()->{
for(int j=0;j<1000;j++){
testData.addOne();
}
},String.valueOf(i)).start();
}
//保证20个线程跑完
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"最后的值为:"+testData.number);
}
}
class TestData{
volatile int number=0;
public void addOne(){
number++;
}
}
假如我们创建20个线程,每个线程执行1000次给常量加一的方法,如果保证其原子性,正常的数字应该是20000,这点大家应该没有疑惑,那么最后结果是什么呢?
无论你执行多少次,他最后的值永远都不会是20000,那么为什么呢?
我们根据JMM的保证可见性的前提下思考一种情况,假如两个线程同时操作主内存里的同一个变量,初始值为0,然后线程A拿到变量后,给变量加1,在他想要将值写入主内存的时候,线程B拿到了主内存中的变量,这个变量是初始值0,然后B线程拿到值之后,A线程将值写入了主内存,次数主内存中的变量值为1,然后B线程执行完操作后值也为1,然后将值写入主内存,同时写了两个1,主内存的值还是1,这就产生了丢失写值的情况。所以无论我们执行多少次,他最后的值都不可能是20000。
因此,Volatile不保证其原子性。
2.2 不保证原子性解决办法
解决办法有两种,一种是给方法上锁,另一种就是原子引用(atomicreference)。CAS时原子引用会出现ABA问题,简单理解就是假如A线程操作10秒,在这十秒内有另一个线程操作了数据,但又把数据还原,但是A线程发现不了,所以还是直接用时间戳原子引用(atomicstampedreference)吧。