3.线程安全之可见性、有序性、原子性是什么?

2023-10-16 10:26:58 浏览数 (2)

小陈:上一篇说了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直接loady,只有一条指令的执行。

(2) x 操作就不是原子性的,之前画图讲解过,i 操作经过,read、load、use、assign、store、write六个操作;虽然每个指令都是原子的,但是合并起来并不是原子的。

比如说线程A执行readload操作将工作内存的变量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并发专题《结丹篇》

  1. 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 读写锁怎么表示?

  1. ReentrantReadWriteLock 读写锁底层源码和机制深度剖析

JAVA并发专题《元神篇》并发数据结构篇

31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?

32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?

33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?

34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?

35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?

36.DelayQueue 底层源码剖析,延时队列怎么实现?

37.SynchronousQueue底层原理解析

JAVA并发专题《飞升篇》线程池底层深度剖析

  1. 什么是线程池?看看JDK提供了哪些默认的线程池?底层竟然都是基于ThreadPoolExecutor的?

39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?

40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?

  1. ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?
  2. ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?
  3. ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?
  4. ThreadPoolExecutor shutdown、shutdownNow内部核心流程
  5. 再回头看看为啥不推荐Executors提供几种线程池?
  6. ThreadPoolExecutor线程池篇总结

0 人点赞