06 Java的内存模型以及可见性,有序性问题的解决

目录

1 Java的内存模型

Java Memory Model

文章翻译

JMM模型的组成:上图中Java内存模型定义了两个逻辑分区一个线程私用的栈线程之间共享的堆空间

  • 堆与栈中实际的物理空间则是 CPU 寄存器、缓存、硬件内存

线程中定义的变量实际存储位置

类型 位置
基本类型变量(byte,short,int,long,float,double,boolean,char) 线程栈上
对象的引用 引用在栈上,对象一般在堆上
对象中方法的基本类型变量/对象的引用 栈上
对象中引用的对象 一般在堆上
对象中的静态成员 和对象一起放在堆上
  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问
  • 两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝

JMM关联的三个重要概念

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响(cpu缓存与内存的一致性问题,对象成员与本地变量可以存在于寄存器,缓存,内存中,同个数据多个副本一致性的维护)
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

2 Java的可见性

2-1 案例分析(main线程与t线程之间)

package chapter5;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.test17")
public class Test17 {
    // volatile  static boolean run = true;  // 情况2
    static boolean run = true;               // 情况1
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(run){
            }
        },"t");
        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.warn("通过静态变量停止线程t");
        run = false;
    }
}
情况1:定义run为普通的静态变量
 static boolean run = true;

上面的代码运行时,线程无法停止

线程无法停止的原因?

  • 线程t读取的run数值是高速缓存中的数值。
    • JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
  • 主线程修改的run的数值在主内存中。
    • 主线程修改了 run 的值,并同步至主存,而 t 是从高速缓存中读取这个变量的值,结果永远是旧值

情况2:用volatile修饰run(主动声明这个变量是易变的,让线程从主存中提取)
  • 线程操作 volatile 变量都是直接操作主存,避免线程从自己的工作缓存中查找变量的值

则线程能够正常停止。

2-2 可见性的小结

可见性通俗的可以理解为变量的最新值能够被各个线程所看到

保证变量可见性的2个策略?

策略1:使用volatile(易变关键子)进行修饰

策略2:访问该变量都去获得锁

  • 使用锁实际上不仅仅保证了变量的可见性,可进一步保证变量的原子性
package chapter5;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.test17")
public class Test17 {
    static boolean run = true;
    static Object t1 = new Object();
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(true){
                synchronized (t1) {
                    if (!run)
                        break;
                }
            }
        },"t1");
        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.warn("通过静态变量停止线程t1");
//        run = false;
        synchronized (t1){
            run = false;
        }
    }
}

上面的代码中,线程不会出现无法停止的问题。

小知识:如果在线程内部使用println函数打印变量值,线程t打印的依旧是主存值,其原因依旧是由于锁的存在,可以看到Java源代码中PrintStream.java文件中println方法有锁

    /**
     * Prints a long and then terminate the line.  This method behaves as
     * though it invokes <code>{@link #print(long)}</code> and then
     * <code>{@link #println()}</code>.
     *
     * @param x  a The <code>long</code> to be printed.
     */
    public void println(long x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

3 volatile的应用

MSEI协议与volatile的关系

3-1 终止模式之二阶段终止模式

题目要求:在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会 。

错误思路

  • 使用线程对象的 stop() 方法停止线程 ,杀死的线程如果没有释放锁,那么其他线程永远无法获得锁
  • System.exit(int) 会停止整个线程。
方式1:利用interrupted的标记

实现

方式2:利用volatile修饰的公共变量
package chapter5;
import lombok.extern.slf4j.Slf4j;
public class Test18 {
    public static void main(String[] args) {
        TwoStageTermination tmp = new TwoStageTermination();
        tmp.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp.stop();

    }
}

@Slf4j(topic = "c.TwoStageTermination")
class TwoStageTermination
{
    private Thread monitor;
    private volatile boolean stop = false;

    public void stop(){       /* 正常状态停止(中断)监控进程*/
        stop = true;
        /*可以通过添加 interrupt让线程即便在睡眠状态下也能实时停止*/
       //monitor.interrupt();
    }
    public void start(){
        monitor = new Thread(() -> {
            while(true){
                Thread current = Thread.currentThread();
                if(stop){
                    log.warn("do something else before termination!");
                    log.warn("stop the thread of monitor!");
                    break;
                }
                try{
                    current.sleep(2000);
                    log.warn("sleep 2 seconds !");
                    log.warn("do something when the monitor thread is running.");
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        });
        monitor.start();
    }
}

运行结果(通过volatile修饰的静态变量可以优雅(线程能够料理后事)的停止线程):

[Thread-0] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-0] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-0] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-0] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-0] WARN c.TwoStageTermination - do something else before termination!
[Thread-0] WARN c.TwoStageTermination - stop the thread of monitor!

