在分析之前,我们需要先了解以下并发三特征,从这三个特征出发,我们来看看volatile和synchronized的差别。
并发三特征
原子性
指一些操作只能同时执行成功或执行失败,整个操作不可分割。
举个简单的例子,往ATM中存钱,存入500,账户余额加500,存入和余额增加两个操作必须同时成功或者失败,不能存入成功,余额不增加。
synchronized
通过lock unlock可确保被锁对象内的整个操作的原子性。
volatile
只能修饰基础类型变量,所以它只能确保变量的原子性操作,比如直接赋值和读取。
int a = 5; //原子操作
int a++;
//非原子操作,等价于
int temp = a; //原子操作
temp = temp +1; //多线程环境中可能在这一步其他线程修改了a的值,导致线程不安全
a = temp; //原子操作
可见性
指多线程访问一个资源(变量/对象)时,一个线程修改变资源,其他线程在进行原子性操作之前能获取到资源的最新状态。
举个简单的例子,从ATM中取钱,账户余额就500块,你取500块,当你取出500块,此时余额还未减500的是时候,你的对象(没有就new一个)在另外一个ATM上看到余额是500,也取500!于是你又有了对象,还多了500块。可谓是:并发程序不对头,对象金钱双丰收!美哉!美哉!
如何解决这个让人不想解决的问题呢?
我们要确保同一个资源,不同线程进行原子性操作执行之前获取该资源的最新状态,原子性操作之后马上更新该资源的状态。
放在上面的例子中就是:你对象 (不同线程) 在取钱 (原子性操作) 的时候,应该看到的是你取钱之后的余额 (资源的最新状态) 。
从这里其实我们已经可以看到,可见性应该是以原子性为基础的。
一个原子性操作不可分割,不同线程进行原子性操作时要彼此可见。
volatile
1,当read一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
2,当write一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量立即刷新到主内存。
synchronized
1,unlock之前将原子性操作的所有修改立即同步回主存
2,在lock之前线程工作内存中的缓存无效,重新从主内存读取
有序性
指代码执行的顺序。
正常来说,我们写的代码应该时从上到下顺序执行,但是在编译时,有时候会发生指令重排,造成代码顺序和实际我们看到的顺序不一致。编译器指令重排时满足happen before规则:
happen before规则
1, 顺序原则
一个线程内保证语义的串行性; a = 1; b = a + 1;相关的代码按顺序执行
2, volatile规则
volatile变量的写,先发生于读,这保证了volatile变量的可见性,
3, 锁规则
解锁(unlock)必然发生在随后的加锁(lock)前.
4, 传递性
A先于B,B先于C,那么A必然先于C.
5, 线程启动, 中断, 终止
线程的start()方法先于它的每一个动作.
线程的中断(interrupt())先于被中断线程的代码.
线程的所有操作先于线程的终结(Thread.join()).
6, 对象终结
对象的构造函数执行结束先于finalize()方法.
volatile修饰和synchronized修饰都是确保其原子性操作所在相对位置不发生改变。
volatile修饰原子性范围只有对变量的直接读取和赋值 相当于无法指令重排
synchronized修饰原子性范围包含整个对象/块 ,内部存在指令重排
public class TestSinglePattern {
//double check locking
private volatile static TestSinglePattern instance;
public static TestSinglePattern getInstance() {
if (instance == null) {//性能优化,排除临界点的锁机制
synchronized (TestSinglePattern.class) {
if (instance == null) {
/* 新对象时分为三步
* 1,开辟内存空间
* 2,初始化对象信息
* 3,返回对象内存地址
*
* 第3步与第二部没有逻辑的先后关系,可能存在指令重排,返回一个没有初始化的对象地址
* 为了避免此处的错误需要volatile修饰instance,防止指令重排
*/
instance = new TestSinglePattern();
}
}
}
return instance;
}
}