剑指JUC原理-8.Java内存模型(下)

简介: 剑指JUC原理-8.Java内存模型

剑指JUC原理-8.Java内存模型(中):https://developer.aliyun.com/article/1413627


关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取INSTANCE 变量的值。


这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例。


对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效


这里面,前面介绍的synchronized不是太严谨 。能保证 原子 可见 有序


synchronized仍然是可以被重排序的,并不能组织重排序,volatile才能组织重排序,但是如果共享变量完全被synchronized 所保护,那么共享变量在使用的过程中是不会有 原子 可见 有序问题的,就算中间发生了重排序,但是只要完全交给synchronized管理,是不会有有序性问题的。


刚才使用出现问题,是因为共享变量并没有完全的被synchronized保护起来,synchronized外面还有共享变量的使用


double-checked locking 解决


public final class Singleton {
    private Singleton() { }
    private static volatile Singleton INSTANCE = null;
    public static Singleton getInstance() {
        // 实例没创建,才会进入内部的 synchronized代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) { // t2
                // 也许有其它线程已经创建实例,所以再判断一次
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

字节码上看不出来 volatile 指令的效果


如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:


可见性


  • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
  • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据


有序性


  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前



happens-before


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


线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见


 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();


线程对 volatile 变量的写,对接下来其它线程对该变量的读可见


volatile static int x;
new Thread(()->{
 x = 10;
},"t1").start();
new Thread(()->{
 System.out.println(x);
},"t2").start();


线程 start 前对变量的写,对该线程开始后对该变量的读可见


static int x;
x = 10;
new Thread(()->{
 System.out.println(x);
},"t2").start();


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


static int x;
Thread t1 = new Thread(()->{
 x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);


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


Thread t2 = new Thread(()->{
            while(true) {
                if(Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        },"t2");
        t2.start();
        new Thread(()->{
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            x = 10;
            t2.interrupt();
        },"t1").start();


具有传递性,如果 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();

主要是 写屏障之前都同步到主内存


balking 模式习题


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

public class TestVolatile {
    volatile boolean initialized = false;
    void init() {
        if (initialized) {
            return;
        }
        doInit();
        initialized = true;
    }
    private void doInit() {
    }
} 

其实这里,在多线程的情况下,因为没有保证多线程之间的原子性,所以会出现问题。


线程安全单例习题


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

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


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


实现1


// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
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;
    }
}

问题1:怕将来有子类,覆盖某些方法,破坏了单例


问题2:反序列化维护的对象,和单例维护的对象不一样,因为 需要加上

public Obejct readResolve(){

return INSTANCE;

}


反序列化的过程中,一旦发现了readResolve 返回的对象,就会用你返回的对象,而不是反序列化字节码生成的对象。


问题3: 设置成public,别的类能够无限的创建对象了,并不能够防止反射


问题4: 静态成员变量,初始化操作是在类加载的时候。类加载阶段是由jvm来保证这些代码的线程安全性。所以类加载阶段做成员赋值都是线程安全的。


问题5: 用方法说明提供了更好的封装性,可以内部实现懒惰的初始化。还可以对创建的时间有更多的控制。


实现2(※※※重点难点※※※)


// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
    INSTANCE;
}

问题1: 枚举单例是一种实现单例模式的方式,它通过使用枚举类型来限制实例个数为一个。在Java中,枚举类型是保证全局唯一的,因此使用枚举来实现单例可以有效地避免多线程环境下的并发问题,并且在序列化和反序列化中也可以得到保证。


问题2: 是静态成员变量,类加载阶段完成的,不会有并发问题


问题3: 枚举单例是一种实现单例模式的有效方式,因为它可以避免通过反射和序列化等方式破坏单例。枚举类型在Java中是天然的单例,只能在JVM中被实例化一次。当使用反射来访问枚举类型时,JVM会保证每个枚举类型只被实例化一次。这是因为枚举类型在Java中是特殊的类,由JVM特别处理。因此,即使使用反射来访问枚举类型并试图创建一个新的实例,JVM也会返回已经存在的单例。因此,枚举单例不能被反射破坏单例。


问题4: 枚举单例在Java中也可以防止被反序列化破坏单例。当一个枚举类型被序列化并再次反序列化时,JVM会自动确保只存在一个实例。这是因为枚举类型的序列化和反序列化是由JVM处理的,并且JVM会保证对于同一个枚举类型只有一个实例。因此,尝试通过反序列化来破坏枚举单例是无效的,反序列化操作会返回已经存在的单例实例,而不会创建新的实例。总之,枚举单例是一种安全且可靠的单例实现方式,即使在面对反射和序列化等特性时也能保持单例的完整性。


问题5: 饿汉式


问题6: 如果希望在枚举单例创建时加入一些初始化逻辑,可以在枚举中定义一个构造函数,并将初始化逻辑放在其中

public enum SingletonEnum {
    INSTANCE;
  private SingletonEnum() {
      // 这里可以加入单例创建时的初始化逻辑
      System.out.println("SingletonEnum has been initialized.");
  }
  // 其它方法
}

在上面的示例中,我们定义了一个名为SingletonEnum的枚举,其中INSTANCE是该枚举的唯一实例。同时,我们定义了一个构造函数来进行单例的初始化逻辑。


此处需要注意,在枚举类型中,构造函数必须是私有的。这是因为Java语言规范规定,只能在枚举类型内部定义枚举常量,而枚举常量的创建是由编译器自动生成的。因此,枚举类型的构造函数必须是私有的,以确保只有编译器才能调用它。


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

性能问题:


由于synchronized关键字锁住的是整个方法,在多线程高并发的情况下,可能会导致性能下降。每次调用getInstance()时都需要获取锁,即使实例已经被创建。


实例提前创建:在多线程环境下,如果有一个线程获取锁并创建了实例,其它线程进入方法后会发现实例已被创建,但仍需要等待锁的释放才能继续执行。这样就造成了实例的提前创建,占用了内存资源。


实现4:DCL


public final class Singleton {
    private Singleton() { }
    // 问题1:解释为什么要加 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;
        }
    }
}

