电子说
介绍
线程安全的三大特性,原子性、可见性、有序性,这三大特性与我们之前整理的内容息息相关。本篇重点介绍下volatile的底层原理,帮助我们更好的理解java并发包。
一、原子性
提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
1. 原子性-synchronizes
2. 原子性-lock
3. 原子性-cas
4. 原子性-对比
二、可见性
可见性指的是一个线程对主内存的修改,可以被其他线程及时的观察到。导致共享变量在线程间不可见的原因:
1. 可见性-synchronizes
JMM中关于synchronized的内存语意:
2. 可见性-volatile
通过加入内存屏障和禁止重排序优化来实现。
JMM中关于volatile的内存语意:
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器中,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值。
下面看一个volatile内存可见性的例子:
public class VolatileCanSeeTest {
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() - > {
log.info("init begin");
while(!initFlag) {
}
// if(!initFlag) {while(true){}} // JIT
log.info("===success===");
}).start();
Thread.sleep(1000);
new Thread(() - > doSomething()).start();
}
public static void doSomething() {
log.info("doSomething begin");
initFlag = true;
log.info("doSomething end");
}
}
查看对应的汇编代码,可以看到使用volatile汇编指令会加上lock前缀指令:
if(!initFlag) {
while(true){
}
}
这里结合java内存模型对volatile底层原理进行说明:
3. 可见性-对比
三、有序性
一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
1. 有序性-happens-before原则
java内存模型中允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile关键字来保证一定的有序性,可以通过synchronized、lock保证同一时刻线程顺序执行来保证有序性。另外,java内存模型具备先天的有序性,称为**happens-before **原则:
程序次序规则(保证单线程的有序性,不保证多线程的有序性)
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
锁定规则
一个unlock操作先行发生于对后面同一个锁的lock操作。
volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作。
传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
线程启动规则
Thread对象的start()方法先行发生于此线程的每一个动作。
线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终结规则
线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
对象终结规则
一个对象的初始化完成先行发生于他的finalize()方法的开始。
如果两个操作的执行顺序无法从happens-before原则推导出来,那么就无法保证他们的有序性,虚拟机可以随意的对他们重排序。
2. 有序性-synchronizes
首先,可以明确的一点是:synchronized是无法禁止指令重排和处理器优化的。那么他是如何保证的有序性呢?
synchronized保证的有序性是多个线程之间的有序性,即被加锁的内容要按照顺序被多个线程执行。但是其内部的同步代码还是会发生重排序,只不过由于编译器和处理器都遵循as-if-serial语义,所以我们可以认为这些重排序在单线程内部可忽略。
as-if-serial语义的意思指:不管怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。简单说就是,as-if-serial语义保证了单线程中,不管指令怎么重排,最终的执行结果是不能被改变的。
3. 有序性-volatile
java内存模型允许编译器和处理器对指令重排序提高运行性能,并且只会对不存在数据依赖性的指令重排序。例:
int a = 1;
int b = 2;
int c = a + b;
变量c的值依赖a和b的值,所以重排序后能保证c的操作在a,b之后,但是a,b谁先执行就不一定,这在单线程下不存在问题。下面看一个多线程下指令重排序的例子:
public class VolatileSerialTest {
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException{
Set< String > resultSet = new HashSet< >();
Map< String, Integer > resultMap = new HashMap< >();
for (int i = 0; i < 1000000; i++) {
x = 0;
y = 0;
resultMap.clear();
Thread one = new Thread(() - > {
int a = y;
x = 1;
resultMap.put("a", a);
});
Thread two = new Thread(() - > {
int b = x;
y = 1;
resultMap.put("b", b);
});
one.start();
two.start();
one.join();
two.join();
resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b"));
log.info("ab结果:{}", resultSet);
}
}
}
由于指令重排序导致可能出现的结果有:
volatile禁止指令重排序原理:
volatile通过加入内存屏障禁止指令重排序。 编译器会根据volatile/synchronized/final等的语义,在特定的位置插入内存屏障。 当遇到特定的内存屏障指令时,处理器将禁止其对应的重排序,保证屏障前面的操作可以被后面的操作可见。
4. 有序性-对比
结语
本文总结了线程安全的三大特性,同时文中几乎涉及到了所以之前总结过的知识,在阅读过程中可以参考之前的文章进行理解。至此,我们对并发包基础应该有了完整的认识。
全部0条评论
快来发表一下你的评论吧 !