标签:输出 main 写在前面 iad 静态变量 编译 简单的 并发 操作
volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确、完整地理解,以至于许多程序员都习惯不去使用它,遇到需要处理多线程数据竞争问题的时候一律使用synchronized来进行同步。了解volatile变量的语义对了解多线程操作的其他特性很有意义,在本文中我们将介绍volatile的语义到底是什么。由于volatile关键字与Java内存模型(Java Memory Model,JMM)有较多的关联,因此在介绍volatile关键字前我们会先介绍下Java内存模型。
Java内存模型请参考JVM与多线程内存模型
这8种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。由于这种定义相当严谨但又十分烦琐,实践起来很麻烦。所以,下文将介绍定义的一个等效判断原则——先行发生原则,用来确定一个访问在并发环境下是否安全。
Java语言中有一个“先行发生”(happens-before)的原则。这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则解决并发环境下两个操作之间是否可能存在冲突的所有问题。
现在就来看看“先行发生”原则指的是什么。先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。这句话不难理解,但它意味着什么呢?我们可以举个例子来说明一下,如代码中所示的这3句伪代码。
//以下操作在线程A中执行 k=1; //以下操作在线程B中执行 j=k; //以下操作在线程C中执行 k=2;
假设线程A中的操作“k=1”先行发生于线程B的操作“j=k”,那么可以确定在线程B的操作执行后,变量j的值一定等于1,得出这个结论的依据有两个:一是根据先行发生原则,“k=1”的结果可以被观察到;二是线程C还没“登场”,线程A操作结束之后没有其他线程会修改变量k的值。现在再来考虑线程C,我们依然保持线程A和线程B之间的先行发生关系,而线程C出现在线程A和线程B的操作之间,但是线程C与线程B没有先行发生关系,那j的值会是多少呢?答案是不确定!1和2都有可能,因为线程C对变量k的影响可能会被线程B观察到,也可能不会,这时候线程B就存在读取到过期数据的风险,不具备多线程安全性。
下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
Java语言无须任何同步手段保障就能成立的先行发生规则就只有上面这些了,下面演示一下如何使用这些规则去判定操作间是否具备顺序性,对于读写共享变量的操作来说,就是线程是否安全,读者还可以从下面这个例子中感受一下“时间上的先后顺序”与“先行发生”之间有什么不同。
private int value=0; pubilc void setValue(int value){ this.value=value; } public int getValue(){ return value; }
上面的代码是一组再普通不过的getter/setter方法,假设存在线程A和B,线程A先(时间上的先后)调用了“setValue(1)”,然后线程B调用了同一个对象的“getValue()”,那么线程B收到的返回值是什么?
答案是不确定。
为什么呢?
接下来我们依次分析一下先行发生原则中的各项规则,由于两个方法分别由线程A和线程B调用,不在一个线程中,所以程序次序规则在这里不适用;由于没有同步块,自然就不会发生lock和unlock操作,所以管程锁定规则不适用;由于value变量没有被volatile关键字修饰,所以volatile变量规则不适用;后面的线程启动、终止、中断规则和对象终结规则也和这里完全没有关系。因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起,因此我们可以判定尽管线程A在操作时间上先于线程B,但是无法确定线程B中“getValue()”方法的返回结果,换句话说,这里面的操作不是线程安全的。
那怎么修复这个问题呢?
我们至少有两种比较简单的方案可以选择:
通过上面的例子,我们可以得出结论:一个操作“时间上的先发生”不代表这个操作会是“先行发生”,那如果一个操作“先行发生”是否就能推导出这个操作必定是“时间上的先发生”呢?很遗憾,这个推论也是不成立的,一个典型的例子就是多次提到的“指令重排序”,演示例子如下代码所示
//以下操作在同一个线程中执行 int i=1; int j=2;
代码清单的两条赋值语句在同一个线程之中,根据程序次序规则,“int i=1”的操作先行发生于“int j=2”,但是“int j=2”的代码完全可能先被处理器执行,这并不影响先行发生原则的正确性,因为我们在这条线程之中没有办法感知到这点。
上面两个例子综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准
Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量定义为volatile之后,它将具备两种特性。
volatile并不能保证原子性,导致volatile变量的运算在并发下一样是不安全的
如:多线程下的自增运算
public class VolatileTest { public static volatile int race = 0; private static final int THREADS_COUNT = 20; public static void increase() { race++; } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(race); } }
如果IDEA下这段代码执行出现死循环,请使用DEBUG运行即可,具体原因可以看:面试必问的CAS,你懂了吗?
例子解析:
这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能够正确并发的话,最后输出的结果应该是200000。运行完这段代码之后,并不会获得期望的结果,而且会发现每次运行程序,输出的结果都不一样,都是一个小于200000的数字,这是为什么呢?
问题就出现在自增运算“race++”之中,我们用Javap反编译这段代码后会发现只有一行代码的increase()方法在Class文件中是由4条字节码指令构成的,从字节码层面上很容易就分析出并发失败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中。
getstatic // 获取静态变量race,并将值压入栈顶 iconst_1 // 将int值1推送至栈顶 iadd // 将栈顶两个int型数值相加并将结果压入栈顶 putstatic // 为静态变量race赋值
从这个例子我们可以确定volatile是不能保证原子性的,要保证运算的原子性可以使用java.util.concurrent.atomic包下的一些原子操作类。例如最常见的: AtomicInteger。
在上面volatile的特性中提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
变量不需要与其他的状态变量共同参与不变约束。
使用volatile来修饰状态标记量,使得状态标记量对所有线程是实时可见的,从而保证所有线程都能实时获取到最新的状态标记量,进一步决定是否进行操作。例如常见的促销活动“秒杀”,可以用volatile来修饰“是否售罄”字段,从而保证在并发下,能正确的处理商品是否售罄。
volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; }
普通的双重检测机制在极端情况,由于指令重排序会出现问题,通过使用volatile来修饰instance,禁止指令重排序,从而可以正确的实现单例。
public class Singleton { // 私有化构造函数 private Singleton() { } // volatile修饰单例对象 private static volatile Singleton instance = null; // 对外提供的工厂方法 public static Singleton getInstance() { if (instance == null) { // 第一次检测 synchronized (Singleton.class) { // 同步锁 if (instance == null) { // 第二次检测 instance = new Singleton(); // 初始化 } } } return instance; } }
多线程下的volatile关键字使用详解及Java先行发生原则
标签:输出 main 写在前面 iad 静态变量 编译 简单的 并发 操作
原文地址:https://www.cnblogs.com/ridong12345/p/12447999.html