单例模式的 8 种实现
饿汉式单例模式
静态常量(线程安全)
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
静态代码块(线程安全)
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
懒汉式单例模式
线程不安全
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
安全同步方法(线程安全,但效率低)
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
部分同步方法(线程不安全)
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
部分同步方法(线程安全,推荐使用)
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) { // A
synchronized (Singleton6.class) {
if (instance == null) { // B
instance = new Singleton6();
}
}
}
return instance;
}
}
第一个 if
是并行的,可以提高效率;第二个 if
是为了防止多个线程等待进入锁而发生多次创建的。这个类的功能是,当实例已经被创建过,则不会经过加锁,直接返回当前实例;否则就加锁,创建实例。这样可以大幅度提高性能,不用每次都加锁,只要对象已经创建成功,getInstance()
方法将不需要获取锁,直接返回已创建好的对象。
至于为什么要用 volatile
修饰,这是因为在执行到 A 或 B 处,如果另一个线程 2 读取到instance
不为 null 时,那么线程 2 就认为对象已经创建过了,直接返回,然后拿来使用,但是此时instance
引用的对象可能还没有初始化完成,那么这样一来,如果线程 2 获取这个对象的属性时就会发生空指针异常。
实际上,对于instance = new Singleton6();
这行代码,可以分解为如下 3 行伪代码:
memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址
由于 2 和 3 之间没有数据依赖性,因此可以进行重排序,重排序后如下:
memory = allocate(); // 1.分配对象的内存空间
instance = memory; // 3.设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory); // 2.初始化对象
虽然 2 和 3 交换了顺序,但是单线程内执行的结果没有改变,然而多线程下执行的结果就不同了,如下图所示。
当线程 A 和 B 按上图顺序执行时,B 线程看到的是一个还没有被初始化的对象。
为了解决上述问题,我们需要在初始化的时候,给它加上一个 volatile
关键字禁止重排序。
volatile
禁止重排序的规则:•
volatile
写之前禁止重排序•volatile
读之后禁止重排序
静态内部类(线程安全,推荐使用)
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
由于使用了静态内部类,相比饿汉式的直接实例化,静态内部类方式可以在需要初始化的时候才初始化,即按需加载。
枚举类型单例模式(线程安全,最佳方法)
public enum Singleton8 {
INSTANCE;
public void getInstance() {
// return whatever
}
}
具体示例:
class User {
// 私有化构造函数
private User() {
}
// 定义一个静态枚举类
static enum SingletonEnum {
// 创建一个枚举对象,该对象天生为单例
INSTANCE;
private User user;
// 私有化枚举的构造函数,只会调用一次
private SingletonEnum() {
user = new User();
}
public User getInstance() {
return user;
}
}
// 对外暴露一个获取User对象的静态方法
public static User getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
}
public class Test {
public static void main(String[] args) {
// true
System.out.println(User.getInstance() == User.getInstance());
}
}
彩蛋
反射攻击破坏单例
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton first = Singleton.getInstance();
Singleton second = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton third = constructor.newInstance();
System.out.println(first + "\n" + second + "\n" + third);
System.out.println("正常情况下,实例化两个实例是否相同:" + (first == second));
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (first == third));
}
// 结果
Singleton@74a14482
Singleton@74a14482
Singleton@1540e19d
正常情况下,实例化两个实例是否相同:true
通过反射攻击单例模式情况下,实例化两个实例是否相同:false
可以看到,反射获取的并不是同一个实例。
序列化破坏单例
// 让单例类实现序列化接口
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File("singleton_file");
// 第一次获取实例
Singleton instance = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
// 第二次获取实例
Singleton newInstance = (Singleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println("序列化前后两个实例是否相同:" + (instance == newInstance));
}
// 结果
Singleton@7f31245a
Singleton@568db2f2
序列化前后两个实例是否相同:false
我们先将第一次获取的实例写入文件,然后第二次读取这个实例,再次获取这个实例,结果表明这两个实例是不同的实例,即通过序列化和反序列化拿了不同的对象。
但我们需要拿到相同对象,那么需要在单例类中加入一个 readResolve()
方法,由于任何一个readObject()
方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例,因此我们只需要让 readObject()
方法返回初始化时的实例对象即可。
修改如下:
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
// 加上这个方法
private Object readResolve() {
return singleton;
}
}
// 结果
Singleton@7f31245a
Singleton@7f31245a
序列化前后两个实例是否相同:true
枚举类型避免反射攻击
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
EnumSingleton singleton1 = EnumSingleton.INSTANCE.getInstance();
EnumSingleton singleton2 = EnumSingleton.INSTANCE.getInstance();
System.out.println("正常情况下,实例化两个实例是否相同:" + (singleton1 == singleton2));
Constructor<EnumSingleton> constructor = null;
constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton singleton3 = null;
singleton3 = constructor.newInstance();
System.out.println(singleton1 + "\n" + singleton2 + "\n" + singleton3);
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (singleton1 == singleton3));
}
}
// 结果
正常情况下,实例化两个实例是否相同:true
Exception in thread "main" java.lang.NoSuchMethodException: concurrency.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at concurrency.singleton.EnumSingleton.main(EnumSingleton.java:22)
可以看到,反射攻击抛出异常了。
枚举类型避免序列化问题
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File("singleton_file");
//第一次获取实例
EnumSingleton instance = EnumSingleton.INSTANCE.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
//第二次获取实例
EnumSingleton newInstance = (EnumSingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println("序列化前后两个实例是否相同:" + (instance == newInstance));
}
}
// 输出
INSTANCE
INSTANCE
序列化前后两个实例是否相同:true