首先来看第一个代码案例,演示什么是可见性问题。
/**
* 演示可见性带来的问题
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b = " + b + ", a = " + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
关于上述程序的运行结果,我们可以很容易分析得到如下三种情况:
然而,在实际运行过程中,还有可能会出现第四种情况(概率低),即 b = 3, a = 1。这是因为 a 虽然被修改了,但是其他线程不可见,而 b 恰好其他线程可见,这就造成了 b = 3, a = 1。
接下来,尝试分析第二个案例。
至此,解答一个问题:为什么会有可见性问题?
Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。
JMM有以下规定:
总结:所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
下面的两种解释其实是一种意思。
Happens-Before规则是用来解决可见性问题的:在时间上,动作 A 发生在动作 B 之前,B 保证能看见 A,这就是Happens-Before。
两个操作可以用Happens-Before来确定它们的执行顺序:如果一个操作Happens-Before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。
两个线程没有相互配合的机制,所以代码 X 和 Y 的执行结果并不能保证总被对方看到的,这就不具备Happens-Before。
(1) 单线程规则
(2) 锁操作(synchronized和Look)
(3) volatile变量
(4) 线程启动
(5) 线程join
(6) 传递性
传递性:如果 hb(A,B) 而且 hb(B,C),那么可以推出 hb(A,C)。
(7) 中断
中断:一个线程被其他线程 interrupt 时,那么检测中断(isInterrupted)或者抛出 InterruptedException 一定能看到。
(8) 构造方法
构造方法:对象构造方法的最后一行指令Happens-Before于 finalize() 方法的第一行指令。
(9) 工具类的Happens-Before原则
Happens-Before有一个原则是:如果 A 是对 volatile 变量的写操作,B 是对同一个变量的读操作,那么 hb(A,B)。
根据上面的原则,可以使用 volatile 关键字解决本文开头第一个案例的可见性问题。
/**
* 使用volatile关键字解决可见性问题
*/
public class FieldVisibility {
int a = 1;
volatile int b = 2; // 只给b加volatile即可
// writerThread
private void change() {
a = 3;
b = a; // 作为刷新之前变量的触发器
}
// readerThread
private void print() {
System.out.println("b = " + b + ", a = " + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
这里体现了 volatile 的一个很重要的功能:近朱者赤。给 b 加了 volatile,不仅 b 被影响,也可以实现轻量级同步。
b 之前的写入(对应代码b=a)对读取 b 后的代码(print b)都可见,所以在 writerThread 里对 a 的赋值,一定会对 readerThread 里的读取可见,所以这里的 a 即使不加 volatile,只要 b 读到是 3,就可以由Happens-Before原则保证了读取到的都是 3 而不可能读取到 1。
volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。
但是开销小,相应的能力也小,虽然说volatile是用来同步地保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。
(1) 不适用于a++
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile的不适用场景
*/
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
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());
}
}
(2) 适用场景一
如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。例如,boolean flag 操作。
注意:volatile 适用的关键并不在于 boolean 类型,而在于和之前的状态是否有关系。
在下面的程序中,setDone() 的时候,done 变量只是被赋值,而没有其他的操作,所以是线程安全的。
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile的适用场景
*/
public class UseVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new UseVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((UseVolatile) r).done);
System.out.println(((UseVolatile) r).realA.get());
}
}
在下面的程序中,虽然 done 变量是 boolean 类型的,但 flipDone() 的时候,done 变量取决于之前的状态,所以是线程不安全的。
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile的不适用场景
*/
public class NoUseVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
flipDone();
realA.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoUseVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoUseVolatile) r).done);
System.out.println(((NoUseVolatile) r).realA.get());
}
}
(3) 适用场景二
作为刷新之前变量的触发器。
可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
禁止指令重排序优化:解决单例双重锁乱序问题。
volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join() 和 Thread.start() 等都可以保证可见性。
具体看上述happens-before原则的规定。
synchronized不仅保证了原子性,还保证了可见性。
synchronized不仅让被保护的代码安全,还近朱者赤。