1. volatile是什么?
volatile是一种同步机制
,比synchronized或者Lock相关类更轻量
,因为使用volatile并不会发生上下文切换
等开销很大的行为。
如果一个变量别修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。
但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护
, volatile仅在很有限的场景
下才能发挥作用。
2. volatile的适用场合
2.1.不适用: a++
/**
* 描述: 不适用于volatile的场景
*/
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile) r).a);
System.out.println(((NoVolatile) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
}
2.2.适用场合
1.对变量的写操作不依赖于当前值。
如果一个共享变量自始至终只被各个线程赋值
,而没有其他的操作,也不依赖原来的状态,那么就可以volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
作为触发器,实现轻量级同步。
public class UseVolatile1 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new UseVolatile1();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((UseVolatile1) r).done);
System.out.println(((UseVolatile1) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
}
true
20000
注意:volatile修饰boolean变量不能依赖原来的状态,否则boolean变量是不可靠的
错误使用
/**
* 描述: volatile不适用的情况2
*/
public class NoVolatile2 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile2();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile2) r).done);
System.out.println(((NoVolatile2) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
flipDone();
realA.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
}
输出结果
true
20000
或者
false
20000
2.作为刷新之前变量的触发器
Map configOptions;
char[] configText;
volatile boolean initialized = false;
. . .
// In thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
. . .
// In thread B
while (!initialized)
sleep();
// use configOptions
有一个 map 叫作 configOptions,还有一个 char 数组叫作 configText,然后会有一个被 volatile 修饰的 boolean initialized,最开始等于 false。再下面的这四行代码是由线程 A 所执行的,它所做的事情就是初始化 configOptions,再初始化 configText,再把这两个值放到一个方法中去执行,实际上这些都代表了初始化的行为。那么一旦这些方法执行完毕之后,就代表初始化工作完成了,线程 A 就会把 initialized 这个变量设置为 true
而对于线程 B 而言,它一开始会在 while 循环中反复执行 sleep 方法(例如休眠一段时间),直到 initialized 这个变量变成 true,线程 B 才会跳过 sleep 方法,继续往下执行。重点来了,一旦 initialized 变成了 true,此时对于线程 B 而言,它就会立刻使用这个 configOptions,所以这就要求此时的 configOptions 是初始化完毕的,且初始化的操作的结果必须对线程 B 可见,否则线程 B 在执行的时候就可能报错
因为这个 configOptions 是在线程 A 中修改的,那么在线程 B 中读取的时候,会不会发生可见性问题,会不会读取的不是初始化完毕后的值?如果我们不使用 volatile,那么确实是存在这个问题的
但是现在我们用了被 volatile 修饰的 initialized 作为触发器,所以这个问题被解决了。根据happens-before 关系的单线程规则,线程 A 中 configOptions 的初始化 happens-before 对 initialized 变量的写入,而线程 B 中对 initialzed 的读取 happens-before 对 configOptions 变量的使用,同时根据 happens-before 关系的 volatile 规则,线程 A 中对 initialized 的写入为 true 的操作 happens-before 线程 B 中随后对 initialized 变量的读取
如果我们分别有操作 A 和操作 B,我们用 hb(A, B) 来表示 A happens-before B。而 Happens-before 是有可传递性质的,如果hb(A, B),且hb(B, C),那么可以推出hb(A, C)。所以根据上面的条件,我们可以得出结论:线程 A 中对于 configOptions 的初始化 happens-before 线程 B 中 对于 configOptions 的使用。所以对于线程 B 而言,既然它已经看到了 initialized 最新的值,那么它同样就能看到包括 configOptions 在内的这些变量初始化后的状态,所以此时线程 B 使用 configOptions 是线程安全的。这种用法就是把被 volatile 修饰的变量作为触发器来使用,保证其他变量的可见性
volatile 的作用是什么?与 synchronized 有什么异同?
3.volatile 的作用
第一层的作用是保证可见性。Happens-before 关系中对于 volatile 是这样描述的:对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作
这就代表了如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值
第二层的作用就是禁止重排序。as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了 volatile 关键字就可以在一定程度上禁止这种重排序
4.volatile 和 synchronized 的关系
相似性:volatile 可以看作是一个轻量版的 synchronized,比如一个共享变量如果自始至终只被各个线程赋值和读取
,而没有其他操作
的话,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,足以保证线程安全。实际上,对 volatile 字段的每次读取或写入都类似于“半同步”——读取 volatile 与获取 synchronized 锁有相同的内存语义,而写入 volatile 与释放 synchronized 锁具有相同的语义
不可代替:但是在更多的情况下,volatile 是不能代替 synchronized 的,volatile 并没有提供原子性和互斥性
性能方面:volatile 属性的读写操作都是无锁的,正是因为无锁,所以不需要花费时间在获取锁和释放锁上,所以说它是高性能的,比 synchronized 性能更好
4.总结
- volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。
- volatile属性的
读写操作都是无锁
的,它不能替代synchronized,因为它没有提供原子性
和互斥性
。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本
的。 - volatile只能作用于属性,我们用volatile
修饰属性
,这样compilers就不会对这个属性做指令重排序
。 - volatile提供了
可见性
,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主存中读取
。 - volatile提供了
happens-before
保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。 - volatile可以使得long和double的
赋值是原子
(a++等不是)的。 - volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。