4.2 单例模式
序言
日常工作中,有些对象我们只需要实例化一个,例如缓存、线程池、日志等对象。这些对象如果实例化多个时,抛去浪费资源不说,而且还可能造成程序错误。例如同事A和同事B需要操作同一个缓存对象,同事A向缓存写入数据,同事B从缓存读取数据。但是他们各自都创建了一个缓存对象。此时同事A将数据写入缓存A,同事B从缓存B读取数据,此时同事B一定是读取不到数据的,这就产生了错误。
产生该错误的原因就是同事A和同事B操作的是不同的缓存(如图1所示),解决方法就是让同事A和同事B操作同一个缓存对象即可(如图2所示)。
由此引出了我们本章的主角:单例模式。下面请跟着我揭开单例模式的神秘面纱。
定义
单例模式的定义很简单:
确保一个类只有一个实例,并对外提供一个全局接口以供访问。
类图
示例代码
// 定义单例 public class Sinleton { private static Sinleton instance = null; // 私有构造函数,避免外部类直接new private Sinleton() {} public static Sinleton getInstance() { // instance未初始化时进行初始化,否则直接返回 if (instance == null) { instance = new Sinleton(); } return instance; } } // 验证 public class Client { public static void main(String[] args) { Sinleton sinleton1 = Sinleton.getInstance(); Sinleton sinleton2 = Sinleton.getInstance(); System.out.println(sinleton1 == sinleton2); // true,说明Sinleton只有一个实例 } }
上述代码是一个最简单的单例模式,主要有几点:
- 私有化构造函数,避免外部类依旧可以new对象
- 对外暴露全局接口
- 实现延时加载(也叫懒加载),即在需要时才进行实例化。所以这种模式也称为懒汉模式
请再次重新阅读上述代码,思考代码是否存在问题,存在怎样的问题?
聪明的你一定会发现上述代码在多线程下是不安全的,因为可能同时有多个线程判断 instance == null
为 true
, 从而造成多个实例化结果。下面我们来验证一下:
public class Client { public static void main(String[] args) throws InterruptedException { // 用来存储实例化结果,方便查看 Set<Sinleton1> sinletonSet = new HashSet<>(); for (int i = 0; i < 100; i++) { new Thread(() -> { sinletonSet.add(Sinleton1.getInstance()); }).start(); } // 等待子线程执行完毕 Thread.sleep(2000); // 输出实例(此处可能会由于多线程问题导致有多个实例) sinletonSet.forEach(System.out::println); } }
上述代码执行完,如果输出了多个内容,则说明了线程不安全,导致产生了多个实例。如果你的输出只有一个结果,那就多执行几次,一定会有问题的
问题原因就是上面提到的:可能同一时刻有多个线程判断 instance == null
为 true
,从而执行new Singleton()
导致生成多个实例。知道了问题的原因,我们就来看看如何解决该问题。
一提到多线程,大家应该会很快想到通过锁机制保证线程安全。此处我们借助 synchronized
加锁保证线程安全。
面试考点:请同学自行查阅保证线程安全的方式有哪些
public class Sinleton { private static Sinleton instance = null; // 私有构造函数,避免外部类直接new private Sinleton() {} public static Sinleton getInstance() { synchronized (Sinleton.class) { if (instance == null) { instance = new Sinleton(); } } return instance; } }
然后请各位继续执行上面的验证代码,自行验证是否只会产生一个实例。虽然加锁能保证线程安全,但是加锁、释放锁会降低系统的吞吐量,那让我们想想能不能不通过加锁就能保证线程安全呢?
面试考点:什么是乐观锁、悲观锁。乐观锁其实没有真正的锁
我们想想懒汉模式为什么会出问题,是因为每个线程都有创建实例的权利,所以会存在线程安全问题。那如果我们把创建线程的权利回收,只有 Singleton
自己才能创建实例,那是不是就没有线程安全问题了呢?
public class Sinleton { // 初始化时就创建实例,避免线程安全问题 private static Sinleton instance = new Sinleton(); private Sinleton1() {} public static Sinleton getInstance() { return instance; } }
然后请各位继续执行上面的验证代码,自行验证是否只会产生一个实例。验证完成你会发现只会产生一个实例,所以这段代码是线程安全的。这段代码有个好听的名字就是饿汉模式。
面试考点:
- 懒汉和饿汉模式代码书写
- 哪种是线程安全的
单例模式就讲这么多,更多关于单例模式问题,请在本章节最后一章“常见面试题”中进行查看。