happens-before规则

基本介绍

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

  1. 线程t1中对x=10 对线程t2读取 x 可见
 static int x = 0;
    static Object m = new Object();

    public static void rule01(){

        Thread t1 = new Thread(() -> {
            synchronized (m){
                x = 10;
            }
        }, "t1");


        Thread t2 = new Thread(() -> {
            synchronized (m){
                System.out.println("x = " + x);
            }
        }, "t2");

        t1.start();
        t2.start();
    }
  1. 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
      volatile static int x1;

    public static void rule02(){

        Thread t1 = new Thread(() -> {
            synchronized (m){
                x1 = 10;
            }
        }, "t1");


        Thread t2 = new Thread(() -> {
            synchronized (m){
                System.out.println("x1 = " + x1);
            }
        }, "t2");

        t1.start();
        t2.start();
    }

  1. 线程 start 前对变量的写,对该线程开始后对该变量的读可见
public static void rule03(){

        int x;
        x = 10;

        Thread t1 = new Thread(() -> {
            synchronized (m){
                System.out.println(x);
            }
        }, "t1");

        t1.start();
    }
  1. 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive()t1.join()等待

    它结束)

    static int x2;
    public static void rule04(){

        Thread t1 = new Thread(() -> {
            synchronized (m){
                x2 = 10;
            }
        }, "t1");

        t1.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("x = " + x2);


    }
  1. 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过

    t2.interruptedt2.isInterrupted

static int x3;

    public static void rule05(){

        // 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过
        // t2.interrupted 或 t2.isInterrupted)

        Thread t2 = new Thread(() -> {

            while(true) {
                // 判断是否设置打断标记
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(x3);
                    break;
                }
            }

        }, "t2");
        t2.start();

        Thread t1 = new Thread(() -> {
            // 睡眠1s
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            // t2线程打断前对x3赋值
            x3 = 10;
            t2.interrupt();
        });

        t1.start();

        while (!t2.isInterrupted()) {
            // 方法作用: 让当前线程从运行状态 转为 就绪状态,以允许具有相同优先级的其他线程获得运行机会
            Thread.yield();
        }

        System.out.println(x3);

    }
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
	static int y;

    public static void rule06(){

        Thread t1 = new Thread(() -> {
            x = 20;
            y = 10;
        } , "t1");

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

        t1.start();
        t2.start();

    }

变量都是指成员变量或静态成员变量

balking 模式习题

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

package cn.knightzz.question;

import lombok.extern.slf4j.Slf4j;

/**
 * @author 王天赐
 * @title: TestVolatile
 * @projectName hm-juc-codes
 * @description:
 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
 * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
 * @create: 2022-08-05 14:34
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestVolatile")
public class TestVolatile {

    volatile boolean initialized = false;
    void init() {
        if (initialized) {
            log.debug("已初始化过 , exit() ");
            return;
        }
        doInit();
        initialized = true;
    }
    private void doInit() {
        log.debug("doInit ... ");
    }

    public static void main(String[] args) {

        TestVolatile testVolatile = new TestVolatile();

        for (int i = 0; i < 100; i++) {
            new Thread(() ->{

                for (int j = 0; j < 100; j++) {
                    testVolatile.init();
                }
            } , "t" + i).start();
        }

    }
}

上面的代码有问题, 假如在 t1线程已经执行过初始化, 但是还未给 initialized 赋值, 此时上下文切换,

t2线程 initialized 为false , 会重复调用 doInit()

线程安全单例习题-实现1

单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用

getInstance)时的线程安全,并思考注释中的问题

package cn.knightzz.question;


import java.io.Serializable;

/**
 * @author 王天赐
 * @title: Singleton
 * @projectName hm-juc-codes
 * @description: 单例模式
 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
 * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
 * @create: 2022-08-05 20:31
 */
public final class Singleton implements Serializable {

    public Singleton() {
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    /**
     * 反序列化时, 直接调用 readResolve 方法去实例化
     * @return Singleton
     */
    public Object readResolve(){
        return INSTANCE;
    }
}

问题1:为什么加 final

因为 Singleton 类是单例模式, 添加 final 目的是为了防止子类去重写某些方法, 导致类无法单例化

问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例

反序列化可以通过 constructor.setAccessible(true) 屏蔽 private 的检查来创建多个对象

我们可以通过添加 readResolve 方法, 让反序列化的时候直接获取INSTANCE

 public Object readResolve(){
        return INSTANCE;
  }

问题4:这样初始化是否能保证单例对象创建时的线程安全?

 private static final Singleton INSTANCE = new Singleton();

可以, 上面的代码灭有线程安全问题, 因为静态变量是在类加载的时候创建的, JVM保证静态对象的线程安全

问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由

