小陈:上一篇说了JAVA内存模型,但是后面说了在多线程并发操作的时候有可见性问题,我现在迫不及待想知道线程安全的可见性、原子性、有序性是啥了
老王:哈哈,可以。我先说说我自己对可见性、有序性、原子性的理解:
可见性
上一篇讲了,多个线程同时对某一个共享变量进行操作的时候,存在线程A的操作对线程B不可见的问题。简单来说就是线程A执行了某些操作对数据进行了变更;但是线程B并不知道,所以还是使用旧数据干它自己的活。
小陈:这么讲,按照概念来理解我还是很模糊啊,能不能搞个例子来讲解一下?
老王,没问题,我就结合上次你提的那个JAVA内存模型可能导致数据不一致的这个例子给你讲解一下
比如线程A和线程B都执行x 操作(x的初始值是0),线程A执行完了之后将主内存的值更新为1,但是线程B由于已经将 x = 0 读取进入自己的工作内存了,不知道线程A将x更新为1了,所以还是使用x=0去进行 操作。
像这种,就是典型的可见性问题,就是线程A操作了数据,但是线程B不可见,感知不到。
小陈:嘿嘿,看来上次我的猜测没错啊,无论是CPU缓存架构下还是JAVA内存模型都是有可见性的问题。
老王:没错,你说的这个问题是存在的,但是还是有些手段可以避免的,后面我们再来讨论。下面我们再来说一下有序性的问题
有序性
有序性是指由于JIT动态编译器、操作系统为了给提高程序的执行效率,可能会对按顺序书写好的指令进行重排,线程或者CPU执行的时候不一定按照程序书写的顺序来执行:
比如程序的书写顺序是 指令1 -> 指令2 -> 指令3;但是由于指令重排序,某个线程执行这几个指令的时候,比如说线程A执行的时候,可能先执行指令3,然后再执行指令2、指令1。导致别的线程,比如说线程B看到线程A的指令执行是乱序的。
我搞个代码给你讲解一下:
线程A在执行数据库、http客户端的初始化工作,初始化完毕之后将initOk初始化表示置为true表示初始化完毕。
代码语言:javascript复制// 步骤1
dataSource = initDataSource();
// 步骤2
httpClient = initHttpClient();
// 步骤3
initOK = true;
线程B在这里一直监听线程A是否初始化资源完毕,看到initOK标识为true表示初始化结束。开始执行业务操作,获取数据,根据数据发起网络调用。
代码语言:javascript复制// 步骤4
while(!initOK) {
}
// 步骤5
Object data = dataSource.getData();
// 步骤6
httpClient.request(data);
上面这段代码,正常来说线程A的执行顺序应该是 步骤1 -> 步骤2 -> 步骤3。但是由于JIT动态编译器或者操作系统可能对指令进行重排序,所以可能执行顺序是 步骤3 -> 步骤1 -> 步骤2。
这样就会导致线程B先看到了initOk = true,这样就会导致线程B直接跳出while循环,跳出等待,执行dataSource.getData方法,执行httpClient.request()方法;但是线程A的步骤1、步骤2还没执行dataSource、httpClient是null,会抛出空指针异常。
小陈:等等,我来理解一下;线程A先执行了initOK = true;导致线程B跳出了while循环,然后调用dataSource.getData方法,由于线程A还没执行dataSource = initDataSource()方法,所以dataSource对象可能是null值,这样线程B调用的时候可能抛出空指针异常,是这样吧?
老王:没错,理解得非常好,小陈你果然聪明啊;你这个理解力,我对后面讲解的文章越来越有信心了。
小陈:嘿嘿......
老王:上面这种有序性问题,在多线程并发执行的时候,由于指令的重排序存在,很可能是会发生的。
这就是有序性带来的线程安全问题,也就是线程B看到线程A的执行时乱序的,也就是不是按照步骤1、2、3这样顺序的来执行。
简单点来讲就是线程A还没初始化好,就将标识initOk设置为true。导致线程B误以为线程A搞定了,然后去获取数据,发起http请求,然后...,然后线程B就挂了...(线程B:线程A这坑爹的,还没初始化好就告诉我搞定了,这不是坑我嘛...)
老王:说了可见性、有序性的问题,下面我们再来说说原子性问题。
原子性
老王:原子性是说某个操作是不可分割的、不可中断的。
小陈:这个不可分割、不可中断是啥意思?
老王:
比如之前说的JAVA内存模型定义的8中操作;read、load、use、assign、store、write、lock、unlock等八种指令都是原子的。
老王:比如说read指令,不可分割:说的是这条指令是读取数据最小的指令了,不能再拆分成更多的指令
小陈:不可分割是不是说它就是最小的执行单元了,不能被拆分的意思?
老王:没错,就是这个意思.....
小陈:哦哦,这个不可分割我懂了,那不可中断又是啥玩意?
老王:简单来讲就是不能执行到一半就不干了,比如这个read指令,你不能读取一个变量的数据,只读取到一半的时候就撂挑子不干了;要执行就一起全部执行,不能干了一半就不干了,同时也不能被其它外部的因素打断了。
小陈:那意思是说cpu执行read指令,执行到一半的时候,就把这个线程挂起来,这个是不被允许的咯。
老王:哈哈,就是这样的。要干就全部都干了,不能中途搞了一半你跟我说退出了....
小陈:老王你真牛逼,这么晦涩的东西都被你三言两句简单的话就给说清楚了。
老王:那是,毕竟我可是单身十多年...;不,是工作十多年的老兵了,“技巧”早就磨练的杠杠的......
小陈:......
小陈:道理我是听明白了,实际编码里面那些操作是原子的,那些不是原子的呢?
老王:给你讲讲下面的例子就知道了:
比如下面的操作:
(1)y = 1;
(2)x ;
(3)z = y;
(1)其中y = 1操作是原子的,因为只是执行了load操作,将1直接load给y,只有一条指令的执行。
(2) x 操作就不是原子性的,之前画图讲解过,i 操作经过,read、load、use、assign、store、write等六个操作;虽然每个指令都是原子的,但是合并起来并不是原子的。
比如说线程A执行read和load操作将工作内存的变量x的值载入自己工作内存的变量副本中。但是还没来得及执行后续的use、assign、store、write指令,这个时候线程A就被挂起了。
线程A被挂起期间,线程B就也执行了read、load指令将变量x放入线程B的工作内存里了。这就相当于线程A的这6条指令没有连续执行完,被中断了,中途CPU又去执行别的指令了,并不是不可分割、不可中断的。
(3)z = y 也不是原子的,它先要执行read指令读取y的值,然后执行load执行赋值给z。并不是单一的原子指令
小陈:哇塞,老王你太牛逼了,你这么说我全懂了。
小陈:既然多线程并发操作的时候会有这些问题,那操作系统或者说JAVA底层是怎么解决这些问题达到并发安全的效果的呢?
老王:操作系统设计者肯定是会想到这些问题的,这就是我们下面要慢慢讲解的话题了,操作系统或者JAVA底层是怎么解决这些并发安全的问题的。
老王:小陈,给你个任务,你去看看MESI一致性协议的内容,下面我们讲解一下MESI一致性协议,以及MESI一致性协议是如何解决可见性问题的。
目录
JAVA并发专题 《筑基篇》
1.什么是CPU多级缓存模型?
2.什么是JAVA内存模型?
3.线程安全之可见性、有序性、原子性是什么?
4.什么是MESI缓存一致性协议?怎么解决并发的可见性问题?
JAVA并发专题《练气篇》
5.volatile怎么保证可见性?
6.什么是内存屏障?具有什么作用?
7.volatile怎么通过内存屏障保证可见性和有序性?
8.volatile为啥不能保证原子性?
9.synchronized是个啥东西?应该怎么使用?
10.synchronized底层之monitor、对象头、Mark Word?
11.synchronized底层是怎么通过monitor进行加锁的?
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
13.synchronized怎么保证可见性、有序性、原子性?
JAVA并发专题《结丹篇》
- JDK底层Unsafe类是个啥东西?
15.unsafe类的CAS是怎么保证原子性的?
16.Atomic原子类体系讲解
17.AtomicInteger、AtomicBoolean的底层原理
18.AtomicReference、AtomicStampReference底层原理
19.Atomic中的LongAdder底层原理之分段锁机制
20.Atmoic系列Strimped64分段锁底层实现源码剖析
JAVA并发专题《金丹篇》
21.AQS是个啥?为啥说它是JAVA并发工具基础框架?
22.基于AQS的互斥锁底层源码深度剖析
23.基于AQS的共享锁底层源码深度剖析
24.ReentrantLock是怎么基于AQS实现独占锁的?
25.ReentrantLock的Condition机制底层源码剖析
26.CountDownLatch 门栓底层源码和实现机制深度剖析
27.CyclicBarrier 栅栏底层源码和实现机制深度剖析
28.Semaphore 信号量底层源码和实现机深度剖析
29.ReentrantReadWriteLock 读写锁怎么表示?
- ReentrantReadWriteLock 读写锁底层源码和机制深度剖析
JAVA并发专题《元神篇》并发数据结构篇
31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?
32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?
33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?
34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?
35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?
36.DelayQueue 底层源码剖析,延时队列怎么实现?
37.SynchronousQueue底层原理解析
JAVA并发专题《飞升篇》线程池底层深度剖析
- 什么是线程池?看看JDK提供了哪些默认的线程池?底层竟然都是基于ThreadPoolExecutor的?
39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?
40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?
- ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?
- ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?
- ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?
- ThreadPoolExecutor shutdown、shutdownNow内部核心流程
- 再回头看看为啥不推荐Executors提供几种线程池?
- ThreadPoolExecutor线程池篇总结