Java并发编程(二)线程安全的单例模式

一、双重检查锁定(Double-Checked Locking)

这种设计模式的目的是为了减少在多线程环境下获取锁的开销,尤其是当实例化对象的操作很昂贵,且该对象只会被实例化一次时。双重检查锁定模式的基本思想是,在创建对象之前进行两次检查:首先,无锁地检查实例是否已经创建;如果未创建,才进行加锁,再次检查实例是否已创建,如果仍未创建,则在同步块内实例化对象。

然而,在早期的Java内存模型(JMM)中,这段看似高效的代码存在一些问题,主要体现在以下几点:

1. 指令重排序 :编译器或处理器为了优化性能,可能会对代码执行顺序进行重新排序。在双重检查锁定模式中,即使实例化的代码在同步块内,对象的字段初始化和对象引用的赋值操作仍可能被重排序。这意味着其他线程有可能在对象完全构造完成前就看到对象引用,从而访问到一个部分构造的对象,导致不可预料的行为。
2. 可见性问题 :没有使用volatile关键字修饰实例变量时,新创建的对象实例可能不会立即对其他线程可见,导致其他线程看到的是旧的值(即null),即使对象已经被正确初始化。

为了解决这些问题,通常采取以下措施:

3. 使用volatile关键字 :确保对实例变量的写操作之前的所有操作都先行发生,且该写操作之后的所有读操作都能看到这个写的结果。这解决了指令重排序和可见性问题。

修正后的双重检查锁定模式示例代码如下:

java 复制代码
/**
 * Singleton类的实例化方法。保证线程安全的单例模式实现。
 * 采用双重检查锁定的方式,避免多次实例化,保证线程安全。
 * 
 * @return 返回Singleton类的唯一实例。
 */
public static Singleton getInstance() {
  // 如果实例尚未创建,则进入同步块
  if(instance ==null){
    synchronized(Singleton.class){
      // 在同步块内部再次检查实例是否存在,避免不必要的锁定
      if(instance == null){
        instance = new Singleton();
      }
    }
  }
  return instance;
}

这段代码中包含了一个静态的、volatile修饰的Singleton实例 ,这是为了确保在多线程环境下,实例的创建是线程安全 的。使用双重检查锁定的方式,在实例未被创建时对访问进行同步,以确保只有一个线程能够创建实例。这种方式既避免了线程安全问题,又尽可能地降低了同步的开销。

尽管使用volatile和双重检查锁定可以有效解决上述问题,但在现代Java实践中,推荐使用基于类加载机制的静态内部类方式来实现单例模式,因为这种方式既简洁又避免了双重检查锁定可能带来的复杂性。

二、 基于类加载机制的静态内部类单例模式

基于类加载机制的静态内部类单例模式是一种非常简洁且线程安全的方式,它利用了Java类加载机制来确保单例的唯一性。以下是该模式的示例代码:

java 复制代码
/**
 * 一个使用静态内部类实现的单例类。这种实现方式保证了线程安全,并且在类加载时才初始化单例,
 * 实现了延迟初始化(lazy initialization)。
 */
public class Singleton {

  /**
   * 私有构造函数,防止外部直接实例化Singleton类。
   */
  private Singleton() {}

  /**
   * 静态内部类SingletonHolder,用于持有Singleton的唯一实例。这种方式确保Singleton实例在需要时才被创建,
   * 且由于类加载机制,其创建过程是线程安全的。
   */
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }

  /**
   * 获取Singleton类的唯一实例。调用此方法将返回Singleton的实例,首次调用时实例才会被创建。
   *
   * @return Singleton类的唯一实例。
   */
  public static Singleton getInstance() {
    // 直接返回SingletonHolder类中持有的Singleton实例,实现延迟初始化和线程安全
    return SingletonHolder.INSTANCE;
  }
}

