《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



相关文章
|
6天前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
2月前
|
Java 程序员 调度
【JAVA 并发秘籍】进程、线程、协程:揭秘并发编程的终极武器!
【8月更文挑战第25天】本文以问答形式深入探讨了并发编程中的核心概念——进程、线程与协程,并详细介绍了它们在Java中的应用。文章不仅解释了每个概念的基本原理及其差异,还提供了实用的示例代码,帮助读者理解如何在Java环境中实现这些并发机制。无论你是希望提高编程技能的专业开发者,还是准备技术面试的求职者,都能从本文获得有价值的见解。
48 1
|
18天前
|
Java 开发者
深入探索Java中的并发编程
本文将带你领略Java并发编程的奥秘,揭示其背后的原理与实践。通过深入浅出的解释和实例,我们将探讨Java内存模型、线程间通信以及常见并发工具的使用方法。无论是初学者还是有一定经验的开发者,都能从中获得启发和实用的技巧。让我们一起开启这场并发编程的奇妙之旅吧!
|
19天前
|
算法 安全 Java
Java中的并发编程是如何实现的?
Java中的并发编程是通过多线程机制实现的。Java提供了多种工具和框架来支持并发编程。
15 1
|
1月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
1月前
|
安全 Java 测试技术
掌握Java的并发编程:解锁高效代码的秘密
在Java的世界里,并发编程就像是一场精妙的舞蹈,需要精准的步伐和和谐的节奏。本文将带你走进Java并发的世界,从基础概念到高级技巧,一步步揭示如何编写高效、稳定的并发代码。让我们一起探索线程池的奥秘、同步机制的智慧,以及避免常见陷阱的策略。
|
2月前
|
安全 Java 编译器
深入Java内存模型:解锁并发编程的秘密
【8月更文挑战第24天】在Java的世界,内存模型是支撑并发编程的基石。本文将深入浅出地探讨Java内存模型(JMM)的核心概念、工作原理及其对高效并发策略的影响。我们将通过实际代码示例,揭示如何利用JMM来设计高性能的并发应用,并避免常见的并发陷阱。无论你是Java新手还是资深开发者,这篇文章都将为你打开并发编程的新视角。
35 2
|
2月前
|
C# 开发者 数据处理
WPF开发者必备秘籍:深度解析数据网格最佳实践,轻松玩转数据展示与编辑大揭秘!
【8月更文挑战第31天】数据网格控件是WPF应用程序中展示和编辑数据的关键组件,提供排序、筛选等功能,显著提升用户体验。本文探讨WPF中数据网格的最佳实践,通过DevExpress DataGrid示例介绍其集成方法,包括添加引用、定义数据模型及XAML配置。通过遵循数据绑定、性能优化、自定义列等最佳实践,可大幅提升数据处理效率和用户体验。
55 0
|
2月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
35 0
|
2月前
|
安全 Java 开发者
Java中的并发编程:从基础到高级
本文将深入浅出地介绍Java并发编程的核心概念,包括线程安全、同步机制、锁和线程池等。我们将从简单的多线程示例出发,逐步深入到高级并发工具类的应用,最后探讨性能优化技巧。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的知识和实践建议。
28 0