从JMM引申到线程安全

1、世界存在线程安全问题
2、提出JMM规范
3、JVM实现参考JMM,解决线程安全问题

JMM定义
屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果,定义了8种内存操作指令。

8种内存操作指令
主内存与工作内存的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存中的实现细节。
java内存模型定义了8种操作来完成,这8种操作每一种都是原子操作:

lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

内存操作规则(执行上述8种基本操作时必须满足)
1.JMM的as-if-serial语义:
不管怎么重排序,单线程的执行结果不能被改变。(为了提高性能,编译器和处理器常常会对指令进行重排序)

2.JMM的happens-before规则:
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

JMM的意义
as-if-seria和happens-before,前者为了解决单线程重排序问题,后者为了解决多线程重排序问题;
JMM解决了重排序问题和不同硬件和OS解决CPU缓存一致性问题的差异;
JVM运行时内存区(堆栈设计)的实现参考JMM;

进程是程序向系统申请资源的基本单位,而线程是进程中可以独立执行的最小单位。

串行、并发与并行
串行:任务一个接一个执行
并发:以交替的方式执行多个任务
并行:以齐头并进的方式同时执行多个任务

竞态:计算结果的正确性与执行时间相关,有可能在不恰当的时间执行会出现错误的结果,存在竞态代表线程不安全。
竞态的两种代码模式:read-modify-write,check-and-act。(代表这两种代码模式容易出线程安全问题)

如何分析一个代码是否会出现线程安全问题?
1.判断是不是竞态
2.分析是不是竞态的两种代码模式
3.考虑共享变量和局部变量,利用二维表分析竞态

线程安全表现在3个方面:原子性,可见性,有序性。
原子性:针对共享变量的操作,从当前执行线程以外的其他线程看来都是不可分割的。
可见性:线程对共享变量操作之后,其他线程能够及时获取最新的值。
有序性:实际执行的结果和单线程的执行结果是一样的。

推荐阅读更多精彩内容