3-2 同步模式之balking模式

定义:balking是犹豫的意思。

应用场景:Balking 模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做
了,直接结束返回。

实例:在二阶段终止模式的代码中monitor对象调用start()方法二次,那么就会创建2个监视进程做着相同的事情,造成资源的浪费

TwoStageTermination tmp = new TwoStageTermination();
tmp.start(); 
tmp.start();                                          //新增一个start

运行结果:(可以看到线程0与线程1做的是同样的事情):

[Thread-1] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-0] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-0] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-1] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-0] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-1] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-0] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-1] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-1] WARN c.TwoStageTermination - do something else before termination!
[Thread-1] WARN c.TwoStageTermination - stop the thread of monitor!
[Thread-0] WARN c.TwoStageTermination - do something else before termination!
[Thread-0] WARN c.TwoStageTermination - stop the thread of monitor!
如何修改代码为balking模式?

模板

public class MonitorService {
    /* 标记是否有线程已经启动*/
    private volatile boolean starting;
    public void start() {
    /* 这里之所以为starting变量加上锁就是为了保证MonitorService在多线程环境下的线程安全性*/
    synchronized (this) {
        if (starting) {
        return;
    }
    starting = true;
}

具体实例

  • start添加volatile是为了保证该变量对其他线程的可见性
  • start变量加锁是为了适应多线程环境下也能至多只创建一个线程
package chapter5;
import lombok.extern.slf4j.Slf4j;
public class Test18 {
    public static void main(String[] args) {
        TwoStageTermination tmp = new TwoStageTermination();
        tmp.start();
        tmp.start();
        tmp.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp.stop();

    }
}

@Slf4j(topic = "c.TwoStageTermination")
class TwoStageTermination
{
    private Thread monitor;
    private volatile boolean stop = false;
    private volatile boolean start = false;
    public void stop(){       /* 正常状态停止(中断)监控进程*/
        stop = true;
        /*可以通过添加 interrupt让线程即便在睡眠状态下也能实时停止*/
       //monitor.interrupt();
        start = false;
    }
    public void start(){
        synchronized (this){
            log.warn("try to create new monitor thread");
            if(start)
                return;
        }
        start = true;
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {
                    log.warn("do something else before termination!");
                    log.warn("stop the thread of monitor!");
                    break;
                }
                try {
                    current.sleep(2000);
                    log.warn("sleep 2 seconds !");
                    log.warn("do something when the monitor thread is running.");
                } catch (Exception e) {
                    e.printStackTrace();
                    current.interrupt();    /* 设置中断标记为true*/
                }
            }
        });
        monitor.start();
    }
}

执行结果

[main] WARN c.TwoStageTermination - try to create new monitor thread
[main] WARN c.TwoStageTermination - try to create new monitor thread
[main] WARN c.TwoStageTermination - try to create new monitor thread
[Thread-0] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-0] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-0] WARN c.TwoStageTermination - sleep 2 seconds !
[Thread-0] WARN c.TwoStageTermination - do something when the monitor thread is running.
[Thread-0] WARN c.TwoStageTermination - do something else before termination!
[Thread-0] WARN c.TwoStageTermination - stop the thread of monitor!
balking用于线程安全的单例
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;
	}
}

4 Java的有序性问题

4-1 概述

Java的有序性体现在指令的重排序。即原本应该串行执行的指令,实际执行时为了提高执行效率进行了指令重排序

原理:CPU层面也会进行指令重排序。可以看看黑皮书《深入理解计算机系统》

  • 指令重排序在可能造成多线程环境下结果无法保持唯一性

因此有序性问题常常与指令重排序相关联。

4-2 多线程下有序性问题与volatile

通过volatile设置写屏障、
  • 写屏障保证在该屏障之前对共享变量的保存都同步到主存中。
num = 2;
volatile ready = true;    // volatile相当于屏障,保证对变量的写入都是在volatile前面

方式2通过volatile设置读屏障

  • 读屏障保证在该屏障之后,对共享变量的读取都是从主存中读取
if(ready){                // ready是volatlie变量,保证对变量的读取都是在volatile的后面。
	num = num+1;         
}                  

**总结: **volatile保证了变量的读取与写入都同步到主存中。

  • 注意volatile无法阻止整体上指令重排的发生,只是保证线程内部代码块的代码有序性
  • volatile能够解决可见性问题,但无法完全解决有序性。

