概述


单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,单例模式一般会屏蔽构造器,另外为单例对象提供一个全局访问点,单例模式属于创建型模式。
单例模式的实现方式比较多,主要分为在实现上是否支持懒汉模式,是否支持在线程安全中运用各项技巧

饿汉式单例


饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。因为在线程还没出现以前对象就被实例化了,所以其线程绝对安全,不可能存在访问安全问题。

饿汉式单例示例

我们将所有单例模式都放在 singleton 目录下,然后为了区分不同的单例模式,再为每种单例模式也新建一个目录,所以饿汉式我们最后新建的目录为 singleton/hungry。

  • 新建一个单例对象:HungrySingleton.java。 ```java package singleton.hungry;

public class HungrySingleton { private static final HungrySingleton hungrySigleton = new HungrySingleton();

  1. private HungrySingleton() {
  2. }
  3. public static HungrySingleton getInstance(){
  4. return hungrySigleton;
  5. }

}

  1. 可以看到,将单例对象的构造器私有化了,所以无法直接通过 new 来创建对象,只能通过我们提供的 getInstance 方法来获得单例对象。<br />当然,我们也可以通过静态代码块的方式来实现,下面就是一个静态代码块的实现方式。
  2. - 定义一个单例对象:HungryStaticSingleton.java
  3. ```java
  4. package singleton.hungry;
  5. public class HungryStaticSingleton {
  6. private static final HungryStaticSingleton hungrySigleton;
  7. static {
  8. hungrySigleton = new HungryStaticSingleton();
  9. }
  10. private HungryStaticSingleton() {
  11. }
  12. public static HungryStaticSingleton getInstance(){
  13. return hungrySigleton;
  14. }
  15. }
  • 新建一个测试类 TestHungrySingleton.java 来进行测试。
  1. package singleton.hungry;
  2. public class TestHungrySingleton {
  3. public static void main(String[] args) {
  4. //测试普通写法
  5. HungrySingleton singleton1 = HungrySingleton.getInstance();
  6. HungrySingleton singleton2 = HungrySingleton.getInstance();
  7. System.out.println(singleton1 == singleton2);//输出:true
  8. //测试静态类写法
  9. HungryStaticSingleton singleton3 = HungryStaticSingleton.getInstance();
  10. HungryStaticSingleton singleton4 = HungryStaticSingleton.getInstance();
  11. System.out.println(singleton3 == singleton4);//输出:true
  12. }
  13. }

