自学内容网 自学内容网

happens-before

一、简介

        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 打断 t2interrupt)前对变量的写,对于其他线程得知 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、对变量默认值(0falsenull)的写,对其它线程对该变量的读可见,并且具有传递性,如果 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) { // t2
                return 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;
    }
}

原文地址:https://blog.csdn.net/xhf852963/article/details/140515929

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!