返回

Java 单例模式详解:私有构造函数与线程安全

java

单例模式:私有构造函数与实例化问题

私有构造器的意义与作用

单例模式是一种常用的设计模式,其核心在于确保一个类只有一个实例,并提供一个全局访问点。为了实现这个目标,最常用的手段就是将类的构造函数设为 private。这会阻止外部类直接使用 new 创建实例,强制开发者通过单例类提供的静态方法获取唯一实例。私有构造函数在这里起到了至关重要的作用,它直接控制了实例的创建,防止了多个实例的产生。但单单设置私有构造函数是不够的,还需要一个方法来返回这个实例。

私有成员的访问权限

许多刚接触Java的开发者容易对 private 关键字的作用范围产生困惑。 简而言之, private 修饰符限制了类成员(变量或方法)在类外部被访问的能力。 在一个类内部,任何方法都可以访问类的任何成员,即使它们被声明为 private 。例如,一个类的方法可以访问同一类中私有的成员变量。这个概念非常重要,决定了单例模式可以成功地工作,并且能够控制和管理类的实例。

在提供的代码片段中,尽管 accountNum 被声明为 private,但 main() 方法是 Account1 类的一部分,可以合法访问其内部的 private 变量。因此直接使用 myA1.accountNum 输出该变量值并没有违反 private 规则。private 的限制仅仅是对类外部(其它类)的访问生效,而不是类本身内部的代码。这表示 private 是为了维护封装性,阻止类外的不安全直接访问。

单例类的实现与初始化

为了保证单例,需要一种机制来存储和返回类的唯一实例。 通常的做法是:

  1. 声明一个静态的私有成员变量,用于保存唯一实例。
  2. 提供一个静态的公共方法(如 getInstance()),作为访问实例的唯一入口。
  3. 在首次调用 getInstance() 方法时,创建唯一的实例(或在类加载时静态初始化),并将该实例赋值给静态变量。
  4. 构造函数保持私有,阻止外部类创建新实例。

以下是一个基本的单例类实现示例:

public class Singleton {

    private static Singleton instance; //静态私有成员,保存唯一实例

    private Singleton() {  //私有构造函数,阻止外部创建实例
    }

    public static Singleton getInstance() { //静态公共方法,返回唯一实例
        if (instance == null) {   //懒汉模式:首次访问才创建
            instance = new Singleton();
        }
        return instance;
    }

    public void someMethod() {
      System.out.println("Singleton Method invoked.");
    }
}

这段代码实现了单例模式的基本架构。instance 变量存储唯一实例,构造函数设为私有,getInstance 方法作为获取实例的入口。初次调用 getInstance 会初始化 instance , 之后的调用则直接返回已初始化的实例,确保了只有一个实例存在。

要使用该单例类,可以通过以下方式进行:

public class Main {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
       
       // 可以看到这两个对象引用同一个实例
        System.out.println(singleton1 == singleton2); //输出: true

        singleton1.someMethod(); // 调用单例对象的方法
    }
}

多线程下的单例模式:线程安全问题

上述 Singleton 实现方式在单线程环境下工作良好。 但是在多线程环境下可能出现线程安全问题,多个线程可能同时通过 instance == null 判断,导致多个实例被创建。 要解决这个问题,可以考虑几种方案。

方案1:使用 synchronized 关键字

public class SynchronizedSingleton {
    private static SynchronizedSingleton instance;

    private SynchronizedSingleton() {}

    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }

}

getInstance 方法添加 synchronized 关键字确保同时只有一个线程能进入这个方法,从而保证实例只会被创建一次。虽然这种方式可以解决线程安全问题, 但会导致性能损失,每次调用 getInstance() 都会等待锁。

方案2:双重校验锁 (Double-Checked Locking, DCL)
为了提高性能, 可以使用双重校验锁模式,只在必要时才加锁。需要注意 volatile 的使用保证原子性操作,并避免指令重排。

public class DclSingleton {
    private static volatile DclSingleton instance;

    private DclSingleton() {
    }

    public static DclSingleton getInstance() {
      if (instance == null) {
        synchronized(DclSingleton.class) {
          if(instance == null) {
              instance = new DclSingleton();
           }
        }
      }
      return instance;
    }
}

双重校验锁相对复杂, 但可以避免大部分不必要的加锁操作,提高了效率。但是它的实现也比较 tricky,需要仔细理解代码逻辑。

方案3:静态内部类

静态内部类是实现线程安全的单例模式的最简洁方法之一。

public class StaticInnerClassSingleton {
  private StaticInnerClassSingleton() {}

    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

当外部类被加载时,静态内部类不会被加载,当外部类的getInstance 方法第一次被调用时,静态内部类才会被加载,内部类的实例被静态变量 INSTANCE 保存。Java 类加载机制天然保证了初始化操作的原子性和线程安全性。这是推荐的方式。

安全建议

使用单例模式时,应当选择适合具体应用场景的实现方式。通常,静态内部类方法更加推荐,因为其简洁高效,并能够有效应对多线程环境。 同时,必须考虑到并发环境下可能出现的问题,选择适合的并发控制方案。

选择正确的技术方案与严谨的代码逻辑才是保证系统稳定运行的基础。