一、简介
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见,需要我们自己来控制。
二、案例展示
1、线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见,即 使用 synchronized 保证了可见性。
static int x;static Object m = new Object();new Thread(()->{synchronized(m) {x = 10;}},"t1").start();new Thread(()->{synchronized(m) {System.out.println(x);}},"t2").start();
2、线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;new Thread(()->{x = 10;},"t1").start();new Thread(()->{System.out.println(x);},"t2").start();
3、线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;x = 10;new Thread(()->{System.out.println(x);},"t2").start();
4、线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join() 等待它结束)
static int x;Thread t1 = new Thread(()->{x = 10;},"t1");t1.start();t1.join();System.out.println(x);
5、线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
static int x;public static void main(String[] args) {Thread t2 = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {System.out.println(x);break;}}}, "t2");t2.start();new Thread(() -> {sleep(1);x = 10;t2.interrupt();}, "t1").start();while (!t2.isInterrupted()) {Thread.yield();}System.out.println(x);}
6、对变量默认值(0,false,null)的写,对其它线程对该变量的读可见,并且具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,如下代码:
volatile static int x;static int y;new Thread(()->{y = 10;x = 20;},"t1").start();new Thread(()->{// x=20 对 t2 可见, 同时 y=10 也对 t2 可见System.out.println(x);},"t2").start()
三、习题演练
3.1 balking 模式
下面的代码只希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?
public class TestVolatile {volatile boolean initialized = false;void init() {if (initialized) {return;}doInit();initialized = true;}private void doInit() {}
}
存在问题,假设 t1 和 t2 线程同时执行 init() 方法,当执行 if 判断的时候,都是 false,每个线程都会执行一次 doInit() 方法,虽然 initialized 使用了 volatile 修饰,但还是无法解决原子性问题,需要使用 synchronized 来解决。
3.2 线程安全单例习题
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用 getInstance)时的线程安全。
3.2.1 饿汉式
饿汉式的特点是类加载就会导致该单实例对象被创建,如下代码:
// 问题1:为什么加 final?
// 回答:防止被子类继承,破坏单例// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例?
// 回答:需要在单例类中加一个返回值类型为 Object 的 readResovle() 方法,方法的返回值为单例对象。
// 在反序列的过程中,一旦发现 readResolve() 方法返回了对象,那么它就会采用你返回的这个对象。
public final class Singleton implements Serializable {// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?// 回答:防止被其他类创建对象,不能防止反射来创建新的实例,因为反射可以获取构造器对象,还可以设置相关的属性,创建新的实例。private Singleton() {}// 问题4:这样初始化是否能保证单例对象创建时的线程安全?// 回答:可以保证,是 JVM 帮我们保证线程安全的private static final Singleton INSTANCE = new Singleton();// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由// 回答:方法可以提供更好的封装性,内部可以实现懒惰的初始化。还可以支持泛型public static Singleton getInstance() {return INSTANCE;}public Object readResolve() {return INSTANCE;}
}
3.2.2 枚举
可以使用枚举类实现单例。如下代码
// 问题1:枚举单例是如何限制实例个数的
// 回答:枚举类里面定义的枚举对象,定义了几个将来就有几个对象,它相当于是枚举类里面的静态成员变量。// 问题2:枚举单例在创建时是否有并发问题
// 回答:没有,因为静态成员变量是在类加载阶段完成的,不存在并发问题。// 问题3:枚举单例能否被反射破坏单例
// 回答:不能// 问题4:枚举单例能否被反序列化破坏单例
// 回答:不能// 问题5:枚举单例属于懒汉式还是饿汉式
// 回答:由于也是类加载阶段创建的,所以也属于饿汉式的// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
// 回答:加一些构造方法即可
enum Singleton {INSTANCE;
}
3.2.3 懒汉式
懒汉式的特点是类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建,如下代码:
public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;// 问:分析这里的线程安全, 并说明有什么缺点?// 回答:是线程安全的,但是锁的范围有点大,每次调用都需要加锁,导致性能很低public static synchronized Singleton getInstance() {if( INSTANCE != null ){return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}
}
3.2.4 双重锁检查
如下代码:
public final class Singleton {private Singleton() { }// 问题1:解释为什么要加 volatile ?// 回答:因为 synchronized 里面构造方法的指令和赋值的指令会重排序,加上 volatile 可以防止指令重排序private static volatile Singleton INSTANCE = null;// 问题2:对比实现3, 说出这样做的意义// 回答:这种方式只有第一次调用时会调用同步代码块,后面的调用直接返回了,提高了性能public static Singleton getInstance() {if (INSTANCE != null) {return INSTANCE;}synchronized (Singleton.class) {// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗// 回答:是为了防止第一次并发访问时单例对象不要被重复创建if (INSTANCE != null) { // t2return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}}
}
3.2.5 静态内部类
如下代码:
public final class Singleton {private Singleton() { }// 问题1:属于懒汉式还是饿汉式// 回答:是懒汉式的,只有当用到的时候,才会对 LazyHolder 类进行加载,对里面的静态变量进行初始化private static class LazyHolder {static final Singleton INSTANCE = new Singleton();}// 问题2:在创建时是否有并发问题// 回答:不会存在线程安全问题。public static Singleton getInstance() {return LazyHolder.INSTANCE;}
}