《JUC并发编程 - 高级篇》04 -共享模型之内存 (Java内存模型 | 可见性 | 有序性 )(下)

简介: 《JUC并发编程 - 高级篇》04 -共享模型之内存 (Java内存模型 | 可见性 | 有序性 )

5.4 习题

5.4.1 balking 模式习题

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

public class TestVolatile {
    boolean initialized = false;
    public void init() {
        synchronized(this){
            if (initialized) {//t2
              return;
           }
            doInit();//t1
            initialized = true;
        }   
    }
    private void doInit() {
    }
}

有问题,对initialized的读和写 多处出现。当t1在执行doInit()方法时,此时t2在第四行判断通过后也会执行doInit()方法,最终会导致doInit()被执行两次。

**解决办法:**参考同步模式之Balking

952e2914f5e9471cc9619e8d293e9613.png

volitile适用于一个线程写多个线程读的情况,也可用于double-checked中synchronized外指令重排序问题


5.4.2 线程安全单例习题


单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用getInstance)时的线程安全,并思考注释中的问题


  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

实现1:饿汉式

/**
 * 问题1:为什么加 final?
 *       为了防止子类中的一些方法覆盖父类的提供单例的方法,从而破坏父类的单例
 * 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
 *       实现了序列化,就可以采用网络流的形式传输Java对象,最终可以通过反序列化创建对象。这个对象和单例模式创建的对象是不同的对象
 *       ,也就是破坏了单例。
 *       解决办法:加一个public Object readResolve(){...}
 *       原因:在通过反序列化创建对象的过程中,如果发现readResolve()有返回值,则直接使用该方法,不再用反序列化的字节码来创建对象。
 *
 * 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
 *       如果为共有的话,也就可以直接new()无限的创建对象,也就不是单例了。
 *       不能,暴力反射依旧可以创建实例对象
 *
 * 问题4:这样初始化是否能保证单例对象创建时的线程安全?
 *       静态成员变量的赋值操作是在类加载阶段完成的,所以可以保证
 *
 *
 *  问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
 *        ① 使用方法,可以有更好的封装性,也可以改进成懒惰初始化。
 *        ② 创建单例对象时可以有更多的控制
 *        ③ 可以对泛型进行支持
 *
 */
public final class Singleton implements Serializable {
    // 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
    private Singleton() {}
    // 问题4:这样初始化是否能保证单例对象创建时的线程安全?
    private static final Singleton INSTANCE = new Singleton();
    // 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
    public static Singleton getInstance() {
        return INSTANCE;
    }
    //解决反序列化破坏单例
    public Object readResolve(){
        return INSTANCE;
    }
}

实现2:枚举类实现单例

/**
 * 问题1:枚举单例是如何限制实例个数的
 *      通过观察源码,可以发现枚举本质是一个静态成员变量,然后通过static{}
 *      进行初始化工作
 * 问题2:枚举单例在创建时是否有并发问题
 *       静态成员变量的赋值操作是在类加载阶段完成的,所以可以保证
 * 问题3:枚举单例能否被反射破坏单例
 *        不能,枚举类型不能通过newInstance反射。
 * 问题4:枚举单例能否被反序列化破坏单例
 *        不能,因为ENUM父类中的反序列化是通过valueOf实现的 不是通过反射
 * 问题5:枚举单例属于懒汉式还是饿汉式
 *        饿汉式
 * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
 *        可参考:https://blog.csdn.net/qq_39714944/article/details/91973832
 * 注意:关于为啥强烈建议使用枚举类来实现单例模式的原因可参考:
 *  https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==
 *  &mid=2650121482&idx=1&sn=e5b86797244d8879bbe9a69fb72641b5
 *  &chksm=f36bb82bc41c313d739f485383d3a868a79020c995ee86daef
 *  026a589f4782916c42a8d3f6c7&mpshare=1&scene=1&srcid=0614J9
 *  OX5zkoAnHiPYX2sHiH#rd
 */
