一、概述
单例模式是最简单的设计模式之一,它的核心就是类只存在一个实例,这在很多应用场景中有些类只需要创建一个全局对象,比如一个数据库连接,一个配置文件的资源,等等,可以节省不必要的内存开销。
单例模式设计时要注意两点:
- 类的构造方法私有化
- 定义公共静态方法(
getInstance()
)来获取实例
这两点主要为了只能在该类内部创建对象实例,外部只能通过静态方法来获取,一般将单例模式分为懒汉式单例和饿汉式单例,下面就讲解这两种方法创建的单例有什么不一样。
二、懒汉式单例
通过上面两个注意点,编写一个简单的单例类,并编写Main类来测试:
public class Singleton { private static Singleton instance; // 构造方法私有化 private Singleton() { } // 通过公共静态方法来获取单对象 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
运行上面代码,控制台打印的对象是同一个。我们通过单例类中的 getInstance()
方法来获取实例而不能使用构造方法主动实例化该类,在第一次调用该方法的时候,才会创建单例对象,这种方式叫做懒加载。我们修改 main 方法如下:
public static void main(String[] args) { Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> System.out.println(Singleton.getInstance())); } for (Thread t : threads) { t.start(); } }
使用10个线程来获取单例对象的实例,运行上面代码发现控制台中打印的对象有时候并不是同一个,这是因为上面的单例代码实现是线程不安全的。我们想到的第一解决方案就是给 getInstance()
方法加上同步锁:
public class Singleton { private static Singleton instance; private Singleton() { } // 同一时间内只能被一个线程调用 public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
线程安全的问题解决了,但这样的写法并不是最完美的,因为此时每一次调用 getInstance()
方法都会使用一次同步锁,这种性能开销很没有必要,如果你觉得 synchronized
的性能损耗对你的代码没有影响,这倒是一种简单的解决方案。
这时候我们来分析代码线程不安全的因素,在多个线程中调用 getInstance()
方法,只有在 instance
为 null
的时候才有线程不安全的风险,因为可能在多个线程中同时判断实例为 null
而创建了新的实例,导致打印出来的对象不一致。
当实例不为 null
的时候,所有的线程调用 getInstance()
方法都只会返回同一个对象实例,此时的同步锁就没有什么用处了。所以我们可以只在 instance == null
的情况使用同步锁,其他情况就没有必要使用了。根据这种设计修改单例类如下:
public class Singleton { private volatile static Singleton instance; private Singleton() { } // 使用双检锁,只有当实例为 null 的时候使用锁 public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
上面的代码中用到了两次非空判断,第一次判断的作用是判断什么时候才使用同步锁,它并不是同步的。第二次判断位于 synchronized
代码块内部,它才是线程安全的,所以第二次非空判断才是关键,这样设计只有当线程判断 instance
为 null
的时候才使用同步锁,不为 null
的时候是不会获取到同步锁操作的,这种方法大大节省了 synchronized
的性能开销。在实例字段中添加 volatile
关键字保证了对象实例的可见性。这两次非空判断使用同步锁的方法我们把它叫做双检锁。
三、饿汉式单例
前面我们讲的懒汉式单例有一个很大的特点,就是懒加载,正是因为这样才引起了线程的不安全,如果我们在定义静态变量的时候就创建对象实例,JVM在类加载的时候执行静态初始化就创建实例,这样就可以保证线程安全了。修改单例实现如下:
public class Singleton { // 定义变量时初始化实例 private final static Singleton INSTANCE = new Singleton(); private Singleton() { } public static Singleton getInstance() { return INSTANCE; } }
这种方法简单有效的实现了线程安全的单例模式,但唯一的缺点是牺牲了内存空间,对于比较轻量的类或者整个程序执行都要使用到的类还是可以选择这种解决方案的。