volatile的使用场景明确:

  • volatile非常适合一个线程写,其他线程读的多线程环境。保证共享变量的改动对读取线程的可见性。
  • volatile也可以通过读写屏障,解决指令重排序引发的问题

4-3 double-checked locking 单例的缺点

public final class Singleton {
	private Singleton() { }
	private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
            if(INSTANCE == null) { // t2
                    // 首次访问会同步,而之后的使用没有 synchronized
                    synchronized(Singleton.class) {
                        if (INSTANCE == null) { // t1
                        INSTANCE = new Singleton();
                    	}
            		}     
        }
        return INSTANCE;
    }
}
/*
特点:
1)懒惰实例化,用到才会实例化。
2)使用synchronized来保证多线程情况下,不会重复创建实例。
3)外面一层的if保证首次访问会加锁,而之后的访问不会去申请锁,避免加锁的代价(存在问题)
*/

单例模式的5种写法:1)懒汉式 2) 饿汉式 3)双检锁 4) 静态内部类 5)枚举

  • 上面的代码存在问题,指令重排可能会造成外层的if在多线程环境下没有发挥应用的作用
为什么上面代码存在安全问题?
0: getstatic #2       // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3             // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2      // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3            // 创建对象
20: dup               // 复制一份对象引用
21: invokespecial #4 //  表示利用一个对象引用,调用构造方法
24: putstatic #2     //  表示利用一个对象引用,赋值给 static INSTANCE
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2     // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

线程t1与线程t2并发执行的情况下

线程t1由于指令重排序,可能会出现下面这样一种情况:

  • 先进行引用的赋值,然后再调用无参的构造方法初始化实例(指令重排序)。

此时线程t2也去获取线程,会获得的一个未初始化的实例

synchoroized保证代码有序性的进一步理解?

​ synchronized能够保证锁住的代码块内部的指令不会与外面的指令混合在一起。如果成员没有完全在代码内使用那么synchronized也无法保证有序性。

  • 下面代码中INSTANCE没有完全交给synchronized同步块管理,因此指令重排序会导致INSTANCE引发线程安全问题
public final class Singleton {
	private Singleton() { }
	private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
            if(INSTANCE == null) { // t2
                    // 首次访问会同步,而之后的使用没有 synchronized
                    synchronized(Singleton.class) {
                        if (INSTANCE == null) { // t1
                        INSTANCE = new Singleton();
                    	}
            		}     
        }
        return INSTANCE;
    }
}
通过volatile来避免多线程环境下指令重排序的安全问题(正确的double-checked locking)
public final class Singleton {
	private Singleton() { }
	// volatile修饰的变量会添加读写屏障,保证多线程环境下命令的有序性,避免对象初始化发生在引用赋值之前
	private static volatile Singleton INSTANCE = null;
    public static Singleton getInstance() {
            if(INSTANCE == null) { // t2
                    // 首次访问会同步,而之后的使用没有 synchronized
                    synchronized(Singleton.class) {
                        if (INSTANCE == null) { // t1
                        INSTANCE = new Singleton();
                    	}
            		}     
        }
        return INSTANCE;
    }
}

4-3 happens-before规则(重要:保证了对变量的写对其他线程可见)

为什么需要happens-before规则?

  • 这个规则保证了对共享变量的写操作对其它线程的读操作可见

个人理解:在多线程环境下,我们希望当前的线程对共享变量的写操作能够对其他线程可见,但是JMM模型从内存层面彻底解决这个问题,因此我们在写多线程代码时要遵循以下的规范从而确保线程中对共享变量的写操作对其他线程可见

规则1:同一对象加锁的同步块内变量的读写对其他线程可见。(锁保证了同步块内变量的写同步到了主存)
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();        // 主线程通过join得知t1线程结束,对于线程t1对于变量的修改,主线程可以看见
    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,y的写都是对采用默认值对象的写。
        x = 20;
    },"t1").start();
    
	new Thread(()->{
    	// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
    	System.out.println(x);
	},"t2").start();

5 习题练习

5-1 balking模式的习题复习

下面实现存在的问题

  • 没有使用synchronized对多线程之间共享的initialized进行保护,可能会造成doInit()被调用多次。
public class TestVolatile {
    volatile boolean initialized = false;
    void init() {
        if (initialized) {
            return;
        }
        doInit();
        initialized = true;
    }
    private void doInit() {
    }
}

5-2 单例模式的线程安全分析习题(良好的代码习惯需要清醒的头脑)

