线程安全问题概括来说表现为三个方面:原子性,可见性和有序性。

在多核处理器的环境下:编译器可能改变两个操作的先后顺序;处理器可能不是完全依照程序的目标代码所指定的顺序执行命令;一个处理器执行的多个操作,在其他处理器的角度来看,其顺序可能与目标代码所指定的顺序不一致。这种现象就叫重排序。

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

内存重排序类型:

重排序类型 含义
LoadLoad重排序 该重排序指一个处理器上先后执行两个读内存操作L1和L2,其他处理器对这两个内存操作的感知顺序可能是L2——>L1,即L1被重排序到L2之后。
StoreStore重排序 该重排序指一个处理器上先后执行两个写内存操作W1和W2,其他处理器对这两个内存操作的感知顺序可能是W2——>W1,即W1被重排序到W2之后。
LoadStore重排序 该重排序指一个处理器上先后执行读内存操作L1和写内存操作W2,其他处理器对这两个内存操作的感知顺序可能是W2——>L1,即L1被重排序到W2之后。
StoreLoad重排序 该重排序指一个处理器上先后执行写内存操作W1和读内存操作L2,其他处理器对这两个内存操作的感知顺序可能是L2——>W1,即W1被重排序到L2之后。

内存重排序与具体的处理器微架构有关,基于不同微架构的处理器所允许的内存重排序是不同的,这里不再阐述。


重排序可能会导致多线程程序出现内存可见性问题

  • 对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序

  • 对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。

常见的处理器都不允许对存在数据依赖的操作做重排序

数据依赖性: 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为下列3种类型:
1.写后读:a=1;b=a;
2.写后写:a=1;a=2;
3.读后写:a=b;b=1;

为了遵守as-if-serial语义,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。因为这种重排序会改变执行结果。
不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。


重排序对多线程的影响

image
当操作1和操作2重排序时
image
当操作3和操作4重排序时
image
重排序在这里破坏了多线程程序的语义!

通过加锁同步可解决该问题
image


为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序

  • 无论是编译器还是处理器,都需要遵循以下重排序规则:
  1. 临界区内的操作不允许被重排序到临界区之外
  2. 临界区内的操作允许被重排序
  3. 临界区外的操作之间可以被重排序
  4. 锁申请与锁释放操作不能被重排序
  5. 两个锁申请操作不能被重排序
  6. 两个锁释放操作不能被重排序
  7. 临界区外的操作可以被重排序到临界区之内

参考资料:
1.Java并发编程的艺术(方腾飞 魏鹏 程晓明 著)
2.Java多线程编程实战指南(黄文海 著)

版权声明:本文为leayun原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/leayun/p/15075979.html