【Java 并发】【二】多线程安全之可见性、有序性、原子性

发布时间 2023-03-28 08:27:56作者: 酷酷-

1  前言

上节我们了解了CPU缓存结构以及我们的Java内存模型结构以及JMM的基本指令,我们能感受到的就是线程并发后带来的数据问题、执行问题,也就涉及到我们平时常说的可见性、有序性、原子性,那么这节我们来大概看看这三者的理解。

2  可见性

多个线程同时对某一个共享变量进行操作的时候,存在线程A的操作对线程B不可见的问题。直接的说就是线程A执行了某些操作对数据进行了变更;但是线程B并不知道,所以还是使用旧数据干它自己的活。有点类似我们数据库的事务,读未提交读已提交啥的。

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

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

其实问题点就在于:

  • 并发的读两个的默认值都是 0
  • 两个线程互相感知不到

至于怎么解决,我们可能在并发读的时候加锁,或者是在A修改后判断B是基于哪个初始值算的等,后续我们详解怎么解决。

3  有序性

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

比如程序的书写顺序是 指令1 -> 指令2 -> 指令3;但是由于指令重排序,某个线程执行这几个指令的时候,比如说线程A执行的时候,可能执行指令3,然后再执行指令2、指令1。我们举个例子:

线程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还没执行,所以是获取兯数据的,还会抛出异常。

这就是有序性带来的线程安全问题,也就是线程B看到线程A的执行时乱序的,也就是不是按照步骤1、2、3这样顺序的来执行。

简单点来讲就是线程A还没初始化好,就将标识initOk设置为true。导致线程B误以为线程A搞定了,然后去获取数据,发起http请求,然后...,然后线程B就挂了...

4  原子性

原子性是说某个操作是不可分割的、不可中断的。不可分割的意思作为一个整体,不能被拆分了,就相当于一个最小的执行单元。不可中断的意思是不能执行一半撂挑子不干了,不执行了。

比如之前说的JAVA内存模型定义的8中操作;read、load、use、assign、store、write、lock、unlock等八种指令都是原子的。

简单来讲就是不能执行到一半就不干了,比如这个read指令,你不能读取一个变量的数据,只读取到一半的时候就撂挑子不干了;要执行就一起全部执行,不能干了一半就不干了,同时也不能被其它外部的因素打断了。类似我们的数据库事务,要么都成功要么就都失败。

比如下面的操作:

(1)y = 1;

(2)x++;

(3)z = y;

其中y = 1操作是原子的,因为只是执行了load操作,将1直接load给y,只有一条指令的执行。

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又去执行别的指令了,并不是不可分割、不可中断的。

z = y 也不是原子的,它先要执行read指令读取y的值,然后执行load执行赋值给z。并不是单一的原子指令。

5  小结

这节我们了解了多线程带来的原子性、可见性、有序性的认识和重要性,有理解不对的地方欢迎指正哈。