第一种实现分析
// 问题1:为什么加 final ?
// 答:通过final避免该对象被继承,防止子类的定义对单例模式造成破坏,或者不需要被子类去继承。
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
// 答:通过反序列化也可以创建对象,会不会产生一种可能即反序列化产生的对象不符合我们创建的单例对象,按照官方文件定义名称为readResolve()的方法返回对象可以确保反序列化不会破坏单例。
public final class Singleton implements Serializable {
	// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
	答: 让构造函数为 private,这样该类就不会被实例化,无法避免反射创建新的实例。反射可以获得类的构造器,强行使用构造函数生成实例。
	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;
	}
}
第二种实现分析

枚举类的字节码分析:

对应字节码

从字节码中可以看到以下几点

  • 实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类
  • 类的内部定义了一个static静态成员变量,变量名为INSTANCE。
  • enum常量(这里是INSTANCE)代表了一个enum的实例,enum类型只能有这些常量实例。标准保证enum常量(INSTANCE)不能被克隆,也不会因为反序列化产生不同的实例,想通过反射机制得到一个enum类型的实例也不行的。

参考1

参考2

// 问题1:枚举单例是如何限制实例个数的。
答:INSTANCE是枚举类的静态成员变量,是枚举类的一个静态实例。
// 问题2:枚举单例在创建时是否有并发问题
答:没有并发问题,静态成员变量在类加载时实现,不会通过线程自己去创建。
// 问题3:枚举单例能否被反射破坏单例
答:不能
// 问题4:枚举单例能否被反序列化破坏单例
答:枚举类默认都是实现序列化接口的,枚举在实现的时候考虑了反序列化,所以可以避免反序列化创建单例。
// 问题5:枚举单例属于懒汉式还是饿汉式
答:枚举是饿汉式,只要在类加载的时候才会创建静态成员变量。
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
答:枚举类可以通过构造方法实现初始化逻辑。
enum Singleton {
    INSTANCE;
}
单例的第三种实现
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点
// 回答:线程是安全的,但是每次每次调用getInstance()方法都要去申请锁,导致程序的性能比较低。
public static synchronized Singleton getInstance() {
    if( INSTANCE != null ){
        return INSTANCE;
    }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}
单例的第四种实现(double checked lock)(好的实现!!!)
public final class Singleton {
private Singleton() { }
     // 问题1:解释为什么要加 volatile ?
     // 答:线程t1在同步代码块内的会发生指令重排序(可能会赋值对象的引用,然后再去创建对象的实例),这样会造成外部的线程t2误判实例已经创建,从而去使用,因此需要加上volatile通过读写屏障确保对象引用的赋值在对象创建之前。
        private static volatile Singleton INSTANCE = null;
     // 问题2:对比实现3, 说出这样做的意义
     // 答:相比较实现3,第二次及以后的后续调用不会去申请锁,提供程序性能。
        public static Singleton getInstance() {
            if (INSTANCE != null) {
                return INSTANCE;
            }
            synchronized (Singleton.class) {
     // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
     // 考虑线程t1在同步代码块中创建INSTANCE的过程中,t2线程进入到锁的的entryset.
     // 线程t1执行完毕,线程t2执行时需要执行下面的条件判断避免重复创建实例。
            if (INSTANCE != null) { // t2
                return INSTANCE;
            }
            INSTANCE = new Singleton();
            return INSTANCE;
        }
	}
}
单例的第五种实现(通过静态成员变量实现,推荐!!!!)
public final class Singleton {
private Singleton() { }
	// 问题1:属于懒汉式还是饿汉式
    // 是懒汉式的,因为类加载是lazy loader的,静态实例在类加载的时候才会被使用。
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
	// 问题2:在创建时是否有并发问题
    // 不会有线程安全问题,JVM保证了类加载时的线程安全性。
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}
单例模式总结
  • 单例模式在设计的时候需要考虑指令重排序,序列化与反序列,反射机制,多线程加锁问题,以及继承问题
  • 采用double checked lock以及懒加载的单例设计是比较推荐的。

6 有序性与可见性知识点总结

  • 可见性 - 由 JVM 缓存优化引起

  • 有序性 - 由 JVM 指令重排序优化引起

  • happens-before 规则(共享变量的写入对其他线程可见的7条规则)

  • 原理方面

    • CPU 指令并行(指令重排序问题)

    • volatile(读写屏障)

  • 应用模式

    • 阶段终止模式的 volatile 改进(使用volatile修饰的全局变量取代interrupt来终止其他线程)
    • 同步模式之 balking (使用volatile修饰的共享变量去避免创建相同的功能的线程

参考资料

多线程基础课程

原文地址:https://www.cnblogs.com/kfcuj/p/14589432.html