二,线程安全问题
非线程安全问题是指多个线程对同一个对象的实例变量进行操作时,会出现值被改变,值不同步的情况
线程安全问题表现为三个方面:原子性,可见性和有序性
2.1原子性
原子就是不可分割的意思。原子操作的不可分割有两层含义:
1.访问(读写)某个共享变量的操作从其他线程来看,该操作要么执行完毕,要么尚未发生,即其他线程显示到当前操作的正阿金讲过
2.访问同一组共享变量的原子操作是不能够交错的,我们现实生活中从ATM取款机取款,对于用户来说,要么操作成功,用户拿到了钱,余额减少了,增加了一条交易记录,要么没拿到钱相当于取款操作没有发生
java有两种方式实现原子性:一种是使用锁,另一种利用处理器的CAS指令
锁具有排他性,保证共享变量在某一时刻只能被一个线程访问,CAS指令直接在硬件(处理器和内存)层次上实现,看做是硬件锁
2.2可见性
在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全问题的另一种形式:可见性
如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读到更新后的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见
多线程程序因为可见性问题可能会导致其他线程读取到旧数据,也就是脏读
2.3有序性
有序性是指在什么情况下一个处理器上运行的一个线程锁执行的 内存访问操作在另一个处理器运行的其他线程来看是乱序的
乱序指的是内存访问的顺序发生了变化
在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的: 编译器可能会改变两个操作的顺序 处理器也可能会不按照代码的顺序执行
2.3.1重排序
这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称之为重排序
重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升提高程序的性能,但是可能对多线程的正确性产生影响,即可能导致线程安全问题
重排序与可见性问题类似,不是必然的出现,是可能存在的问题。
与操作顺序有关的几个概念: 源代码顺序,就是源代码中指定的访问顺序。 程序顺序,处理器上运行的目标代码所指定的内存访问顺序 执行顺序,内存访问操作在处理器上的实际执行顺序 感知顺序,给定处理器所感知到的该处理器和其他处理器的内存访问操作的顺序
可以把重排序分为指令重排序与存储子系统重排序两种 指令重排序主要是由JIT编译器,处理器引起的,指程序顺序与执行顺序不一样 存储子系统重排序是由高速缓存,写缓冲器引起的,感知顺序与执行顺序不一致
2.3.2指令重排序
在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序 指令重排序是一种动作,确实对指令执行的顺序做出了调整,重排序的对象是指令 javac编译器一般不会执行指令重排序,而JIT编译器可能执行指令重排序 处理器也可能进行指令重排序,使得执行顺序与程序顺序不一致
指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程出现非预期的结果
2.3.3存储子系统重排序
存储子系统是指写缓冲器与高速缓存
高速缓存: 是CPU是为了匹配与主内存处理器速度不匹配而设计的一个高速缓存
写缓冲器:用来提高写高速缓存操作的效率
即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个的操作的执行顺序看起来像是发生了变化,这种现象称为存储子系统重排序
存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的现象
存储子系统重排序对象是内存操作的结果
从处理器的角度来看,读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作;写内存就是把数据存储到指定到指定的地址表示的RAM存储单元中。称为Store操作。内存重排序有以下四种:
1.LoadLoad重排序,一个处理器先后执行两个读操作L1和L2其他处理器对这两个内存操作的感知顺序可能是L2->L1
2.StoreStore重排序 一个处理器先后执行两个写操作S1和S2其他处理器对这两个内存操作的感知顺序可能是S2->S1
3.LoadStore重排序 一个处理器先执行一个读操作L1然后执行一个写操作S1其他处理器对这两个内存操作的感知顺序可能是S1->L1
4.StoreLoad重排序 一个处理器先执行一个写操作S1然后执行一个读操作L1其他处理器对这两个内存操作的感知顺序可能是L1->S1 内存重排序与具体的处理器微架构有关,不同的架构的处理器所允许的内存重排序不同
内存重排序可能会导致线程安全问题