1.volatile
volatile关键字是一个特征修饰符,确保本条指令不会因编译器的优化而省略。可以li理解为阻止编译器对代码进行优化。
先了解一下原子性(atomicity)和 可见性(visibility)以及有序性
1.1原子性
即一个操作或者一段代码,要么全部执行并且执行过程中不被任何因素打算,要么不执行。
1.2原子操作
1.2.1处理器实现原子操作-(总线锁、缓存锁)
1.处理器会自动保证内存操作的原子性:即从内存中读取或写入一个字节的操作是原子的。处理器处理一个字节的时候,其它处理器不能访问这个字节的内存地址。
2.总线锁保证原子性:即处理器会提供一个LOCK信号,当一个处理器在总线上输出此信号时,会阻塞其它处理器的请求。该处理器独占共享内存。
3.缓存锁保证原子性:因为锁住总线的消耗太大,于是有了缓存锁。即在LOCK期间,处理器缓存的内存区域将会被锁定。其它处理器无法处理该块内存中的数据。
1.2.2Java实现原子操作
Java实现原子操作的方式是锁(悲观锁,乐观锁)和循环CAS
悲观锁:当发生冲突时,即多个线程去争抢共享资源的时候。只有一个线程会获得资源。即获得锁,也称独占锁或互斥锁。其它线程将会被阻塞。直到锁被释放。例如Java中使用synchronized关键字。
乐观锁:CAS就是乐观锁的一种实现方式。当一个线程尝试去获取锁,若是发现锁被占用,会继续尝试获取锁,直到获取成功。
CAS:compare and swap CAS的思想可以这样理解。有三个值,当前内存值A,旧的预期值B,更新值C。当且仅当A=C时,修改A为C,并返回true。否则什么都不做,返回false。即确认更改再赋值
1.3可见性
可见性是指当多个线程操作同一变量时,一个线程修改了变量的值,其它变量能够立刻看到被修改的值。
1.4有序性
即程序执行的顺序按照代码编写的顺序来执行。
int i = 1;
int b = 2;
boolean y = false;
// 语句1
i = 3;
// 语句2
b = 4;
为什么会有有序性的概念呢?
如以上代码,从代码顺序上来看,语句1是排在语句2前面的。但是JVM在执行的时候却不一定能保证语句1在语句2之前执行。这是因为编译器会对代码进行优化,它不保证程序中的各个语句的执行顺序同代码中的相同。但它会保证执行的最终结果是一致的。
是什么会造成代码执行顺序的变化呢?答案是指令重排序。处理器会通过指令重排序来提高程序的运行效率。
指令重排序并不会影响单个线程内代码执行的结果。
boolean y = false;
int i = 1;
// 线程一
i = 4;
y = true;
// 线程2
if (y) {
i = 5;
}
在线程并发执行的时候,线程2有可能在xian线程1执行y=true之前已经完成执行。从而导致程序的执行没有达到我们的预期。
总结:要想并发程序正确地执行,必须要同时保证原子性、可见性和有序性。只要其中一个没有保证,就有可能保证程序运行不正确。
2.volatile关键字的作用
通过了解关于并发编程的原子性、可见性以及有序性之后。就是禁止指令重排序。使代码按照我们的期望来执行。但是volatile的作用也是有限的。
当多个线程执行同一个方法对一个变量进行自增操作的时候。实际数据结果总是会小于 线程数*执行次数。
这是因为自增操作不是原子操作,而volatile只能保证可见性,并不能保证原子性。
总结:volatile关键字能够用来帮助我们实现线程安全,但是应用十分有限。即:变量的当前值和修改值之间没有约束。
3.使用场景
3.1状态标识
用代码来说明问题
volatile boolean tag = true;
public void stop(){
tag = false;
}
public void start() {
while(tag) {
// TO DO
}
}
线程1执行start()的时候,我们可以使用其它的线程来调用stop()。由于使用了共同变量,使用volatile关键字保证了其它线程对tag的修改对于线程1可见。能够使线程1及时收到终止信号。
3.2你有可能获得不完整的对象
/* volatile 关键字,解决指令重排序的问题 **/
private volatile static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton () {}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) { // 这里的if是一个避免锁竞争的优化
synchronized (LazyDoubleCheckSingleton.class) {
// 这里的if判断防止多个进入外层if条件后实例被覆盖
if (lazy == null) {
lazy = new LazyDoubleCheckSingleton();
/* CPU 执行的时候会转换成JVM指令来执行 **/
/* 指令重排序 (volatile)
* CPU 执行时第二步和第三步有可能会颠倒
*/
/* 1、分配内存给目标对象(当一个对象 new ,被分配内存) **/
/* 2、初始化对象 此时 lazy == null **/
/* 3、将初始化好的对象和内存地址建立关联(给这个对象赋值) lazy != null 但未完成初始化 **/
/* 4、用户初次访问 **/
}
}
}
return lazy;
}
通过经典的双重检查锁定示例可以发现,变量lazy在未经volatile关键字修饰的情况下。
当处理器进行指令重排序,即 执行指令顺序变成1》3》2》4,指令3执行完成,指令2未执行的时候。有其他线程来调用单例,将会返回一个不完整的对象。
总结:通过灵活运用volatile和synchronized关键字可以帮助我们写出高校的代码,比如可以使用synchronized控制变量的写操作,使用volatile来控制变量的读操作,从而提高性能。