Java中volatile关键字

简介

在Java内存模型中,有主内存和每个线程各自的工作内存。为了提高运行性能,一个线程会在自己的内存中拷贝一份成员变量的副本。而工作内存在线程之间是互相隔离的,彼此对其他线程不可见。线程对变量的所有操作都必须在工作内存中进行,修改结束后,变量副本需写会主内存。这样的内存机制变回导致同一个变量在某个时间点,在一个线程的内存中的值(如B)与另一个线程的内存中的值(如C),或是与主内存中的值(如A)不一致的情况。而volatile关键字正是为了避免这种情况,它会告诉JVM(Java虚拟机),被volatile修饰的变量不在其他线程的内存中保留拷贝,而是直接访问主内存的变量。

volatile关键字用法

当一个变量声明为volatile时,就意味着这个变量被修改时其他所有使用到此变量的线程都立即能见到变化(称之为可见性),并可随时被其他线程修改。具体是在每次使用前都要先刷新,以保证别的线程中的修改已经反映到本线程工作内存中,因此可以保证执行时的一致性。举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StoppableTask extends Thread {  
private volatile boolean pleaseStop;

public void run() {
while (!pleaseStop) {
// do some stuff...
}
}

public void tellMeToStop() {
pleaseStop = true;
}
}

假如pleaseStop变量没有用volatile声明,那么在线程执行run的时候检查的就是自己的变量副本,如果其他线程已经调用了tellMeToStop()方法修改了pleaseStop的值,当前线程就无法及时得知此消息。

volatile和sychronized

Volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性!这里要注意,i++这样的操作并不是原子性的,它实际上也是由多个原子操作组成的。假如多个线程同时执行i++,volatile只能保证他们操作的i变量是同一块内存,但依然可能出现写入脏数据的情况。

Volatile是变量修饰符,而synchronized作用于一段代码或方法。举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int i1;
int geti1() {
return i1;
}

volatile int i2;
int geti2() {
return i2;
}

int i3;
synchronized int geti3() {
return i3;
}

geti1()

在上面的代码中,3个get()方法都是用来取得当前线程中对于变量的值。对于geti1()方法,多个线程中有着多个i1变量的拷贝,并且这些值可以互不相同。因此有如下可能性:主内存中 i1 = 0,线程1中 i1 = 1,线程2中 i1 = 2……这里线程1和线程2中都改变了它们各自的i1的值,而这个改变还未及时传递给主内存和其他线程,就导致了这种情况的发生。

对于geti2()方法,无论哪个线程去调用,得到的都是主内存中的i2的值。因为i2是被volatile修饰的,它在所有线程中必须是同步的,任何线程改变了它的值的同时,其他线程便立即获得了相同的值。注意:volatile修饰的变量存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。

既然volatile关键字已经实现了线程间数据同步,为什么我们还要用synchronized呢?一方面,synchronized会获得并释放监视器。话句话说,如果两个线程使用了同一个对象锁,监视器能强制保证被synchronized修饰的代码块或方法同时只被一个线程所执行。另一方面,synchronized也同步内存。它在“主内存区域同步整个线程的内存。因此,上述geti3()方法在执行时做了如下几步:

  1. 线程请求获得监视this对象的对象锁(假设此时还未被锁,否则线程将等待直到锁释放后获得)
  2. 线程内存的数据被消除,从主内存区域中读入
  3. 代码块被执行
  4. 对于变量的任何改变现在可以安全地写到主内存区域中(上述代码中geti3()方法并没有修改变量值)
  5. 线程释放监视this对象的对象锁

总结一下就是:volatile关键字只是在线程内存和主内存间同步某个变量的值,而synchronized通过加锁和解锁,保证代码块被执行的安全,并同步所有变量的值。所以synchronized要比volatile消耗更多资源。

volatile与变量类型

在Java中,volatile关键字一般用于声明简单类型变量,比如int、float、 boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会成为原子性操作。但这也有一定的限制(如之前提到的i++操作),举个栗子:

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
public class MyThread extends Thread {

public volatile int n = 0;

public void run() {
for (int i = 0; i < 10; i++) {
try {
n = n + 1;
sleep(2);
} catch (Exception e) {
System.out.println("Some exceptions...");
}
}
}

public static void main(String[] args) throws Exception {
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new MyThread(); // 创建100个线程
}
for (int i = 0; i < threads.length; i++) {
threads[i].start(); // 运行刚刚的线程
}
for (int i = 0; i < threads.length; i++) {
threads[i].join(); // 100个线程都执行完后继续
}
System.out.println("n = " + MyThread.n);
}
}

如果上述代码对变量n的操作是原子性的,那么最后输出的结果应该为 n = 1000。但是在执行上面积代码时,很多时侯输出的n都小于1000,这说明 n = n + 1 操作不是原子性的!原因是,如果声明为volatile的简单变量的当前值和该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:

n++;

n = n + 1;

如果要想使这种情况成为原子性操作,就需要使用synchronized关键字。上述的代码可以改成如下的形式:

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
public class MyThread extends Thread {

public volatile int n = 0;

public static synchronized void addOne() {
n++; // 或 n = n + 1;
}

public void run() {
for (int i = 0; i < 10; i++) {
try {
addOne();
sleep(2);
} catch (Exception e) {
System.out.println("Some exceptions...");
}
}
}

public static void main(String[] args) throws Exception {
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new MyThread(); // 创建100个线程
}
for (int i = 0; i < threads.length; i++) {
threads[i].start(); // 运行刚刚的线程
}
for (int i = 0; i < threads.length; i++) {
threads[i].join(); // 100个线程都执行完后继续
}
System.out.println("n = " + MyThread.n);
}
}

上面的代码将简单的 n = n + 1 操作抽象成了 addOne() 方法,并使用synchronized关键字修饰,这时线程对于 n = n + 1 的操作便成为了原子性的。

在使用volatile关键字时一定要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作就都成为了原子性操作而保证线程安全,当变量的值受自身之前的值影响时,如 n = n + 1、n++ 等,volatile关键字便会失效;只有当变量的值和自身上一个值无关时,该变量的操作才是原子性的,如 n = m + 1 这个操作就是原子性的。

其他

Volatile的另外一个作用是禁止指令的重排序优化。在一般情况下,Java执行语句的顺序可能会因为自动优化而修改,例如下面的例子,initialized的赋值有可能执行在doInitialize()语句之前,从而导致其他线程可能不会正确的等待初始化完成。

boolean initialized = false;

1
2
3
// run in one thread
doInitialize();
initialized = true;
1
2
3
4
// run in another thread
while (!initialized) {
sleep();
}

如果将initialized变量用volatile关键字修饰,就能保证它的执行顺序不会被改变(似乎 Java SE 5之前的版本依然会有问题)

Reference

Java的volatile关键字的作用

Java中关键字volatile的作用

文章作者:Xiao

原始链接:https://zxshwan.github.io/archives/34ac48e.html

许可协议: 署名-非商业性使用 转载请保留原文链接及作者。

0%