现在我们需要验证一下结果,先执行 javac singleton/hungry/*.java 命令进行编译。然后再执行 java singleton.hungry.TestHungrySingleton 命令运行测试类(大家一定要自己动手运行哦,只有自己实际去运行了才会更能体会其中的思想)。
创建型——单例(Singleton) - 图1
可以看到,最后的输出结果一定和预想中的一样返回 true,因为不论我们调用多少次 getInstance 方法最后返回的就是同一个对象。

饿汉式单例的优缺点

  • 优点:创建对象时没有加任何的锁、执行效率比较高。
  • 缺点:也很明显,因为其在类加载的时候就初始化了,也就是说不管我们用或者不用都占着空间,如果项目中有大量单例对象,则可能会浪费大量内存空间。

懒汉式单例

懒汉式单例正如其名字一样,很懒,所以类加载的时候不会初始化,而是等到需要用到单例对象了才会进行初始化。

懒汉式单例示例

这里懒汉式我们再新建一个 singleton/lazy 目录。

  • 改造一下测试类。 ```java package singleton.lazy;

public class LazySingleton { private static LazySingleton lazySingleton = null;

  1. private LazySingleton() {
  2. }
  3. public static LazySingleton getInstance(){
  4. if(null == lazySingleton){//为空则说明第一次获取单例对象,进行初始化
  5. lazySingleton = new LazySingleton();
  6. }
  7. return lazySingleton;//不为空则说明已经初始化了,直接返回
  8. }

}

  1. 这里我们一开始只是赋了一个 null 值,并没有进行初始化,而是在调用 getInstance 方法的时候才会去初始化单例对象。<br />这种写法是最简单的一种懒汉式单例写法,但是存在线程安全问题,多线程情况下会有一定几率返回多个单例对象,这明显违背了单例对象原则,那么如何优化上面的代码呢?答案就是加上 synchronized 关键字。
  2. - 新建一个 LazySyncSingleton.java 类。
  3. ```java
  4. package singleton.lazy;
  5. public class LazyDoubleCheckSingleton {
  6. private volatile static LazyDoubleCheckSingleton lazySingleton = null;
  7. private LazyDoubleCheckSingleton() {
  8. }
  9. public static LazyDoubleCheckSingleton getInstance(){
  10. if(null == lazySingleton){
  11. synchronized (LazyDoubleCheckSingleton.class){
  12. if(null == lazySingleton){
  13. lazySingleton = new LazyDoubleCheckSingleton();
  14. }
  15. }
  16. }
  17. return lazySingleton;
  18. }
  19. }

上面这个示例就是加了一个 synchronized 关键字,其它地方和第一个示例没有任何改动。现在确实是解决了线程安全的问题,但是同样这里也可能存在一个问题。假如单例对象的创建非常复杂耗时的情况下,一旦并发量上来了,CPU 压力上升,那么就可能会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。所以这就有了另一种写法:双重检查锁(double-checked locking)单例写法。

  • 新建一个 LazyDoubleCheckSingleton.java 类。 ```java package singleton.lazy;

public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazySingleton = null;

  1. private LazyDoubleCheckSingleton() {
  2. }
  3. public static LazyDoubleCheckSingleton getInstance(){
  4. if(null == lazySingleton){
  5. synchronized (LazyDoubleCheckSingleton.class){
  6. if(null == lazySingleton){
  7. lazySingleton = new LazyDoubleCheckSingleton();
  8. }
  9. }
  10. }
  11. return lazySingleton;
  12. }

}

  1. 这种写法相比较于上面的写法两个地方做了改变:
  2. - 一个是 lazySingleton 属性上加了 volatile 关键字来修饰,原因就是解决多线程下的可见性问题,因为我们的 getInstance 方法在判断 lazySingleton 是否为 null 时候并没有加锁,所以假如线程 t1 初始化过了对象,另外线程如 t2 是无法感知的,而加上了 volatile 就可以感知到。
  3. - 另一个改变就是把 synchronized 关键字移到了方法内部,尽可能缩小加锁的代码块,提升效率。
  4. 这种写法是不是很完美?然而结果是残酷的,这种写法也不完美,依然会存在问题,这其中就涉及到了指令重排序的概念了。<br />上面 new 对象只有一行代码,然而这一行代码在 JVM 底层却分成了 3 步:
  5. - 分配内存来创建对象,即:new
  6. - 创建一个对象 lazySingleton,此时 lazySingleton == null
  7. - new 出来的对象赋值给 lazySingleton
  8. 实际运行的时候为了提升效率,这 3 步并不会按照实际顺序来运行的。那我们打个比方,假如有一个线程 t1 进入同步代码块正在创建对象,而此时执行了上面 3 个步骤中的后面 2 步,也就是说这时候 lazySingleton 已经不为 null 了,但是对象却并没有创建结束;此时又来了一个线程 t2 进入 getInstance 方法,这时候 if 条件肯定不成了,线程 t2 会直接返回,也就相当于返回了一个残缺不全的对象,这时候代码就会报错了。<br />下面我们再看另一种内部的懒汉式单例写法。
  9. - 新建一个类 LazyInnerClassSingleton.java
  10. ```java
  11. package singleton.lazy;
  12. public class LazyInnerClassSingleton {
  13. private LazyInnerClassSingleton(){
  14. }
  15. public static final LazyInnerClassSingleton getInstance(){
  16. return InnerLazy.LAZY;
  17. }
  18. private static class InnerLazy{
  19. private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
  20. }
  21. }

这种写法巧妙的利用了内部类会等到外部调用时才会被初始化的特性,用饿汉式单例的思想实现了懒汉式单例。
这种写法看起来是不是效率又高又完美,然而就目前的写法而言依然存在不安全的隐患,下面我们用一个测试来破坏内部类的单例写法。

  • 新建一个测试类 TestLazyInnerClassSingleton.java。 ```java package singleton.lazy;

import java.lang.reflect.Constructor;

public class TestLazyInnerClassSingleton { public static void main(String[] args) throws Exception { Class<?> clazz = LazyInnerClassSingleton.class; Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); Object o1 = constructor.newInstance(); Object o2 = LazyInnerClassSingleton.getInstance();

  1. System.out.println(o1 == o2);
  2. }

}

  1. 执行 javac singleton/lazy/*.java 命令进行编译。然后再执行 java singleton.lazy.TestLazyInnerClassSingleton 命令运行测试类<br />![](https://cdn.nlark.com/yuque/0/2021/png/333017/1625066770332-4a8e6754-477e-4800-b7e5-54698daf72dc.png#clientId=u2a1262c3-83d5-4&from=paste&id=uf40e09d3&margin=%5Bobject%20Object%5D&originHeight=380&originWidth=984&originalType=url&ratio=1&status=done&style=none&taskId=u31cba120-4d31-4c0f-8429-014311d55e5)<br />可以看到,虽然构造方法被私有化了,但是我们仍然可以利用反射来破坏单例。为了防止反射破坏单例,我们将上面的写法再改造一下。
  2. - 改造 LazyInnerClassSingleton.java 类。
  3. ```java
  4. package singleton.lazy;
  5. public class LazyInnerClassSingleton {
  6. private LazyInnerClassSingleton(){
  7. //防止反射破坏单例
  8. if(null != InnerLazy.LAZY){
  9. throw new RuntimeException("不允许通过反射类构造单例对象");
  10. }
  11. }
  12. public static final LazyInnerClassSingleton getInstance(){
  13. return InnerLazy.LAZY;
  14. }
  15. private static class InnerLazy{
  16. private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
  17. }
  18. }

再次执行 javac singleton/lazy/*.java 命令进行编译。然后再执行 java singleton.lazy.TestLazyInnerClassSingleton 命令运行测试类(大家一定要自己动手运行哦,只有自己实际去运行了才会更能体会其中的思想)。
创建型——单例(Singleton) - 图2
如此,完美防止了反射来破坏单例。
但是假如我们的单例对象实现了 Serializable 接口,那么内部类的写法就还是能通过序列化来破坏。

  • 实现了序列化接口的内部类单例写法,修改 LazyInnerClassSingleton.java 文件。
  1. package singleton.lazy;
  2. import java.io.Serializable;
  3. public class LazyInnerClassSingleton implements Serializable {
  4. private LazyInnerClassSingleton(){
  5. //防止反射破坏单例
  6. if(null != InnerLazy.LAZY){
  7. throw new RuntimeException("不允许通过反射类构造单例对象");
  8. }
  9. }
  10. public static final LazyInnerClassSingleton getInstance(){
  11. return InnerLazy.LAZY;
  12. }
  13. private static class InnerLazy {
  14. private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
  15. }
  16. }
  • 新建一个测试类 TestLazyInnerClassSingleton2.java 来通过序列化破坏单例。
  1. package singleton.lazy;
  2. import java.io.FileInputStream;
  3. import java.io.FileOutputStream;
  4. import java.io.ObjectInputStream;
  5. import java.io.ObjectOutputStream;
  6. public class TestLazyInnerClassSingleton2 {
  7. public static void main(String[] args) {
  8. //序列化攻击内部类式单例
  9. LazyInnerClassSingleton s1 = null;
  10. LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
  11. FileOutputStream fos = null;
  12. try {
  13. fos = new FileOutputStream("LazyInnerClassSingleton.text");
  14. ObjectOutputStream oos = new ObjectOutputStream(fos);
  15. oos.writeObject(s2);
  16. oos.flush();
  17. oos.close();
  18. FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.text");
  19. ObjectInputStream ois = new ObjectInputStream(fis);
  20. s1 = (LazyInnerClassSingleton)ois.readObject();
  21. ois.close();
  22. System.out.println(s1 == s2);//输出:false
  23. }catch (Exception e){
  24. e.printStackTrace();
  25. }
  26. }
  27. }

再次执行 javac singleton/lazy/*.java 命令进行编译。然后再执行 java singleton.lazy.TestLazyInnerClassSingleton2 命令运行测试类
创建型——单例(Singleton) - 图3
上面示例中 s1 是通过我们自己提供的全局入口创建的对象,而 s2 是通过序列化的方式创建的对象,不相等说明这是两个对象,也就是说序列化破坏了单例模式。
解决办法就是在 LazyInnerClassSingleton 类中加一个 readResolve 方法。

  • 改写 LazyInnerClassSingleton.java 类,防止序列化破坏单例。
  1. package singleton.lazy;
  2. import java.io.Serializable;
  3. public class LazyInnerClassSingleton implements Serializable {
  4. private LazyInnerClassSingleton(){
  5. //防止反射破坏单例
  6. if(null != InnerLazy.LAZY){
  7. throw new RuntimeException("不允许通过反射类构造单例对象");
  8. }
  9. }
  10. public static final LazyInnerClassSingleton getInstance(){
  11. return InnerLazy.LAZY;
  12. }
  13. private static class InnerLazy {
  14. private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
  15. }
  16. private Object readResolve(){
  17. return InnerLazy.LAZY;
  18. }
  19. }

package singleton.lazy; import java.io.Serializable; public class LazyInnerClassSingleton implements Serializable { private LazyInnerClassSingleton(){ //防止反射破坏单例 if(null != InnerLazy.LAZY){ throw new RuntimeException(“不允许通过反射类构造单例对象”); } } public static final LazyInnerClassSingleton getInstance(){ return InnerLazy.LAZY; } private static class InnerLazy { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } private Object readResolve(){ return InnerLazy.LAZY; } }
这里我们加了一个 readResolve,这个方法返回的也是我们内部类中的对象。
再次执行 javac singleton/lazy/*.java 命令进行编译一下。最后再执行 java singleton.lazy.TestLazyInnerClassSingleton2 命令运行测试类(大家一定要自己动手运行哦,只有自己实际去运行了才会更能体会其中的思想)。
创建型——单例(Singleton) - 图4
这次返回了 true,也就是序列化没有破坏单例了。原因是因为 JDK 源码中在序列化的时候会检验一个类中是否存在一个 readResolve 方法,如果存在,则会放弃通过序列化产生的对象,而返回原本的对象。
这种方式虽然保证了单例,但是在校验是否存在 readResolve 方法前还是会产生一个对象,只不过这个对象会在发现类中存在 readResolve 方法后丢掉,然后返回原本的单例对象。这种写法只是保证了结果的唯一,但是过程中依然会被实例化多次,假如创建对象的频率增大,就意味着内存分配的开销也随之增大。
上面介绍了这么多种写法,看起来每种写法似乎都存在问题,难道就没有一种最优雅、安全、高效的方法吗?这就是我们最后要介绍的枚举式单例,不过在介绍枚举式单例之前,我们先看看其它写法。

注册式单例


注册式单例就是将每一个实例都保存起来,然后在需要使用的时候直接通过唯一的标识获取实例。

注册式单例示例

在这里我们新建一个 singleton/register 目录。

  • 新建一个 ContainerSingleton.java 类。 ```java package singleton.register;

import java.util.Map; import java.util.concurrent.ConcurrentHashMap;

public class ContainerSingleton { private ContainerSingleton(){ }

  1. private static Map<String,Object> ioc = new ConcurrentHashMap<>();//存储单例对象
  2. public static Object getBean(String className){
  3. synchronized (ioc){
  4. if(!ioc.containsKey(className)){//如果容器中不存在当前对象
  5. Object obj = null;
  6. try {
  7. obj = Class.forName(className).newInstance();
  8. ioc.put(className,obj);//将className作为唯一标识存入容器
  9. }catch (Exception e){
  10. e.printStackTrace();
  11. }
  12. return obj;
  13. }
  14. return ioc.get(className);//如果容器中已经存在了单例对象,则直接返回
  15. }
  16. }

}

  1. - 新建一个空对象 MyObject.java,用来测试单例。
  2. ```java
  3. package singleton.register;
  4. public class MyObject {
  5. }
  • 新建一个测试类 TestContainerSingleton.java。
  1. package singleton.register;
  2. public class TestContainerSingleton {
  3. public static void main(String[] args) {
  4. MyObject myObject1 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");
  5. MyObject myObject2 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");
  6. System.out.println(myObject1 == myObject2);//输出:true
  7. }
  8. }

执行 javac singleton/register/*.java 命令进行编译。然后再执行 java singleton.register.TestContainerSingleton 命令运行测试类
创建型——单例(Singleton) - 图5
上面返回 true 是因为我们加了 synchronized 关键字,实际上 Spring 框架中用的就是容器式单例,默认是线程不安全的。

ThreadLocal 式单例

ThreadLocal 式单例不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,在单线程环境下线程天生安全。

ThreadLocal 式示例

在这里我们新建一个 singleton/thread 目录。

  • 新建一个 ThreadLocalSingleton.java 类。
  1. package singleton.thread;
  2. public class ThreadLocalSingleton {
  3. private ThreadLocalSingleton() {
  4. }
  5. private static final ThreadLocal<ThreadLocalSingleton> singleton =
  6. new ThreadLocal<ThreadLocalSingleton>() {
  7. @Override
  8. protected ThreadLocalSingleton initialValue() {
  9. return new ThreadLocalSingleton();
  10. }
  11. };
  12. public static ThreadLocalSingleton getInstance(){
  13. return singleton.get();
  14. }
  15. }
  • 新建一个测试类 TestThreadLocalSingleton.java 进行测试。
  1. package singleton.thread;
  2. public class TestThreadLocalSingleton {
  3. public static void main(String[] args) {
  4. System.out.println(ThreadLocalSingleton.getInstance());//主线程输出
  5. System.out.println(ThreadLocalSingleton.getInstance());//主线程输出
  6. Thread t1 = new Thread(()-> {
  7. ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
  8. System.out.println(Thread.currentThread().getName() + ":" + singleton);//t1线程输出
  9. });
  10. t1.start();
  11. }
  12. }

执行 javac singleton/thread/*.java 命令进行编译。然后再执行 java singleton.thread.TestThreadLocalSingleton 命令运行测试类
创建型——单例(Singleton) - 图6
从上图可以看到,main 线程输出的和 t1 线程输出的并不是同一个对象,故而 ThreadLocal 式示例仅对单线程是安全的。

枚举式单例

枚举式单例充分利用了枚举类的特性来创建单例对象,目前来说这是最优雅的一种写法。

枚举式单例示例

在这里我们新建一个 singleton/meiju 目录。

  • 照例我们新建一个空的对象 MyObject.java 来测试单例。
  1. package singleton.meiju;
  2. public class MyObject {
  3. }
  • 新建一个枚举类 EnumSingleton.java。
  1. package singleton.meiju;
  2. public enum EnumSingleton {
  3. INSTANCE;
  4. private MyObject myObject;
  5. EnumSingleton() {
  6. this.myObject = new MyObject();
  7. }
  8. public Object getData() {
  9. return myObject;
  10. }
  11. public static EnumSingleton getInstance(){
  12. return INSTANCE;
  13. }
  14. }
  • 新建测试类 TestEnumSingleton.java 进行测试。
  1. package singleton.meiju;
  2. public class TestEnumSingleton {
  3. public static void main(String[] args) throws Exception{
  4. EnumSingleton enumSingleton = EnumSingleton.getInstance();
  5. System.out.println(enumSingleton.getData() == enumSingleton.getData());//输出:true
  6. }
  7. }

执行 javac singleton/meiju/*.java 命令进行编译。然后再执行 java singleton.meiju.TestEnumSingleton 命令运行测试类(大家一定要自己动手运行哦,只有自己实际去运行了才会更能体会其中的思想)。
创建型——单例(Singleton) - 图7
输出结果为 true,但是大家可以看到,上面的构造器我并没有私有化,私有化尚且不能阻止反射来破坏单