多线程学习------02线程安全

一、线程安全问题概述

  • 非线程安全:主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况.

  • 线程安全问题表现为三个方面: 原子性,可见性和有序性

二、原子性

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

  • 即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

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

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

  • 如现实生活中从 ATM 机取款, 对于用户来说,要么操作成功,用户拿到钱, 余额减少了,增加了一条交易记录; 要么没拿到钱,相当于取款操作没有发生

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

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

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

三、可见性

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

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

可能会出现以下情况: 在main线程中 t.flag=false执行后,可能存在子线程看不到main线程对flag做的修改,在子线程中flag变量一直为false导致子线程看不到main线程对flag变量更新的原因,可能:

1)JIT即时编译器可能会对run方法中while循环进行优化为:

if( !flag ){
  	while( true ){
  	    sout("flag为true,程序继续执行!");
  	    }
}

2)可能与计算机的存储系统有关.假设分别有两个cpu内核运行main线程与子线程,运行子线程的cpu无法立即读取运行main线程cpu中的数据

四、有序性

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

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

1、重排序

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

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

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

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

  • 重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能.但是,可能对多线程程序的正确性产生影响,即可能导致线程安全问题

  • 重排序与可见性问题类似,不是必然出现的.

  • 与内存操作顺序有关的几个概念:

源代码顺序, 就是源码中指定的内存访问顺序.

程序顺序, 处理器上运行的目标代码所指定的内存访问顺序

执行顺序,内存访问操作在处理器上的实际执行顺序

感知顺序,给处理器所感知到的该处理器及其他处理器的内存访问操作的顺序

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

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

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

2、指令重排序

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

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

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

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

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

3、存储子系统重排序

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

  • 高速缓存(Cache)是 CPU 中为了匹配与主内存处理速度不匹配而设计的一个高速缓存写缓冲器(Store buffer, Write buffer)用来提高写高速缓存操作的效率

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

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

  • 存储子系统重排序对象是内存操作的结果.

从处理器角度来看, 读内存就是从指定的 RAM 地址中加载数据到寄存器,称为 Load 操作; 写内存就是把数据存储到指定的地址表示的 RAM 存储单元中,称为 Store 操作.内存重排序有以下四种可能

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

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

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

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

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

4、貌似串行语义

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

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

5、保证内存访问的顺序性

可以使用基于JDK的 volatile 关键字和基于JVM的 synchronized 关键字实现有序性

五、Java内存模型

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致

要解决这个问题:就需要把变量声明为volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

所以,volatile 关键字 除了防止 JVM 的指令重排(实现有序性) ,还有一个重要的作用就是保证变量的可见性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值