ps:最近年底忙成狗,很久没更新了,收收心写一篇java基础
单例中使用volatile关键字
首先先看一个我们最熟悉的使用场景
DCL 双重检查单例模式
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
}
假设A执行到instance = new Singleton();
,这里看起来是一句代码,但是实际上,它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,大致做了以下3件事情:
- 给singleton的实例分配内存;
- 调用singleton()的构造方法,初始化成员字段;
- 将instance对象指向分配的内存空间(此时instance就不是null了)
但是,由于java编译器允许处理器乱序执行,执行顺序可能是1-2-3,也有可能是1-3-2,如果是后者,在2没有执行前,被切换到B线程上,这时候的instance不为空了,所以B线程直接取走了instance,再使用时就会出错,这就是DCL失效,而且很难跟踪。
在JDK1.5以后,只需要使用volatile
修饰instance,就可以保证instance对象每次都是从主内存中读取,禁止指令重排。
volatile
在讲volatile前,我们需要了解内存模型以及并发编程的3个特性:原子性,可见性和有序性
java内存模型
java中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此它存在内存可见性的问题。而局部变量,方法定义的参数则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
java内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了线程共享变量的副本。要注意的是本地内存是java内存模型的一个抽象,并不是真实的存在,它涵盖了缓存,写缓冲区,寄存器等区域。java内存模型控制线程之间的通信,决定了一个线程对主存共享变量的写入何时对另一个线程可见。
抽象示意图如下:
![](https://img.haomeiwen.com/i1981764/3b75b878cc0aeb66.png)
线程A与线程B之间通信,要经历两个步骤:
- 线程A与线程A本地内存中更新过的共享变量刷新到主存中
- 线程B到主存中去读取线程A之前已更新过的共享变量,如:int i = 3;执行线程必须先在自己的工作线程中对变量i所在的缓存中进行赋值操作,然后再写入主存中,而不是直接把3直接写入主存。
原子性,可见性和有序性
- 原子性
对基本数据类型变量的读取和赋值操作是原子性操作,不可被中断,如下
1) x = 1;
2) x = y;
3) x++;
上面3个语句中,只有语句1是原子性的,其它两个只要认真想一下,就知道不是原子性,它们都包含超过1个了的操作。其中2执行了两个操作,先读取y,再把y赋值给x,3包含了3个操作,读取x,对x进行加1,向工作内存写入新的值。
由此可知,一个语句包含了多个操作,就不是原子性操作,只有简单的读取和赋值才是原子性操作,java.util.concurrent.atomic
包中有很多类使用了高效的机器级指令(不是锁)来保证其他操作的原子性。如AtomicInteger类提供了方法incrementAndGet和decrementAndGet,它们分别以原子方式将一个整数自增和自减,使用AtomicInteger类作为共享计数器而无须同步。在多并发程序中,可以考虑使用。
-
可见性
指线程的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存,所以对其他线程是可见的。当有其他线程需要读取该值时,其他线程会去主存读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即被写入内存,何时被写入主存也是不确定的。当其他线程去读取这个值时,此时主存中可能还是原来的旧值,这样无法保证可见性。 -
有序性
java内存模型中允许编译器和处理器对指令进行重排,虽然重排过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。这时可以通过volatile来保证有序性,除了volatile,也可以通过synchronizaed和lock来保证有序性。
volatile关键字
volatile保证可见性
当一个共享变量被volatile修饰之后,其具备了两个含义,一个线程修改了变量的值时,变量的新值对其他线程是立即可见的。换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是禁止指令重排。
指令重排,通常是编译器或运行环境为了优化程序性能采用的对指令进行重新排序执行的一种手段,重排分为两类:编译期重排和运行期重排。
//线程A
boolean stop = false;
while(!stop){
//todo
}
//线程B
stop = true;
如果使用线程B来中止线程A,有一定机会造成线程A死循环。因为线程A运行时,会将stop的变量复制一份到私有的内存中去,当线程B改变了stop的变量时,线程B去执行别的任务了,这时就无法将更改的stop变量写入到主存中,这样线程A就不会知道线程B已经改了stop的值,造成死循环。解决方法就是使用volatile
修饰stop,当线程B修改时,会强制将修改的值写入主存中去,并且会导致线程A中的工作内存中变量stop缓存无效。
volatile不保证原子性
先看代码
public class VolatileTest {
public volatile int inc = 0;
public void inCreate() {
inc++;
}
@Test
public void test() {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
test.inCreate();
}
}
}.start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(test.inc);
}
}
代码每次运行,结果都不一样。分析一下,假如某个时刻,inc为9,线程1对变量进行自增,线程1先读取了inc的值,然后线程1被阻塞了,之后线程2对变量进行自增,线程2也去读取变量inc的原始值,然后进行加1操作,并把10定入工作内存,最后写入主存。线程1继续对inc进行操作,之前读取了inc为9,不会再读取了,进行加1后inc的值为10,然后把10写入主存。两个线程对inc进行了一次加1操作,但是inc只增加了1,因此volatile也无法保证变量操作的原子性
volatile保证有序性
volatile能禁止指令重排,所以能保证有序性。禁止指令重排包含了
- 当一个程序执行到volatile变量的操作时,在其前面的操作已全部完毕,且结果会对后面的操作可见,在其后面的操作还没有进行
- 在进行指令优化时,在volatile变量之前的语句不能在volatile变量后执行
- 在volatile变量之后的语句也不能在volatile变量之前执行
正确使用volatile关键字
synchronized关键字可防止多个线程同时执行一段代码,但这对程序执行效率比较大,而volatile关键字在某些情况下是是性能优于synchronized。但是并不能代替synchronized,因为volatile无法保证操作的原子性。
通常来讲,使用volatile必须有以下两个条件:
- 对变量写操作不会依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中
第一个条件就是不能是自增,自减等操作,上面已说到volatile不保证原子性。第二个条件,可以看下面的例子:
public class NumRange {
private volatile int lower,upper;
public int getLower(){
return lower;
}
public int getUpper() {
return upper;
}
public void setLower(int value) {
if (value>upper){
throw new IllegalArgumentException("error");
}
this.lower = value;
}
public void setUpper(int value) {
if (value<lower){
throw new IllegalArgumentException("error");
}
this.upper = value;
}
}
如果初始为(0,5),在同一时间内,线程a调用setLower(4)并且线程B调用setUpper(3),虽然这两个操作交叉不符合条件,但是都会通过if的保护检查,最后范围变成(4,3),显然已经出错
使用volatile的场景
- 状态标志
volatile boolean flag;
...
public void shutdown(){
flag = true;
}
public void doWork(){
while(flag){
//TODO
}
}
- DCL双重检查单例模式
如文章开头
参考
- 《android进阶之光》
End
下面的是我的公众号,欢迎大家关注我。
![](https://img.haomeiwen.com/i1981764/823a5149bfabc4e3.jpg)
网友评论