//无参构造的枚举类
enum Singleton {
    INSTANCE;
}
//有参构造的枚举类
enum Singleton {
    INSTANCE(0,"INSTANCE");
    int key;
    String value;
    //构造成员方法默认都是private,不能通过new的方式创建对象
    Singleton(int key, String value) {
        this.key = key;
        this.value = value;
    }
    public int getKey() {
        return key;
    }
    public void setKey(int key) {
        this.key = key;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
}

实现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;
    }
}

实现4:DCL ,本质就是对懒汉式的改进

  • 缩小了synchronized的范围
  • 但也带来了其他问题,比如指令可能重排序,可能重复创建对象。可通过双层判断 + volatile 解决
public final class Singleton {
    private Singleton() { }
    // 问题1:解释为什么要加 volatile ?
    // 为了防止指令的重排序。INSTANCE = new Singleton(); 对t1当发生重排序时候可能先进行
    // 赋值操作,此时还没有进行实例化,这时候t2进来后 外部if (INSTANCE != null) 不为空,直接返回了INSTANCE,但是这个是还未初始化的实例
    // 存在一定的问题。
    //加上volatile后,就不会出现指令重排序了。要么当t2进行 外部if (INSTANCE != null) 时,已经初始化赋值完毕。要么t2进行if (INSTANCE != null)
    //时,此时INSTANCE为null,直接继续向下运行... 就不会出现问题了。
    private static volatile Singleton INSTANCE = null;
    // 问题2:对比实现3, 说出这样做的意义
    //缩小了锁的范围,第一次需要进入synchronized代码块,之后就不需要进入synchronized同步了。
    public static Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (Singleton.class) {
            // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
            //为了防止,第一次并发访问时,对象不要重复创建。比如t1正在 new 创建对象,此时t2在synchronized外等待,当t1
            //t1创建完成并走出synchronized时,t2也会进入synchronized块,所以需要再次进行判断一下,防止重复创建。
            if (INSTANCE != null) { // t2
                return INSTANCE;
            }
            INSTANCE = new Singleton();
            return INSTANCE;
        }
    }
}

实现5:内部类的方式

public final class Singleton {
    private Singleton() { }
    // 问题1:属于懒汉式还是饿汉式
    // 类加载本身就是懒惰的,第一次使用时才进行加载。
    // 如果仅仅创建是使用外部的Singleton,没有使用getInstance(),
    // 就不会触发LazyHolder的类加载,也就不会进行静态变量的初始化操作。
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    // 问题2:在创建时是否有并发问题
    // 无 静态成员变量在类加载时进行初始化操作
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

本章小结


本章重点讲解了 JMM 中的


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

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

happens-before 规则

原理方面

CPU 指令并行

volatile

模式方面

两阶段终止模式的 volatile 改进

同步模式之 balking



相关文章
|
5天前
|
存储 安全 Java
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
37 13
|
1天前
|
Java 调度 开发者
Java中的并发编程:从基础到高级
【7月更文挑战第14天】在Java的世界中,并发编程是提升应用性能和响应能力的关键。本文将带领读者从线程的基础概念出发,深入探讨Java内存模型,逐步过渡到高级并发工具类如Executors框架和并发集合,最后通过案例分析展示如何在实际开发中运用这些知识解决并发问题。文章旨在为初学者提供清晰的学习路径,同时为有经验的开发者提供深度参考。
11 4
|
3天前
|
NoSQL Java 应用服务中间件
Java高级面试题
Java高级面试题
|
5天前
|
Java 程序员 编译器
Java面试题:解释Java内存模型(JMM)是什么,它为何重要?
Java面试题:解释Java内存模型(JMM)是什么,它为何重要?
20 2
|
5天前
|
安全 Java
Java面试题:解释synchronized关键字在Java内存模型中的语义
Java面试题:解释synchronized关键字在Java内存模型中的语义
9 1
|
3天前
|
存储 算法 Java
JAVA内存模型与JVM内存模型的区别
JAVA内存模型与JVM内存模型的区别
|
5天前
|
存储 缓存 安全
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
10 0
|
5天前
|
存储 Java 程序员
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
26 10
|
23小时前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
23 14
|
21天前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
24 2