JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。

JMM体现在以下几个方面:

  • 原子性:保证指令不会受到线程上下文切换的影响
  • 可见性:保证指令不会受CPU缓存的影响
  • 有序性:保证指令不会受CPU指令并行优化的影响

3.1 可见性

3.1.1 退不出的循环

先看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:

@Slf4j(topic = "While")
public class WhileDemo {
    static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) { }
        }, "t");
        t.start();
        TimeUnit.SECONDS.sleep(1);
        run = false;
    }
}

3.1.2 分析

  1. 初始状态,t线程刚开始从主内存读取了run的值到工作内存。
  1. 因为t线程要频繁地从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率。

  2. 1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。

3.1.3 解决办法

(1)给变量run加上修饰符volatile

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volitale变量都说直接操作主存。

(2)使用synchronized

@Slf4j(topic = "While")
public class WhileDemo {
    private static volatile boolean run = true;
    private static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                synchronized (lock) { }
            }
        }, "t");
        t.start();
        TimeUnit.SECONDS.sleep(1);
        run = false;
        synchronized (lock) {
            run = false;
        }
    }
}

(3)在while循环中使用System.out.println()也会终止循环,原因可以看println()的源代码,其中执行的是PrintStream#newLine()方法:

private void newLine() {
    try {
        synchronized (this) {  // 加锁
            ensureOpen();
            textOut.newLine();
            textOut.flushBuffer();
            charOut.flushBuffer();
            if (autoFlush)
                out.flush();
        }
    }
    catch (InterruptedIOException x) {
        Thread.currentThread().interrupt();
    }
    catch (IOException x) {
        trouble = true;
    }
}

3.1.4 synchronized/volatile

  • synchronized可以修饰变量和方法,volatile只能修饰变量。
  • synchronized保证操作的原子性,volatile保持变量的可见性。
  • synchronized通常适用于写多读少的场景,volatile通常适用于写少读多的场景。

3.2 模式

3.2.1 终止模式之两阶段终止模式

使用volatile实现两阶段终止:

public class TwoStageTerminationDemo {
    public static void main(String[] args) throws InterruptedException {
        TwoStageTermination tst = new TwoStageTermination();
        tst.start();
        Thread.sleep(3500);
        tst.stop();
    }
}

@Slf4j(topic = "TwoStageTermination")
class TwoStageTermination {
    // 监控线程
    private Thread monitorThread;
    // 是否被打断
    private volatile boolean stop = false;

    public void start() {
        monitorThread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 是否被打断
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    log.info("执行监控...");
                } catch (InterruptedException e) {
                    // 睡眠被打断被清除打断标记,需要重新设置
                    current.interrupt();
                }
            }
        }, "monitor");
        monitorThread.start();
    }

    public void stop() {
        stop = true;
    }
}

3.2.2 同步模式之Balking

Balking(犹豫)模式用在一个线程发现另一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回,实际上是一种单例模式。

修改两阶段终止如下,监控线程执行一次之后就不再执行。

public class BalkingTwoStageTerminationDemo {
    public static void main(String[] args) throws InterruptedException {
        BalkingTwoStageTermination btst = new BalkingTwoStageTermination();
        btst.start();
        TimeUnit.SECONDS.sleep(3);
        btst.stop();
        TimeUnit.SECONDS.sleep(1);
        btst.start();
        TimeUnit.SECONDS.sleep(3);
        btst.stop();
    }
}

@Slf4j(topic = "BalkingTwoStageTermination")
class BalkingTwoStageTermination {
    // 监控线程
    private Thread monitorThread;
    // 是否被打断
    private volatile boolean stop = false;
    // 是否已执行
    private volatile boolean started = false;

    // 启动
    public void start() {
        synchronized (this) {
            if (started) {
                return;
            }
            stop = false;
            started = true;
        }
        monitorThread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 是否被打断
                if (stop) {
                    log.debug("停止监控");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("执行监控...");
            }
        }, "monitor");
        monitorThread.start();
    }

    // 暂停
    public void stop() {
        stop = true;
        started = false;
    }
}

3.3 有序性

3.3.1 指令重排序

JIT即时编译器的优化,可能会导致指令重排。JVM会在不影响正确性的前提下,调整语句的执行顺序。

