0%

volatile的使用及原理

一、volatile的使用

我们先理解可见性、有序性以及原子性三个概念,通常我们用synchronized关键字来解决这些问题,不过synchronized是重量级锁,对系统的性能有比较大的影响,所以如果有其他解决方案,我们都会优先考虑其他方案,避免使用synchronized关键字。而volatile关键字是就是java提供的解决可见性和有序性问题的关键字。注意:对于原子性,volatile变量的单次读写操作可以保证原子性,如long、double类型变量,但是不能保证i++这种操作的原子性,因为i++本质上是读、写两次操作。

二、volatile的使用

通过一个经典的单例(DCL单例设计模式)来说明volatile的使用。
如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Singleton {

private static volatile Singleton single;
private Singleton() {

}//私有化构造函数,禁止外部实例化
public static Singleton getInstance () {
if (single == null) {
synchronized (single) {
if (single == null) {
single = new Singleton();
}
}
}

return single;
}

public static void main(String[] args) {
// TODO Auto-generated method stub

}

}

1,防止重排序

加了volatile关键字才能真正保证线程安全,为什么呢?首先了解对象的构造过程,实例化一个对象可以分为三个步骤:

  • 1),分配内存空间
  • 2),将内存空间的地址赋给对应的引用
  • 3),初始化对象
    按照这个流程的话,多线程环境下,就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的后果,因此我们要将变量设置为volatile了类型的变量。

####,2,实现可见性
可见性指一个线程修改了共享变量,而另一个线程却看不到。引用可见性的原因是每个线程拥有自己一个高速缓存–线程工作内存。volatile关键字能有效的解决这个问题,看下面代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package test2;

public class VolatileTest {
int a = 1;
int b = 2;

void change () {
a = 3;
b = a;
}

void print() {
System.out.println("b=" + ";a+" + a);
}



public static void main(String[] args) {
while (true) {
final VolatileTest test = new VolatileTest();
new Thread(new Runnable() {

@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();

}
}).start();

new Thread (new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test.print();
}
}).start();
}

}

}

直观上说,上述代码只会有两种结果:b=2;a=1 、b=3;a=3。但是居然出了第三种结果:b=3;a=1。为什么会出现这种结果呢?正常情况下,如果先执行change方法,再执行print方法,输出结果应该是b=3;a=3。相反,如果先执行print方法,再执行change方法,结果就应该是b=2;a=1。那么b=3;a=1是怎么产生的?原因是第一个线程将值a=3修改后,但是对第二个线程是不可见的,所以才会出现这一结果,如果将a和b也改为用volatile修饰,则不会出现这样的问题了。

3,保证原子性

volatile关键字只能保证单词读写的原子性。这个在JLS中有描述。关于volatile关键字还有一个情况容易被误解,接下来用代码演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class VolatileTest2 {
volatile int i;

public void add() {
i++;
}

public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileTest2 test2 = new VolatileTest2();
for (int i = 0; i < 1000; i++) {
new Thread (new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test2.add();
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(test2.i);
}

}

这里运行结果是981(在我的电脑上),如果是线程安全的那么这段程序的运行结果应该是1000才对,我们从这里可以看出,volatile是无法保证原子性的,i++是一个复合操作,包括三个步:

  • 1),读取i的值
  • 2),对i加1操作
  • 3),将i的值写会内存

volatile是无法保证这三个步骤具有原子性的,我们可以通过AtomicInteger或者synchronized来保证+1这个操作的原子性。

三、volatile的原理

voliti有三个特征或者说三个作用。

1,保证可见性

线程本身并不与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。因此要实现volatile变量的可见性,volatile变量的读写与普通变量的读写有以下区别:

  • 1),修改volatile变量时会强制将修改后的值刷新到主内存中
  • 2), 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再次读取该变量的值的时候就要重新从主内存去读取

这两个特性,能保证volatile的可见性。

2,禁止指令重排序

看一段代码:

1
2
3
4
5
int a=1,b=3;//1
b++;//2
volatile int c;//3

c = a + b;//4

处理器有的时候会将执行的顺序进行优化,会调乱执行的顺序,禁止指令重排序的意思是说第三行代码之前代码一定会全部执行完之后,才会执行第四行的代码,至于第三行代码前面的代码什么顺序,volatile不关心,也控制不了,但是一定能保证他们会在4之前执行。

3,内存屏障

四、总结

只有在一种情况下使用volatile才能保证并发安全,同时满足两个条件:

  • 1,对变量的写操作不依赖于当前值
  • 2,该变量没有包含在具有其他变量的不变式中