在程序的执行过程当中,涉及到两个方面:指令的执行和数据的读写。其中指令的执行经过处理器来完成,而数据的读写则要依赖于系统内存,可是处理器的执行速度要远大于内存数据的读写,所以在处理器中加入了高速缓存。在程序的执行过程当中,会 先将数据拷贝处处理器的高速缓存中,待运算结束后再回写到系统内存当中。html
在单线程的状况下不会有什么问题,可是若是在多线程状况下就可能会出现异常的状况,如下面这段代码为例,i
是放在堆内存的共享变量:java
i = i + 1; //i 的初始值为0。
复制代码
假如线程A
和线程B
都执行这段代码,那么就可能出现下面两种状况:编程
A
先执行+1
操做,而后将i
的值写回到系统内存中;线程B
从系统内存中拷贝i
的值1
到高速缓存中,执行完+1
操做再回写到系统内存中,最终的结果是i=2
。A
和线程B
首先都将i
的值0
拷贝到各自处理器的高速缓存当中,线程A
首先执行+1
操做,以后i
的值为1
,而后写回到系统内存中;可是对于线程B
而言,它并不知道这一过程,在运行该线程的处理器的高速缓存中i
的值仍然为0
,所以在它执行+1
操做后,再将i
的值写回到系统内存中,最终的结果是i=1
。这种不肯定性就称为 缓存不一致。缓存
在并发编程中,有三个关键的概念:可见性、原子性和有序性,只有保证了这三点才能使得程序在多线程状况下得到预期的运行结果。安全
可见性:是指线程之间的可见性,一个线程修改的状态对另外一个线程是可见的。也就是一个线程修改的结果,另外一个线程立刻就能看到。在1.1
所举的例子就存在可见性的问题。多线程
在Java
中volatile
、synchronized
和final
实现可见性。并发
原子性:即一个操做或者多个操做,要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行。框架
再好比a++
,这个操做实际是a=a+1
,是可分割的,因此它不是一个原子操做。非原子操做都会存在线程安全问题,须要咱们使用同步技术来让它变成一个原子操做。一个操做是原子操做,那么咱们称它具备原子性。编程语言
在Java
中synchronized
和在lock
、unlock
中操做或者原子操做类来保证原子性。函数
有序性:即程序执行的顺序按照代码的前后顺序执行。如下面的代码为例:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
复制代码
在上面的代码中定义了一个整形和Boolean
型变量,并经过语句1
和语句2
对这两个变量赋值,可是JVM
在执行这段代码的时候并不保证语句1
在语句2
以前执行,也就是说可能会发生 指令重排序。
指令重排序指的是在 保证程序最终执行结果和代码顺序执行的结果一致的前提 下,改变语句执行的顺序来优化输入代码,提升程序运行效率。
可是这一前提条件在多线程的状况下就有可能出现问题,如下面的代码为例:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while (!inited) {
sleep()
}
doSomethingWithConfig(context);
复制代码
对于线程1
来讲,语句1
和语句2
没有依赖关系,所以有可能会发生指令重排序的状况。可是对于线程2
来讲,语句2
在语句1
以前执行,那么就会致使进入doSomethingWithConfig
函数的时候context
没有初始化。
Java
语言提供了volatile
和synchronized
两个关键字来保证线程之间操做的有序性,volatile
是由于其 自己包含禁止指令重排序 的语义,synchronized
是由 一个变量在同一个时刻只容许一条线程对其进行 lock 操做 这条规则得到的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
volatile
的定义以下:Java
编程语言容许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保 经过排它锁单独地得到这个变量。若是一个字段被声明成volatile
,Java
线程内存模型确保 全部线程看到这个变量的值是一致的。
一旦一个共享变量被volatile
修饰以后,那么就具有了两层语义:
下面,咱们用两个小结解释一下这两层语义。
当咱们在X86
处理器下经过工具获取JIT
编译器生成的汇编指令,来查看对volatile
进行写操做时,会发生下面的事情:
//Java 代码
instance = new Singleton(); //instance 是 volatile 变量
//转变成汇编代码
0x01a3de1d: move $0 x 0, 0 x 1104800 (%esi);
0x01a3de24: lock add1 $ 0 x 0, (%esp);
复制代码
有volatile
变量修饰的共享变量 进行写操做的时候 会多出两行汇编代码,Lock
前缀的指令在多核处理器下引起了两件事情:
volatile
关键字禁止指令重排序有两层意思:
volatile
变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行;volatile
变量访问的语句放在其后面执行,也不能把volatile
变量后面的语句放到其前面执行。如下面的例子为例:
//flag 为 volatile 变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
复制代码
因为flag
为volatile
变量,所以,能够保证语句1/2
在语句3
以前执行,语句4/5
在其以后执行,可是并不保证语句1/2
之间或者语句4/5
之间的顺序。
对于1.2.3
举的有关Context
问题,咱们就能够经过将inited
变量声明为volatile
,这样就会保证loadContext()
和inited
赋值语句之间的顺序不被改变,避免出现inited=true
可是Context
没有初始化的状况出现。
volatile
相对于synchronized
的优点主要缘由是两点:简易和性能。若是从读写两方便来考虑:
volatile
读操做开销很是低,几乎和非volatile
读操做同样volatile
写操做的开销要比非volatile
写操做多不少,由于要保证可见性须要实现 内存界定,即使如此,volatile
的总开销仍然要比锁获取低。volatile
操做不会像锁同样 形成阻塞。以上两个条件代表,能够被写入volatile
变量的这些有效值 独立于任何程序的状态,包括变量的当前状态。大多数的编程情形都会与这两个条件的其中之一冲突,使得volatile
不能像synchronized
那样广泛适用于实现线程安全。
所以,在可以安全使用volatile
的状况下,volatile
能够提供一些优于锁的可伸缩特性。若是读操做的次数要远远超过写操做,与锁相比,volatile
变量一般可以减小同步的性能开销。
要使volatile
变量提供理想的线程安全,必须同时知足如下两个条件:
x++
这样的增量操做,它其实是一个由读取、修改、写入操做序列组成的组合操做,必须以原子方式执行,而volatile
不能提供必须的原子特性。避免滥用volatile
最重要的准则就是:只有在 状态真正独立于程序内其它内容时 才能使用volatile
,下面,咱们总结一些volatile
的应用场景。
用volatile
来修饰一个Boolean
状态标志,用于指示发生了某一次的重要事件,例如完成初始化或者请求停机。
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
复制代码
在解释 一次性安全发布 的含义以前,让咱们先来看一下 单例写法 当中著名的 双重检查锁定问题。
//使用 volatile 修饰。
private volatile static Singleton sInstance;
public static Singleton getInstance() {
if (sInstance == null) { //(0)
synchronized (Singleton.class) { //(1)
if (sInstance == null) { //(2)
sInstance = new Singleton(); //(3)
}
}
}
return sInstance;
}
复制代码
假如 没有使用volatile
来修饰sInstance
变量,那么有可能会发生下面的场景:
Thread1
进入getInstance()
方法,因为sInstance
为空,Thread1
进入synchronized
代码块。Thread1
前进到(3)
处,在构造函数执行以前使sInstance
对象成为非空,并设置sInstance
指向的内存空间。Thread2
执行,它在入口(0)
处检查实例是否为空,因为sInstance
对象不为空,Thread2
将sInstance
引用返回,此时sInstance
对象并无初始化完成。Thread1
经过运行Singleton
对象的构造函数并将引用返回给它,来完成对该对象的初始化。经过volatile
就能够禁止第二步和第四步的重排序,也就是使得 初始化对象在设置 sInstance 指向的内存空间以前完成。
volatile bean
模式适用于将JavaBeans
做为“荣誉结构”使用的框架。在volatile bean
模式中,JavaBean
被用做一组具备getter
和/或setter
方法的独立属性的容器。
volatile bean
模式的基本原理是:不少框架为易变数据的持有者提供了容器,可是放入这些容器中的对象必须是线程安全的。
在volatile bean
模式中,JavaBean
的全部数据成员都是volatile
类型的,而且 getter
和setter
方法必须很是普通,除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
复制代码
若是读操做远远超过写操做,您能够结合使用内部锁和volatile
变量来减小公共代码路径的开销。下面的代码中使用synchronized
确保增量操做是原子的,并使用volatile
保证当前结果的可见性。若是更新不频繁的话,该方法可实现更好的性能,由于读路径的开销仅仅涉及volatile
读操做,这一般要优于一个无竞争的锁获取的开销。
public class CheesyCounter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
复制代码
(1) Java 并发编程:volatile 关键字解析
(2) Java 中 volatile 关键字详解
(3) 正确使用 volatile 变量
(4) volatile 的使用