《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



相关文章
|
1天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
8 1
|
1天前
|
Java 编译器 开发者
Java并发编程中的锁优化策略
【5月更文挑战第13天】在Java并发编程中,锁是一种重要的同步机制,用于保证多线程环境下数据的一致性。然而,不当的使用锁可能会导致性能下降,甚至产生死锁等问题。本文将介绍Java中锁的优化策略,包括锁粗化、锁消除、锁降级等,帮助开发者提高程序的性能。
|
1天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
3天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
3 0
|
3天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
3天前
|
缓存 Java 数据库
Java并发编程学习11-任务执行演示
【5月更文挑战第4天】本篇将结合任务执行和 Executor 框架的基础知识,演示一些不同版本的任务执行Demo,并且每个版本都实现了不同程度的并发性。
24 4
Java并发编程学习11-任务执行演示
|
4天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
14 0
|
5天前
|
缓存 Java 数据库
Java并发编程中的锁优化策略
【5月更文挑战第9天】 在高负载的多线程应用中,Java并发编程的高效性至关重要。本文将探讨几种常见的锁优化技术,旨在提高Java应用程序在并发环境下的性能。我们将从基本的synchronized关键字开始,逐步深入到更高效的Lock接口实现,以及Java 6引入的java.util.concurrent包中的高级工具类。文中还会介绍读写锁(ReadWriteLock)的概念和实现原理,并通过对比分析各自的优势和适用场景,为开发者提供实用的锁优化策略。
6 0
|
5天前
|
算法 安全 Java
深入探索Java中的并发编程:CAS机制的原理与应用
总之,CAS机制是一种用于并发编程的原子操作,它通过比较内存中的值和预期值来实现多线程下的数据同步和互斥,从而提供了高效的并发控制。它在Java中被广泛应用于实现线程安全的数据结构和算法。
19 0
|
5天前
|
存储 安全 算法
掌握Java并发编程:Lock、Condition与并发集合
掌握Java并发编程:Lock、Condition与并发集合
11 0