回顾之前Java内存模型特征可以了解到该模型是围绕着并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。
原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个,如果应用场景需要更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足需求,比如synchronize关键字。在synchronize块之间的操作就具备原子性。
可见性:指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。
有序性:在本线程内,所有操作都是有序的,即按照代码先后顺序执行;如果在一个线程观察另一个线程,所有操作都是无序的,因为有“指令重排序”现象和“工作内存与主内存同步延迟”现象。
了解上述并发处理三大特性之后,再看volatile关键字,该关键字满足可见性和有序性,所以在Java中能提供最轻量级的同步机制。
volatile可见性:volatile的规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此volatile保证了多线程操作变量的可见性。
volatile有序性:使用volatile修饰的变量会禁止指令重排序,JVM中对不是原子性的操作会进行重排序的优化,只要不影响最终计算结果。
例如:
// 线程1中
{
...
obj= getObject(); // 步骤1
isRegister = true; // 步骤2
...
}
// 线程2中
{
...
if (isRegister) { // 步骤3,依赖步骤2的值
fun(obj); // 步骤4,依赖步骤1的值
}
...
}
上述代码中,线程1中的代码步骤1和步骤2可能出现重排序的情况,因为对线程1来说顺序打乱不影响线程1自己的运算结果,但是对线程2来说,如果线程1中的步骤2先执行,最终就无法得到正确的结果。
volatile修饰的变量,在读取或者写入的前后都会插入内存屏障来达到禁止重排序的效果,进而保证有序性。
volatile不能保证原子性,因此不能完全达到线程安全效果,除非满足以下条件:
- 运算结果不依赖当前值,或者能够确保只有单一的线程修改该变量的值。
- 变量不需要与其他状态变量共同参与不变约束。
比如volatile修饰的变量count出现count++、count+1等非原子操作,就无法确保线程安全。
import java.io.*;
class test
{
private static final int THREAD_COUNT = 20;
public static volatile int race = 0;
public static void increase () {
race++;
}
public static void main (String[] args) throws java.lang.Exception
{
System.out.println("hi");
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
increase();
}
}
}).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("race = "+ race);
}
}
运行结果:
hi
race = 19902
注意,测试的结果跟环境有关,有的测试环境可能结果出现正确的情况,可以将测试数据改大一点。本人测试时,开始选择10个线程,然后只累加10次,测试下来发现结果都是正确的~
另外大家可能会疑惑volatile不是保证变量的可见性了吗?一旦被修改会立即同步到主内存中,确保其它线程都拿到最新数据。这里可以这样理解,比如线程1和线程2都拿到最新volatile修饰的变量count = 10,当count++时,由于不是原子操作,当线程1还在执行加加指令时,线程2已经将count数据11更新到主存了,此时对于线程1来说,数据已经过时了,等线程1执行完时,又将11同步到主存,结果导致两个线程都执行count++,但是最终结果却小于正常值。
Android中volatile经常跟单例模式一起使用,确保线程安全。