volatile
义为“挥发性的;不稳定的;爆炸性的;反复无常的”,可以看出用它来修饰的变量似乎存在一些不确定性,事实是这样吗?我们来看看它的作用。
内存可见性
Java内存模型(JMM)规定,所有的变量都存放在主内存中,每个线程都有自己的工作内存(高速缓存)。
线程在工作时,需要将主内存中的数据拷贝到工作内存中,这样一来,对这些数据的操作就是基于工作内存的,并不直接操作主内存或其他线程工作内存中的数据,操作完成之后将工作内存中的数据再刷新到主内存中去。
这里所提到的主内存可以简单认为是堆内存,而工作内存为栈内存
这样操作造成的一个问题是,在并发运行时,线程B从主内存中拿到的数据可能是“旧数据”——线程A已经将主内存中的数据拷贝并且修改了,只是还没有刷新进主内存中。这样数据不同步,显然是会出现问题的。因此volatile
就派上用场了。
被
volatile
修饰的变量,任何线程对它的写操作都会立即刷新进主内存中,并且强制让缓存了此变量的线程中的数据情况,必须重新从主内存中读取最新的数据。
volatile
修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。
内存可见性的应用
当两个线程必须依据主内存通信时,通信的变量必须用volatile
关键字修饰。
Example
package volatiletest;
import java.util.Scanner;
public class VolatileTest implements Runnable {
private volatile Boolean flag = true; //flag被volatile修饰
@Override
public void run() {
while (flag) {
}
System.out.println(Thread.currentThread().getName() + " 执行完毕...");
}
private void stopRun() {
flag = false;
}
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
new Thread(volatileTest, "Thread A").start();
System.out.println("main 线程正在运行...");
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String value = sc.next();
if (value.equals("1")) {
new Thread(new Runnable() {
@Override
public void run() {
volatileTest.stopRun();
}
}).start();
break;
}
}
System.out.println("main 线程执行完毕...");
}
}
输出结果:
main 线程正在运行...
1
main 线程执行完毕...
Thread A 执行完毕...
主线程在修改了flag
变量之后,Thread A
线程内的数据被清空,并重新从主内存中拿到被主线程main
改变的flag
,Thread A
立即线程停止运行。
而如果flag
没有被volatile
关键字修饰的话,这个过程并不会立即完成。
输出结果:
main 线程正在运行...
1
main 线程执行完毕...
(等待中...)
其实被volatile
修饰会给人一个错觉,或者说疑问:那这个变量在并发操作中是不是就是线程安全的呢?
这里要重点强调,
volatile
并不能保证线程安全性!
Example
package volatiletest;
public class VolatileNotThreadSafe implements Runnable {
private volatile int count = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
System.out.println(Thread.currentThread().getName() + "更新count为: " + count);
}
public static void main(String[] args) {
VolatileNotThreadSafe test = new VolatileNotThreadSafe();
Thread A = new Thread(test, "Thread A");
Thread B = new Thread(test, "Thread B");
A.start();
B.start();
for (int i = 0; i < 10000; i++) {
count++;
}
System.out.println("count最终为: " + count);
}
}
输出结果为:
Thread A更新count为: 16257
count最终为: 17899
Thread B更新count为: 16264
count最终为: 16054
Thread A更新count为: 15845
Thread B更新count为: 16382
...
每次结果都是不同的,但是相同点是在三个线程A
、B
和main
中都给count
自增10000后,count
最终的结果都没有达到30000。而count
是被volatile
修饰的。
这是因为虽然
volatile
保证了内存可见性,每个线程拿到的值都是最新值,但count ++
这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。
所以想到达到线程安全可以:
-
使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。
-
也可以使用
synchronized
或者是锁的方式来保证原子性。 -
还可以用
Atomic
包中AtomicInteger
来替换int
,它利用了CAS
算法来保证了原子性。
下面用第三种方法在原代码上修改后如下:
package volatiletest;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileNotThreadSafe implements Runnable {
private volatile AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
//Atomically increments by one the current value.
count.getAndIncrement();
}
System.out.println(Thread.currentThread().getName() + "更新count为: " + count);
}
public static void main(String[] args) {
VolatileNotThreadSafe test = new VolatileNotThreadSafe();
Thread A = new Thread(test, "Thread A");
Thread B = new Thread(test, "Thread B");
A.start();
B.start();
for (int i = 0; i < 10000; i++) {
count.getAndIncrement();
}
System.out.println("count最终为: " + count);
}
}
输出结果为:
Thread A更新count为: 30000
Thread B更新count为: 30000
count最终为: 30000
总结
volatile
在 Java 并发中用的很多,比如像Atomic
包中的 value
、以及 AbstractQueuedLongSynchronizer
中的 state
都是被定义为volatile
来用于保证内存可见性。将这里理解透彻对我们编写并发程序时可以提供很大帮助。
网友评论