文章目录
- 1. 线程安全与线程不安全
- 2. JMM介绍
- 3.线程安全问题的原因
- 主内存和工作内存数据不一致
- 程序代码指令的重排序
- 3.as-if-serial
- 3.happens-before
- 4. as-if-serial和happens-before对比
1. 线程安全与线程不安全
线程不安全是出现在多线程的情况下,多个线程对同一共享资源进行修改时,就有可能出现结果不一致的情况。
那么什么是线程安全呢?当多个线程访问同一个对象时,如果不用考虑这些线程在实际中的运行顺序,也不需要进行额外的同步操作,每次都能得到正确的结果,我们就称之为线程安全。
2. JMM介绍
JMM是Java Memory Model(Java内存模型),它是共享内存的并发模型,抽象图如下所示:
共享变量会被存放在主内存中,主内存对于所有线程都是可见的。同时每个线程又会拥有自己的本地内存,并且会将位于主存中的共享变量拷贝到自己的工作内存中,之后的读写操作均使用位于工作内存中的变量副本。本地内存中的内容只有当前线程能够看到,其他线程无法获得。当操作完成后,会在某一个时刻将工作内存中的变量副本写回到主内存中去。
3.线程安全问题的原因
主内存和工作内存数据不一致
根据Java内存模型,线程A和线程B要完成通信的话,需要经历以下两步:
- 线程A从主内存中将共享变量读入线程A的工作内存中,进行操作,再将数据重新写回到主内存中
- 线程B从主内存中读取新的共享变量到线程B的工作内存进行操作。
从横向来看,线程A和线程B在进行隐式通信。
但是如果出现线程A更新了数据之后,没有及时将数据写回到主内存中。而线程B又在此时读取了主内存中的数据,这就出现了脏读现象。
具体可以通过同步机制(控制不同线程之间操作发生的相对顺序)或者volatile关键字(使得每次volatile关键字的变量强制刷新到主内存)来解决这个问题。
程序代码指令的重排序
我们日常在编写代码的时候,都是按照我们自己的逻辑去进行编写的。但是实际上,在具体执行的时候,为了尽可能提高并行度,编译器和处理器会对执行进行重排序,一般包括下面几种:
- 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
- 指令集并行重排序:现代处理器采用了指令集并行技术来将多条指令重叠执行。如果不存在依赖性,处理器可以改变对应机器指令的执行顺序;
- 内存系统重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储看上去是乱序的。
针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列时会通过插入内存屏障指令来禁止某些特殊的处理器重排序。
3.as-if-serial
那什么情况下不能进行重排序呢,我们来看下面的例子:
double A = 1.0
double B = 2.0
double C = A*B
上面的代码中,A和B之间没有数据依赖的关系,所以先执行A再执行B,先执行B再执行A都是可行的。但是C对于A和B是有依赖关系的。因此,A->B->C 或者 B->A->C 都是可行的。
数据依赖性的具体定义为:如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时两个操作就存在数据依赖性。具体包括三种情况:读后写、写后读、写后写;这三种操作都是存在数据依赖性的,如果重排序会对最终执行结果产生影响。此时,编译器和处理器在进行重排序的时候,会遵守数据依赖性,不会改变存在数据依赖性关系的两个操作的执行顺序。
as-if-serial的含义是:不管怎么进行重排序,单线程程序的执行结果不能被改变。因为编译器和处理器都必须遵守as-if-serial,所以为程序员创建了一个幻觉:单线程是按代码编写的顺序执行的。
3.happens-before
happens-before原则提供了跨线程之间的内存可见性,具体定义为:
- 如果一个操作发生在另一个操作之前,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味Java平台在进行具体实现的时候必须按照指定的顺序来执行。如果重排序后的执行效果,与happens-before关系执行的结果一致,那么这种重排序也是可以的。
happens-before的具体规则包括:
- 程序顺序原则:一个线程内保证语义的串行性。
- volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性。
- 锁规则:解锁必然发生在随后的加锁(lock)前。
- 传递性:A先于B,B先于C,那么A必然先于C。
- 线程的start()方法先于它的每一个动作。
- 线程的所有操作先于线程的终结(Thread.join())
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
- 对象的构造函数的执行、结束先于finalize()方法。
4. as-if-serial和happens-before对比
- as-if-serial保证单线程内程序执行的结果不会改变,happens-before保证正确同步的多线程执行结果不会改变。
- as-if-serial和happens-before都是为了不改变程序结果的前提下,尽可能提高效率。