Java多线程-04 (线程安全问题)

                                                         线程安全的问题

                                                                                             个人博客:www.xiaobeigua.icu 

        非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况,这种情况在计算机领域是很常见的,所以我们要尽力去避免出现非线程安全的情况。

线程安全问题表现为三个方面:

        原子性可见性有序性


1.1 线程原子性

原子(Atomic)就是不可分割的意思. 原子操作的不可分割有两层含义: 

         访问(读,写)某个共享变量的操作从其他线程来看,该操作要 么已经执行完毕,要么尚未发生, 即其他线程年示到当前操作的中间结果。

         访问同一组共享变量的原子操作是不能够交错的。

        如现实生活中从 ATM 机取款, 对于用户来说,要不就是操作成功,取到了钱,生成了交易记录; 要不就是没拿到钱,相当于取款操作没有发生。

                        

Java 有两种方式实现原子性:  一种是使用锁; 另一种利用处理器 的 CAS(Compare and Swap)指令。

         锁具有排它性,保证共享变量在某一时刻只能被一个线程访问。

         CAS 指令直接在硬件(处理器和内存)层次上实现,看作是硬件。


 1.2 线程可见性

        在多线程环境中, 一个线程对某个共享变量进行更新之后 , 后续 其他的线程可能无法立即读到这个更新的结果, 这就是线程安全问 题的另外一种形式: 可见性(visibility)。

        如果一个线程对共享变量更新后, 后续访问该变量的其他线程可以读到更新的结果, 称这个线程对共享变量的更新对其他线程可见, 否则称这个线程对共享变量的更新对其他线程不可见.。

        多线程程序因为可见性问题可能会导致其他线程读取到了旧数据 (脏数据).

             

                              

         不同的线程将内存中的共享数据拷贝到本地内存中处理,这个处理过程对别的线程是不可见的


1.3 线程有序性

        有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of Order)。

        简单地说就是,你是按照这样来做的,但是被人看到的是按照那样来做的。

        乱序是指内存访问操作的顺序看起来发生了变化

1.3.1 重排序

在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:

        编译器可能会改变两个操作的先后顺序;

        处理器也可能不会按照目标代码的顺序执行;

        这种一个处理器上执行的多个操作,在其他处理器来看它的顺序 与目标代码指定的顺序可能不一样,这种现象称为重排序。

        重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能。

        但是可能对多线程程序的正确 性产生影响,即可能导致线程安全问题 重排序与可见性问题类似,不是必然出现的。

可以把重排序分为指令重排序与存储子系统重排序两种。

        指令重排序主要是由 JIT 编译器,处理器引起的, 指程序顺序与执行顺序不一样。

        存储子系统重排序是由高速缓存,写缓冲器引起的, 感知顺序与执 行顺序 不一致。

1.3.2 指令重的排序

        在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)

        指令重排是一种动作,确实对指令的顺序做了调整, 重排序的对象指令。

        javac 编译器一般不会执行指令重排序, 而 JIT 编译器可能执行指令重排序。

        处理器也可能执行指令重排序, 使得执行顺序与程序顺序不一致.。

JIT编译器:

        JIT 是 just in time 的缩写, 也就是即时编译编译器。 使用即时编译器技术,能够加速 Java 程序的执行速度 。

         java在允许一个源程序文件时: javac 将程序源代码编译,转换成 java 字节码,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译 。

指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程程序出现非预期的结果。

1.3.3 存储子系统的重排序

        存储子系统是指写缓冲器与高速缓存。 

        高速缓存(Cache)是 CPU 中为了匹配与主内存处理速度不匹配而设计的一个高速缓存

         写缓冲器(Store buffer, Write buffer)用来提高写高速缓存操作的效率。

                          

        即使处理器严格按照程序顺序执行两个内存访问操作,在存储子 系统的作用下, 其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的顺序顺序看起来像是发生了变化, 这种现象称为存储子系统重排序。

         存储子系统重排序并没有真正的对指令执行顺序进行调整,而是 造成一种指令执行顺序被调整的现象。存储子系统重排序对象是内存操作的结果。 

从处理器角度来看:

        读内存就是从指定的 RAM 地址中加载数据到 寄存器,称为 Load 操作。

        写内存就是把数据存储到指定的地址表示 的 RAM 存储单元中,称为 Store 操作。

内存重排序有以下四种可能:

        LoadLoad 重排序,一个处理器先后执行两个读操作 L1 和 L2,其他处 理器对两个内存操作的感知顺序可能是 L2->L1

         StoreStore重排序,一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是 W2->W1

         LoadStore 重排序,一个处理器先执行读内存操作 L1 再执行写内存 操作 W1, 其他处理器对两个内存操作的感知顺序可能是 W1->L1

         StoreLoad重排序,一个处理器先执行写内存操作W1再执行读内存 操作 L1, 其他处理器对两个内存操作的感知顺序可能是 L1->W1

        内存重排序与具体的处理器微架构有关,不同架构的处理器所允 许的内存重排序不同。

       

  内存重排序可能会导致线程安全问题.假设有两个共享变量 :

 int num = 0;

 boolean go = false;​​​​​​​
处理器1处理器2

int num = 1;   第一步

boolean go = true;  第二步​​​​​​​

while( !go){} 第三步

 sout( num ); 第四步  

步骤 :处理器1 先给 num 与 go赋值  然后处理器2再执行 while判断,是可以走到第四步正常              输出的。

        但是如果由于内存重排序原因 第二步的 num=true赋值,没有被处理器2读取到,所以当处          理器2运行到第三步时 ,就是因为 go=false而出现死循环,也就出现的线程安全问题。

1.3.4 貌似串行语义(好像串行

        JIT 编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序, 给单线程程序造成一种假象----指令是按照源码的顺序执行的.这种假象称为貌似串行语义。 它并不能保证多线程环境程序的正确性。

        为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序.如果两个操作(指令)访 问同一个变量,且其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系(Data dependency)。  如:


i = 1; j = i + 1; //后一条语句的操作数包含前一条语句的执行结果;
i = j; i = 1; //先读取 x 变量,再更新 x 变量的值;
i = 1; i = 2; //两条语句同时对一个变量进行写操作

如果不存在数据依赖关系则可能重排序。    如:

int price = 50;
int quantity = 10;
double sum = price * quantity;

        存在控制依赖关系的语句允许重排.一条语句(指令)的执行结果会 决定另一条语句(指令)能否被执行,这两条语句(指令)存在控制依赖关 系(Control Dependency). 如在 if 语句中允许重排,可能存在处理器先 执行 if 代码块,再判断 if 条件是否成立

1.3.5 保证内存访问的顺序性

        可以使用 volatile 关键字, synchronized 关键字实现有序性。这个后面会重点说到!


1.4 Java 内存模型 

1.4.1 初始模型

这里注意: Cache缓存是位于CPU中的,分离出来是为了更好理解!

 ​​​​​​​

1.4.2简化模型

        可以把JVM内存模型简化为简单的抽线模型:

                                

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值