(1)例如以下代码:

static int i, j;

// 操作
i = 0;
j = 1;

对于ij的赋值操作顺序,对最终的结果都没有影响。因此在真正执行的时候,可能i也可能是j先被赋值。但是在多线程情况下指令重排序会影响正确性。

(2)比如new一个对象: 一般顺序为(1)分配内存(2)对象初始化(3)建立指针对应关系,高并发情况下顺序可能乱成132,对象初始化时就被另一个线程读取造成问题。

3.3.3 诡异的结果

在如下代码中,I_Result 是一个对象,有一个属性r1用来保存结果。

线程1执行actor1方法,线程2执行actor2方法,思考r1的可能结果。

int num = 0;
boolean ready = false;

public void actor1(I_Result r) {
    if (ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

public void actor2(I_Result r) {
    num = 2;
    ready = true;
}
  1. 线程1先执行,线程2后执行,此时结果为1
  2. 线程2先执行,线程1后执行,此时结果为4
  3. 线程2先执行,执行到num = 2,线程1开始执行,此时结果为1
  4. 线程2先执行,但是指令重排,先执行ready = true,线程1开始执行,进入if (ready),此时结果为2

解决方法:给ready加上修复符volatile

3.3.2 重排序规则

  • 指令重排序不会对存在数据依赖关系的操作进行重排序。比如:a= 1; b = a;
  • 重排序是为了优化性能,但是不管如何重排,单线程下程序的执行结果不能改变。
  • 指令重排序保证单线程模式下的结果正确性,但是不保证多线程模式下的正确性。
  • 解决方法:volatile修饰的变量,可以禁用指令重排。

3.4 volatile原理

volatile的底层实现原理是内存屏障,Memory barrier。

  • volatile变量的写指令后会加入写屏障。
  • volatile变量的读指令后会加入读屏障。

3.4.1 保证可见性

写屏障保证在该屏障之前的,对共享变量的改动,都同步在主存当中。

public void actor2(I_Result r) {
    num = 2;
    readt = true; // ready是volatile赋值带写屏障
    // 写屏障
}

读屏障保证在该屏障之后的,对共享变量的读取,加载的是主存中的最新数据。

public void actor1(I_Result r) {
    // 读屏障
    // ready是volatile读取值带读屏障
    if (ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

3.4.2 保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。

public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready是volatile赋值到
       // 写屏障
}

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

public void actor1(I_Result r) {
    // 读屏障
    // ready是volatile读取值
    if (ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

但是不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读在写操作之前还是之后
  • 有序性的保证也只是保证了本线程内相关代码不被重排序

3.4.3 double-checked locking

以著名的DCL(double-checked locking)单例模式为例

public class DCLSingleton {
    private DCLSingleton() { }
    private static DCLSingleton INSTANCE = null;
    /**
     * 在第一次线程调用getInstance(),直接在synchronized外,判断instance对象是否存在
     * 如果不存在,才会去获取锁,然后创建单例对象,并且返回;第二个线程调用getInstance(),
     * 会进行instance的空判断,如果已经有单例对象就不会去同步块中获取锁,提高效率。
     */
    private static DCLSingleton getInstance() {
        if (INSTANCE == null) {
            // 这个判断并不在synchronized同步代码块中,
            // 不能保证原子性、可见性和有序性,可能导致指令重排
            synchronized (DCLSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new DCLSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

以上实现的特点:

  • 延迟实例化
  • 首次使用getInstance()才使用synchronbized加锁,后续使用时无需加锁。
  • 有隐含的,但很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外。

但是在多线程环境下,上面的代码是有问题的,getInstance()方法对应的字节码为:

 0 getstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE>  # 获取静态变量INSTANCE
 3 ifnonnull 37 (+34)                                  # 判断INSTANCE是否为空
 6 ldc #3 <top/parak/jmm/DCLSingleton>                 # 从运行时常量池中将DCLSingeton的class对象推入操作数栈
 8 dup                                                 # 复制操作数栈栈顶的值,即DCLSingleton的class对象
 9 astore_0                                            # 将DCLSingleton的class对象存入局部变量表索引为0的位置
10 monitorenter                                        # 进入INSTANCE对象的monitor
11 getstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE>  # 获取静态变量INSTANCE
14 ifnonnull 27 (+13)                                  # 判断INSTANCE是否为空
17 new #3 <top/parak/jmm/DCLSingleton>                 # new一个DCLSingleton
20 dup                                                 # 复制操作数栈栈顶的值,即DCLSingleton的class对象
21 invokespecial #4 <top/parak/jmm/DCLSingleton.<init>># 调用DCLSingeton的构造方法
24 putstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE>  # 将新对象赋值给静态变量INSTANCE
27 aload_0                                             # 从局部变量表导出索引为0位置的元素
28 monitorexit                                         # 退出INSTANCE对象的monitor
29 goto 37 (+8)                                        # goto37行处理
32 astore_1                                            # 32-35是异常
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE> # 获取静态变量INSTANCE
40 areturn

重点关注17-24行:

  • 17:创建DCLSingleton对象
  • 20:复制DCLSingleton的class对象
  • 21:调用class对象的默认构造方法
  • 24:将新对象赋值给静态变量INSTANCE

也许JVM会优化为:先执行24,再执行21,即先赋值,再调构造。

可能造成:一个线程正在造对象,对象还为空的时候就被另一线程拿去使用。

解决方法:

public class DCLSingleton {
    private DCLSingleton() { }
    private volatile static DCLSingleton INSTANCE = null;
    private static DCLSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (DCLSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new DCLSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

从字节码上看不出volatile的效果,但是我们从屏障的角度分析:

 # ====================================================> 加入对INSTANCE的读屏障
 # 保证此后的读取,加载的都是主存中的最新数据
 0 getstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE>
 3 ifnonnull 37 (+34)
 6 ldc #3 <top/parak/jmm/DCLSingleton>
 8 dup
 9 astore_0
10 monitorenter # =====================================> 保证原子性和可见性
11 getstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE>
14 ifnonnull 27 (+13)
17 new #3 <top/parak/jmm/DCLSingleton>
20 dup
21 invokespecial #4 <top/parak/jmm/DCLSingleton.<init>>
24 putstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE>
 # ====================================================> 加入对INSTANCE的写屏障
 # 保证此前的写入,都同步到主存当中
27 aload_0
28 monitorexit  # =====================================> 保证原子性和可见性
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <top/parak/jmm/DCLSingleton.INSTANCE>
40 areturn

3.4.4 happens-before

happens-before规定了对共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见。

  1. 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
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");
  1. 线程对volatile变量的写,对接下来其他线程对改变了的读可见
volatile static int x;

new Thread(() -> {
    x = 10;
}, "t1").start();

new Thread(() -> {
    System.out.println(x);
}, "t2").start();
  1. 线程start前对变量的写,对该线程开始后对该变量的读可见
static int x;

x = 10;

new Thread(() -> {
    System.out.println(x);
}, "t2").start();
  1. 线程结束前对变量的写,对其他线程得知它结束后的读可见(比如其他线程调用t1.isAlive()t1.join()等待它结束)
static int x;

Thread t1 = new Thread(() -> {
    x = 10;
});
t1.start();

t1.join();
System.out.println(x);
  1. 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见
@Slf4j(topic = "InterruptRead")
public class InterruptReadDemo {
    private static int x;
    public static void main(String[] args) {
        Thread t2 = new Thread(() -> {
            while (true) {
                log.debug("x: {}", x);
                if (Thread.currentThread().isInterrupted()) {
                    log.debug("x: {}", x);
                    break;
                }
            }
        }, "t2");
        t2.start();
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            x = 3;
            t2.interrupt();
        }, "t1").start();
        while (!t2.isInterrupted()) {
            Thread.yield();
        }
        log.debug("x: {}", x);
    }
}
  1. 对变量默认值(0,false,null)的写,对其他线程对该变量的读可见

  2. 具有传递性,如果x hb => y并且y hb => z那么有x hb => z,配合volatile的防止指令重排序,有下面的例子

    volatile static int x;
    static int y;
    
    new Thread(() -> {
        y = 10;
        x = 10;
    }, "t1").start();
    
    new Thread(() -> {
        // x = 20对t2可见
        // 同时y = 20对t2可见 
        System.out.println(x);
    }, "t2").start();

3.5 习题

3.5.1 balking模式

希望doInit()方法仅被调用一次,下面的实现是否有问题,为什么?

@Slf4j(topic = "BalkingPractice")
public class BalkingPractice {
    static volatile boolean initialized = false;

    private static void init() {
        if (initialized) {
            return;
        }
        doInit();
        initialized = true;
    }

    private static void doInit() {
        log.debug("init...");
    }
}

有问题,volatile无法保证原子性,当多个线程同时调用init()方法时,此时都进入到if判断,都调用doInit()方法,此时就调用了多次。

解决方法:对init()方法的方法体,通过synchronized加锁,防止多个线程共享initialized

@Slf4j(topic = "BalkingPractice")
public class BalkingPractice {
    static volatile boolean initialized = false;
    final static Object lock = new Object();

    private static void init() {
        synchronized (lock) {
            if (initialized) {
                return;
            }
            doInit();
            initialized = true;
        }
    }

    private static void doInit() {
        log.debug("init...");
    }
}

3.5.2 线程安全单例

饿汉式

// 问题1:为什么加final?
// 答:防止子类继承修改。
public final class Singleton implaments Serializable {
    // 问题2:为什么构造函数设为私有?是否能防止反射创建新的实例?
    // 答:防止其他类中使用new生成新的实例,不能防止反射创建新的实例。
    private Singleton() { }

    // 问题3:这样初始化是否能保证单例对象创建时的线程安全?
    // 答:能,类变量在JVM类加载的初始化(clinit)阶段完成赋值,JVM保证此操作的线程安全性。
    private static Singleton INSTANCE = new Singleton();

    // 问题4:为什么提供静态方法而不是直接将INSTANCE设置为public?
    // 答:(1)提供更好的封装性(2)提供泛型的支持
    public static Singleton getInstance() {
        return INSTANCE;
    }

    // 问题5:这个方法有什么作用?
    // 答:防止反序列化时生成不同的单例对象。
    public Object readResolve() {
        return INSTANCE;
    }
}

DCL懒汉式

public final class DCLSingleton {
    private DCLSingleton() { }
    // 问题1:为什么要给单例变量加上volatile关键字修饰?
    // 答:防止第一次创建对象时指令重排序。
    private volatile static DCLSingleton INSTANCE = null;
    private static DCLSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (DCLSingleton.class) {
                // 问题3:前面已经进行一次判空,为什么还要在这里加上为空判断?
                // 答:防止多线程并发导致不安全的问题:单例对象重复创建。
                // 比如:有两个线程。都进入了第一次判空,它们都判断单例为空。
                // (1)t1线程获取到单例对象锁,然后创建对象,释放锁。
                // (2)t2线程获取到单例对象锁,由于第一次判空为真,
                //     如果没有进行第二次判空,它也会创建单例对象,破坏单例。
                if (INSTANCE == null) {
                    INSTANCE = new DCLSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

静态内部类

public final class Singleton {
    private Singleton() { }
    // 问题1:静态内部类属于饿汉式还是懒汉式?
    // 答:属于懒汉式,当Sinleton加载的时候,SingletonHolder并没有加载进内存,只要当第一次调用getInstance的时候,SingletonHolder才会加载进内存,并且初始化INSTANCE实例。
    private static class SingletonHolder {
        // 问题2:这样创建单例对象是否存在线程安全问题?
        // 答:类加载时JVM会保证线程安全。
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return Singletonolder.INSTANCE;
    }
}

枚举类

// 问题1:枚举单例是如何限制实例个数的?
// 答:创建枚举类的时候就已经定义好了,每个枚举常量其实就是枚举类的一个静态成员变量。
// 问题2:枚举单例在创建时是否有并发问题?
// 答:没有并发问题,枚举类成员底层是一个静态成员边框,在类加载时就创建了,JVM会保证线程安全。
// 问题3:枚举单例能否被反射破坏单例?
// 答:不能,反射在newInstance的时候会检查,如果是枚举类型就会抛出异常。
// 问题4:枚举单例能否被反序列化破坏单例?
// 答:不能,枚举类的实现考虑到了这一点,反序列化后依然是从前的单例。
// 问题5:枚举单例如果希望加入一些单例创建时的初始化逻辑应该如何做?
// 答:加构造方法。
enum Singleton{
    INSTANCE;
}