一、定义
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
注意:
- 单例类只能有一个实例
- 单例类自己创建自己的唯一实例
- 单例类必须给所有对象提供这个实例
二、单例模式的实现
2.1 懒汉式,线程不安全
1 public class Singleton { 2 private static Singleton instance; 3 4 private Singleton() { 5 6 } 7 8 public static Singleton getInstance() { 9 if(instance == null) { 10 instance = new Singleton(); 11 } 12 13 return instance; 14 } 15 }
2.2 懒汉式,线程安全
1 public class Singleton { 2 private static Singleton instance; 3 4 private Singleton() { 5 6 } 7 8 public static synchronized Singleton getInstance() { 9 if(instance == null) { 10 instance = new Singleton(); 11 } 12 13 return instance; 14 } 15 }
虽然做到了线程安全,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
2.3 双重检验锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
1 public class Singleton { 2 private volatile static Singleton instance; //声明成 volatile 3 private Singleton (){} 4 5 public static Singleton getSingleton() { 6 if (instance == null) { 7 synchronized (Singleton.class) { 8 if (instance == null) { 9 instance = new Singleton(); 10 } 11 } 12 } 13 return instance; 14 } 15 16 }
使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。
2.4 饿汉式 线程安全
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
1 public class Singleton{ 2 //类加载时就初始化 3 private static final Singleton instance = new Singleton(); 4 5 private Singleton(){} 6 7 public static Singleton getInstance(){ 8 return instance; 9 } 10 }
2.5 静态内部类
1 public class Singleton { 2 private static class SingletonHolder { 3 private static final Singleton INSTANCE = new Singleton(); 4 } 5 private Singleton (){} 6 public static final Singleton getInstance() { 7 return SingletonHolder.INSTANCE; 8 } 9 }
这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
2.6 枚举Enum
1 public enum EasySingleton{ 2 INSTANCE; 3 }
我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。