  • 使用方法可以实现懒惰的初始化方式
  • 提供泛型的支持

线程安全单例习题-实现2

enum Singleton { 
 	INSTANCE; 
}

问题1:枚举单例是如何限制实例个数的

public final static enum Lcn/knightzz/other/Singleton; INSTANCE

枚举实例实际上是类的一个静态实例, 所以是单例的

问题2:枚举单例在创建时是否有并发问题

public final static enum Lcn/knightzz/other/Singleton; INSTANCE

因为枚举单例是静态的成员变量, 在类加载的时候就创建好了, 所以不存在并发问题

问题3:枚举单例能否被反射破坏单例

不能用反射破坏单例

问题4:枚举单例能否被反序列化破坏单例

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

所有的枚举类都继承 Enum 抽象了, 并且在自带防止反序列化破坏单例

问题5:枚举单例属于懒汉式还是饿汉式

饿汉式

问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做

增加一个构造方法即可

package cn.knightzz.other;

@SuppressWarnings("all")
public enum Singleton {
    /**
     * 单例
     */
    INSTANCE;

    void Singleton(){
        // 初始化逻辑
    }
}

线程安全单例习题-实现3

  1. 分析这里的线程安全, 并说明有什么缺点?
package cn.knightzz.question;

/**
 * @author 王天赐
 * @title: Singleton01
 * @projectName hm-juc-codes
 * @description:
 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
 * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
 * @create: 2022-08-06 10:00
 */
public final class Singleton01 {
    private Singleton01() { }
    private static Singleton01 INSTANCE = null;
    // 分析这里的线程安全, 并说明有什么缺点
    public static synchronized Singleton01 getInstance() {
        if( INSTANCE != null ){
            return INSTANCE;
        }
        INSTANCE = new Singleton01();
        return INSTANCE;
    }
}

getInstance 是线程安全的, 但是存在一个问题 :

synchronized 不可以直接锁 INSTANCE , 因为 INSTANCE 需要先实例化才可以 , 并且有可能是 null

另外的就是 第一次创建的是偶加锁可以保证线程安全, 但是, 第二次, 第三次加锁就会导致效率比较低, 而此时``INSTANCE已经不为null` , 加锁只会降低效率

线程安全单例习题-实现4

package cn.knightzz.question.q4;

/**
 * @author 王天赐
 * @title: Singleton02
 * @projectName hm-juc-codes
 * @description: 单例实现
 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
 * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
 * @create: 2022-08-06 10:18
 */
@SuppressWarnings("all")
public class Singleton {

    private Singleton() { }
    // 问题1:解释为什么要加 volatile ?
    // 防止  INSTANCE = new Singleton(); 指令重排
    private static volatile Singleton INSTANCE = null;

    // 问题2:对比实现3, 说出这样做的意义
    // 1. 提高并发
    public static Singleton getInstance() {
        // 在INSTANCE创建以后, 再次调用的时候是不会出现并发问题的
        if (INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (Singleton.class) {
            // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
            // 1.为了防止首次创建 Signleton 的时候出现并发问题
            if (INSTANCE != null) { // t2
                return INSTANCE;
            }
            INSTANCE = new Singleton();
            return INSTANCE;
        }
    }

}

问题1:解释为什么要加 volatile ?

防止 INSTANCE = new Singleton(); 指令重排 , 获取对象引用先执行, 调用构造函数后执行

问题2:对比实现3, 说出这样做的意义?

  • 提高并发度, 因为实现3的代码只有在第一次创建实例的时候会出现并发安全问题
  • INSTANCE创建以后, 再次调用的时候是不会出现并发问题的

线程安全单例习题-实现5

package cn.knightzz.question.q5;

/**
 * @author 王天赐
 * @title: Singleton
 * @projectName hm-juc-codes
 * @description:
 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
 * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
 * @create: 2022-08-06 11:05
 */
@SuppressWarnings("all")
public class Singleton {
    private Singleton() {
    }

    // 问题1:属于懒汉式还是饿汉式
    // 懒汉式 : 用的时候再创建
    // 饿汉式 : 饿汉式是先创建好
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }

    // 问题2:在创建时是否有并发问题
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

问题1:属于懒汉式还是饿汉式

属于懒汉式 , 因为类加载本身是懒汉式的, 当第一次使用到 LazyHolder 时, 才会进行类加载, 对里面的静态变量进行初始化

问题2:在创建时是否有并发问题

不会有并发问题, 类加载时对静态变量赋值时, 是由JVM进行赋值, 可以保证不会出现并发问题, 并且这种方式是比较推荐的实现单例的方式