volatile
- 保证可见性
- 不保证原子性
- 禁止指令重排
1.1 验证volatile保证可见性
import java.util.concurrent.TimeUnit;
class A {
int num = 0;
public void add() {
this.num = 1;
}
}
public class VolatileDemo {
public static void main(String[] args) {
A a = new A();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in ");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
a.add();
System.out.println(Thread.currentThread().getName() + " add " + a.num);
}, "ThreadA").start();
while (a.num == 0) {
// 如果保证了可见性的话 while循环是可以结束的
// 不加volatile的话 一直循环 ,加了之后是可以结束的 证明volatile保证了可见性
}
System.out.println(Thread.currentThread().getName() + "结束" + a.num);
}
}
2.1 验证volatile不保证原子性
二十个线程 每个线程对a.num进行1000次++操作,结果不一定等于20000,证明volatile不保证原子性
class A {
volatile int num = 0;
public void addPlusPlus() {
this.num++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
A a = new A();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
a.addPlusPlus();
// a.num++;
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " a.num:" + a.num);
// 输出结果: main a.num:19977
}
}
2.2 上面的例子证明了num++ 在多线程的情况下是非线程安全的,那么如何解决?(++操作分为三个步骤 取 加 存)
- 使用synchronized
- 使用AtomicInteger的getAndIncrement()或者IncrementAndGet()
3.1 什么是指令重排?
编译器为了优化指令序列,通常会按照一定规则对指令进行重新排序,编译器不会对存在数据依赖的指令进行重排,这里的依赖仅仅针对单线程下,多线程下此规则将失效。
3.2 volatile利用Memory Barrier(内存屏障)禁止指令重排,内存屏障的另一个作用是强制刷出CPU的缓存数据,使得CPU的任何线程都能读取到这些数据的最新版本,从而保证了可见性
下面的例子中,不能保证结果一致,存在a的最终值为2的可能
package top.ijuer.JUC;
class A {
int a = 0;
boolean flag = false;
public void m1() {
a = 1;
flag = true;
}
public void m2() {
if (flag) {
a = a + 2;
if (a != 3)
System.out.println("a:" + a);
}
}
}
public class VolatileDemo {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
A a = new A();
new Thread(() -> {
for (int j = 0; j < 10; j++) {
a.m1();
}
}, "A").start();
new Thread(() -> {
a.m2();
}, "B").start();
}
}
}
voltile 经典应用:volatile + DCL(Double Check Lock 双端检锁机制)单例模式
public class VolatileDemo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println("构造方法被调用了 " + Thread.currentThread().getName());
}
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
}
在此例中DCL并不足以保证多线程的安全,因为存在指令重排的情况。加入volatile主要是为了禁止指令重排,instance = new SingletonDemo()的执行顺序理应如下(伪代码):
- memory = alloc();申请内存
- instance(memory);实例化
- instance = memory;让引用指向实例
但是因为步骤2和3不存在数据依赖,所以编译器可能会对2和3进行指令重排,使指令顺序变为:
- memory = alloc();申请内存
- instance = memory;让引用指向实例
- instance(memory);实例化
因此存在此种情况,线程A完成了instance = memory;这个指令后被挂起,此时线程B判断instance == null得到的结果为false,会导致线程B直接拿到一个未被实例化的对象。所以在此时使用volatile禁止指令重排是有必要的。