在这个例子中,Singleton 类包含一个私有的静态内部类 SingletonHolder。SingletonHolder 类中有一个静态常量 INSTANCE,用于持有 Singleton 的单例实例。由于静态内部类只有在外部类首次被加载时才会被加载,而类加载是线程安全的,因此 Singleton 的实例化过程也是线程安全的。

当调用 Singleton.getInstance() 方法时,会直接返回 SingletonHolder.INSTANCE,而不需要进行任何同步操作。这样既保证了单例的线程安全性,又避免了使用 synchronized 或 ReentrantLock 带来的额外开销。

这种方式不仅保证了线程安全性,而且通过延迟初始化(lazy initialization)提高了效率,因为Singleton实例只有在第一次调用getInstance()时才会被创建 。同时,由于静态内部类的特性,它的加载是在第一次被引用时才会发生 ,因此避免了类加载时就初始化单例的问题。这种实现方式的优点在于:
线程安全 :由于类加载过程是线程安全的,所以无需担心并发问题。
延迟初始化 :单例实例只在 getInstance 被第一次调用时创建,实现了延迟初始化。
代码简洁 :没有使用锁或其他同步机制,代码更简洁易懂。

请注意,这种方式适用于不需要在类加载时就初始化单例的场景。如果单例需要在类加载时就初始化,那么应选择其他实现方式,例如枚举单例。

三、枚举单例

枚举单例是Java中实现单例模式的一种优雅且线程安全的方式。这种方式利用了Java枚举的特性,使得单例在类加载时自动初始化,而且无法通过反射或其他方式破坏其唯一性。以下是一个简单的枚举单例模式的示例:

java 复制代码
public enum Singleton {
    INSTANCE;

    // 可以添加方法和其他属性
    private String someProperty = "Some value";

    public String getSomeProperty() {
        return someProperty;
    }

    public void setSomeProperty(String value) {
        this.someProperty = value;
    }
}

在这个例子中,Singleton 是一个枚举类型,只有一个元素 INSTANCE。这个元素就是单例的实例。由于枚举在Java中是唯一的,不能被实例化多次,因此保证了单例的唯一性 。同时,枚举的初始化是在类加载时完成的,因此是线程安全的

要使用这个单例,只需调用 Singleton.INSTANCE,就像访问枚举常量一样:

java 复制代码
Singleton singleton = Singleton.INSTANCE;
System.out.println(singleton.getSomeProperty());

枚举单例的优点包括:
线程安全 :Java保证了枚举实例的创建是线程安全的。
懒加载 :尽管枚举在类加载时初始化,但由于枚举的静态特性,实际上只在第一次访问时才会触发枚举实例的创建。
防止反射攻击 :枚举实例不能通过反射创建,增强了单例的安全性。

简单明了:代码简洁,易于理解和维护。

这种实现方式是Joshua Bloch在《Effective Java》一书中推荐的单例实现方法。

相关推荐
Mcworld8578 分钟前
整数分解JAVA
java·开发语言
请你喝好果汁64111 分钟前
python_竞态条件
开发语言·python
正在走向自律13 分钟前
Python 数据分析与可视化:开启数据洞察之旅(5/10)
开发语言·人工智能·python·数据挖掘·数据分析
小南家的青蛙34 分钟前
LeetCode面试题 01.09 字符串轮转
java·leetcode
dudly36 分钟前
Python 字典键 “三变一” 之谜
开发语言·python
秋野酱1 小时前
基于javaweb的SpringBoot爱游旅行平台设计和实现(源码+文档+部署讲解)
java·spring boot·后端
饕餮争锋1 小时前
org.slf4j.MDC介绍-笔记
java·开发语言·笔记
shane-u1 小时前
Maven私服搭建与登录全攻略
java·maven
半部论语1 小时前
jdk多版本切换,通过 maven 指定编译jdk版本不生效,解决思路
java·开发语言·maven·intellij-idea
阿沁QWQ1 小时前
单例模式的两种设计
开发语言·c++·单例模式