问题1: 保障了可见性 和 有序性


可见性


  • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
  • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据


有序性


  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前


问题2: 这种方式在第一次检查时可以避免不必要的锁竞争,提高了性能。并且通过使用volatile关键字修饰实例变量,保证了线程之间对实例的可见性,确保正确的初始化。这样既满足了线程安全性,又提高了性能。


问题3: 其实是为了防止最一开始同时有多个线程 绕过了第一个 不等于 null的判断


实现5


public final class Singleton {
    private Singleton() { }
    // 问题1:属于懒汉式还是饿汉式
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    // 问题2:在创建时是否有并发问题
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

这段代码使用了静态内部类的方式实现了单例模式,常被称为“静态内部类单例模式”。


属于懒汉式还是饿汉式?

该实现方式属于懒汉式,因为实例对象的创建发生在静态内部类LazyHolder被调用时。而不是在类加载时就创建实例对象。


在创建时是否有并发问题?

由于静态内部类LazyHolder在类加载时并不会被初始化,只有当getInstance()方法被调用时才会加载,因此不存在并发创建实例的问题。


同时,由于Java虚拟机在加载类时会对类进行加锁,所以多线程同时加载Singleton类也不会导致并发问题。


这种实现方式同时具备了懒汉式和饿汉式的优点。在需要使用实例对象时才会进行实例化,避免了饿汉式可能造成的资源浪费;同时在加载静态内部类时,JVM会自动加锁,保证了线程安全性。因此,该实现方式既满足了线程安全性,又提高了性能和资源利用率。


目录
相关文章
|
2天前
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
6天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
1天前
|
存储 Java
JAVA并发编程AQS原理剖析
很多小朋友面试时候,面试官考察并发编程部分,都会被问:说一下AQS原理。面对并发编程基础和面试经验,专栏采用通俗简洁无废话无八股文方式,已陆续梳理分享了《一文看懂全部锁机制》、《JUC包之CAS原理》、《volatile核心原理》、《synchronized全能王的原理》,希望可以帮到大家巩固相关核心技术原理。今天我们聊聊AQS....
|
5天前
|
监控 算法 Java
Java中的内存管理:理解垃圾回收机制的深度剖析
在Java编程语言中,内存管理是一个核心概念。本文将深入探讨Java的垃圾回收(GC)机制,解析其工作原理、重要性以及优化方法。通过本文,您不仅会了解到基础的GC知识,还将掌握如何在实际开发中高效利用这一机制。
|
5天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理策略和垃圾回收机制。首先介绍了Java内存模型的基本概念,包括堆、栈以及方法区的划分和各自的功能。进一步详细阐述了垃圾回收的基本原理、常见算法(如标记-清除、复制、标记-整理等),以及如何通过JVM参数调优垃圾回收器的性能。此外,还讨论了Java 9引入的接口变化对垃圾回收的影响,以及如何通过Shenandoah等现代垃圾回收器提升应用性能。最后,提供了一些编写高效Java代码的实践建议,帮助开发者更好地理解和管理Java应用的内存使用。
|
7天前
|
Java 开发者 数据格式
【Java笔记+踩坑】SpringBoot基础4——原理篇
bean的8种加载方式,自动配置原理、自定义starter开发、SpringBoot程序启动流程解析
【Java笔记+踩坑】SpringBoot基础4——原理篇
|
1天前
|
监控 算法 Java
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,特别是垃圾回收(Garbage Collection, GC)机制。我们将从基础概念开始,逐步解析垃圾回收的工作原理、不同类型的垃圾回收器以及它们在实际项目中的应用。通过实际案例,读者将能更好地理解Java应用的性能调优技巧及最佳实践。
9 0
|
1天前
|
Java
JAVA并发编程ReentrantLock核心原理剖析
本文介绍了Java并发编程中ReentrantLock的重要性和优势,详细解析了其原理及源码实现。ReentrantLock作为一种可重入锁,弥补了synchronized的不足,如支持公平锁与非公平锁、响应中断等。文章通过源码分析,展示了ReentrantLock如何基于AQS实现公平锁和非公平锁,并解释了两者的具体实现过程。
|
1月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
|
2月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
189 14