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

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

小陈:上一篇说了JAVA内存模型,但是后面说了在多线程并发操作的时候有可见性问题,我现在迫不及待想知道线程安全的可见性、原子性、有序性是啥了

老王:哈哈,可以。我先说说我自己对可见性、有序性、原子性的理解:

可见性

上一篇讲了,多个线程同时对某一个共享变量进行操作的时候,存在线程A的操作对线程B不可见的问题。简单来说就是线程A执行了某些操作对数据进行了变更;但是线程B并不知道,所以还是使用旧数据干它自己的活。

小陈:这么讲,按照概念来理解我还是很模糊啊,能不能搞个例子来讲解一下?

老王,没问题,我就结合上次你提的那个JAVA内存模型可能导致数据不一致的这个例子给你讲解一下

比如线程A和线程B都执行x++操作(x的初始值是0),线程A执行完了之后将主内存的值更新为1,但是线程B由于已经将 x = 0 读取进入自己的工作内存了,不知道线程A将x更新为1了,所以还是使用x=0去进行++操作。

像这种,就是典型的可见性问题,就是线程A操作了数据,但是线程B不可见,感知不到。

image.png

小陈:嘿嘿,看来上次我的猜测没错啊,无论是CPU缓存架构下还是JAVA内存模型都是有可见性的问题。

老王:没错,你说的这个问题是存在的,但是还是有些手段可以避免的,后面我们再来讨论。下面我们再来说一下有序性的问题

有序性

有序性是指由于JIT动态编译器、操作系统为了给提高程序的执行效率,可能会对按顺序书写好的指令进行重排线程或者CPU执行的时候不一定按照程序书写的顺序来执行

比如程序的书写顺序是 指令1 -> 指令2 -> 指令3;但是由于指令重排序,某个线程执行这几个指令的时候,比如说线程A执行的时候,可能先执行指令3,然后再执行指令2、指令1。导致别的线程,比如说线程B看到线程A的指令执行是乱序的。

我搞个代码给你讲解一下:

线程A在执行数据库、http客户端初始化工作,初始化完毕之后将initOk初始化表示置为true表示初始化完毕。


// 步骤1
dataSource = initDataSource();
// 步骤2
httpClient = initHttpClient();
// 步骤3
initOK = true;

线程B在这里一直监听线程A是否初始化资源完毕,看到initOK标识为true表示初始化结束。开始执行业务操作,获取数据,根据数据发起网络调用


// 步骤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一致性协议是如何解决可见性问题的。

相关文章
|
5月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
88 1
|
5月前
|
缓存 Java 编译器
多线程内存模型问题之保证Java中的原子性,如何解决
多线程内存模型问题之保证Java中的原子性,如何解决
|
5月前
线程可见性和关键字volatile
线程可见性和关键字volatile
|
7月前
|
缓存 安全 Java
多线程的三大特性:原子性、可见性和有序性
多线程的三大特性:原子性、可见性和有序性
161 1
|
7月前
|
缓存 算法 Java
多线程04 死锁,线程可见性
多线程04 死锁,线程可见性
47 0
|
7月前
|
Java
Java线程面试题:什么是原子性问题?如何解决?
Java线程面试题:什么是原子性问题?如何解决?
79 0
|
7月前
|
Java
【Java】有 A、B、C 三个线程,如何保证三个线程同时执行?在并发情况下,如何保证三个线程依次执行?如何保证三个线程有序交错执行?
【Java】有 A、B、C 三个线程,如何保证三个线程同时执行?在并发情况下,如何保证三个线程依次执行?如何保证三个线程有序交错执行?
83 0
|
7月前
|
Java
【Java】有 A、B、C 三个线程,如何保证三个线程同时执行?在并发情况下,如何保证三个线程依次执行?如何保证三个线程有序交错执行?
> 在多线程的面试中,经常会遇到三个类似的线程执行问题: > Q1:有 A、B、C 三个线程,如何保证三个线程同时执行? > Q2:有 A、B、C 三个线程,在并发情况下,如何保证三个线程依次执行? > Q3:有 A、B、C 三个线程,如何保证三个线程有序交错执行? ## Q1:有 A、B、C 三个线程,如何保证三个线程同时执行? 保证线程同时执行可以用于并发测试。可以使用倒计时锁CountDownLatch实现让三个线程同时执行。代码如下所示: ```java ExecutorService executorService = Exec
188 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
27 3