单例模式

单例模式

单例模式, 顾名思义就是只有一个实例, 并且它自己负责创建自己的对象, 这个类提供了一种访问其唯一的对象的形式. 可以直接访问, 不需要实例化该类的对象.


单例模式的几种形式

饿汉式

class Singleton {
    private Singleton() { }
    private static Singleton instance = new Singleton();
    public static Singleton newInstance() {
        return instance;
    }
}

实例在类初始化的时候就创建好了, 不管你有没有用到. 好处是没有线程安全问题, 坏处是比较浪费内存空间.


懒汉式

class Singleton {
    private Singleton() { }
    private static Singleton instance;
    public static Singleton newInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式, 顾名思义就是实例在用到的时候才去创建.
有线程安全和不安全两种写法, 区别就是synchronized关键字


双检锁

class Singleton {
    private Singleton() { }
    private static Singleton instance;
    public static Singleton newInstance() {
        if (instance == null) {		// 第一次检查, 有则返回  (此时尚未加锁, 可以提高效率)
			   synchronized(Singleton.class) {
					if (instance == null) {		// 第二次检查, 再获取到锁之间, 再次检查是为完成初始化
						instance = new Singleton();
					}
			   }
        }
        return instance;
    }
}

双检锁, 又叫双重校验锁, 综合了饿汉式和懒汉式两者的优缺点整合而成, 从上面的代码实现看, 特点是在 synchronized关键字内外都加了一层 if条件判断, 这样既保证了线程安全, 又比直接上锁提高了执行效率, 还节省了内存空间. (其实有坑)


静态内部类

class Singleton {
    private Singleton() { }
    private static class Inner {
		  private static Singleton instance = new Singleton();
	  }
    public static Singleton newInstance() {
        return instance;
    }
}
  • 静态内部类不会随着外部类的初始化而初始化, 它是需要单独去加载和初始化的, 当第一次执行 getInstance()方法时, Inner类会被初始化.
  • 静态对象 instance的初始化在 Inner类初始化阶段进行, 类初始化阶段即虚拟机执行类构造器<client>() 方法的过程.
  • 虚拟机会保证一个类的 <client>()方法在多线程环境下被正确地加锁和同步, 如果多个线程同时初始化一个类, 只会有一个线程执行这个类的 <client>方法, 其他线程都会阻塞等待.

枚举

enum Singleton {
    INSTANCE;
    public void doSomething() {
        System.out.println("do something ...");
    }
}

枚举的方式是比较少见一种的实现方式. 但是看上面的代码, 却简洁清晰. 并且它还自动支持序列化机制, 绝对防止多次实例化.


传统单例模式双重检查锁存在的问题

单例模式 1.0

class Singleton {
    private Singleton() { }
    private static Singleton instance;
    public static Singleton newInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这种方式很辣鸡, 因为多线程环境下不能保证单例.


单例模式 2.0

class Singleton {
    private Singleton() { }
    private static Singleton instance;
    public static synchronized Singleton newInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这种方式也很辣鸡, 因为多线程环境下, 每个线程执行 getInstance()都要阻塞, 效率很低.


单例模式 3.0

class Singleton {
    private Singleton() { }
    private static Singleton instance;
    public static Singleton newInstance() {
        if (instance == null) {		// 位置1
			   synchronized(Singleton.class) {
					if (instance == null) {
						instance = new Singleton();		// 位置2
					}
			   }
        }
        return instance;
    }
}

这种方式使用双重检查锁, 多线程环境下执行 getInstance()时, 先判断单例对象是否已初始化, 如果已经初始化, 就直接返回单例对象, 如果未初始化, 就在同步代码快中先完成初始化, 然后返回, 效率很高.

但是: 这种方式是一个错误的优化, 问题的根源出现在位置2,
instance = new Singleton(); 这句话创建了一个对象, 它可以分解成如下3行代码:

memory = allocate();    // 1. 分配对象的内存空间
ctorInstance(memory);   // 2. 初始化对象
instance = memory;      // 3. 设置 instance指向刚分配的内存地址.

上述伪代码中的 2和3之间可能发生重排序, 重排序后的执行顺序如下.

memory = allocate();   // 1.分配对象的内存空间
instance = memory;     // 2.设置instance指向刚分配的内存地址,此时对象还没有被初始化
ctorInstance(memory);   // 3.初始化对象

因为这种重排序并不影响 java规范中的规范, intra-thread sematics允许那些在单线程内不会改变单线程程序执行结果的重排序.
但是多线程并发时可能会出现以下情况: 线程B访问到的是一个还未初始化的对象

双检锁单例模式的问题演示图

解决方案1:

将对象声明为 volatile后, 前面的重排序在多线程环境下将被禁止.
class Singleton {
    private Singleton() { }
    private static volatile Singleton instance;
    public static Singleton newInstance() {
        if (instance == null) {		// 位置1
			   synchronized(Singleton.class) {
					if (instance == null) {
						instance = new Singleton();		// 位置2
					}
			   }
        }
        return instance;
    }
}

解决方案2:

class Singleton {
    private Singleton() { }
    private static class Inner {
		  private static Singleton instance = new Singleton();
	  }
    public static Singleton newInstance() {
        return instance;
    }
}
  • 静态内部类不会随着外部类的初始化而初始化, 它是需要单独去加载和初始化的, 当第一次执行 getInstance()方法时, Inner类会被初始化.
  • 静态对象 instance的初始化在 Inner类初始化阶段进行, 类初始化阶段即虚拟机执行类构造器<client>() 方法的过程.
  • 虚拟机会保证一个类的 <client>()方法在多线程环境下被正确地加锁和同步, 如果多个线程同时初始化一个类, 只会有一个线程执行这个类的 <client>方法, 其他线程都会阻塞等待.

似乎静态内部类看起来已经是最完美的方法了, 其实不是, 可能还存在反射攻击或者反序列化攻击. 且看如下代码:

public static void main(String[] args) throws Exception {
    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton);		// 运行结果:  false
}

通过结果看, 这两个实例不是同一个, 这就违背了单例模式的原则了. 出了反射攻击之外, 还可能存在序列化攻击的情况.

class Singleton8 implements Serializable {
    private Singleton8() {}
    private static class SingletonHolder {
        private static Singleton8 instance = new Singleton8();
    }
    public static Singleton8 getInstance() {
        return SingletonHolder.instance;
    }

    public static void main(String[] args) {
        Singleton8 instance = Singleton8.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton8 newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance);        // false
    }
}

解决方案3:

通过枚举实现单例模式
<<Effective Java>>书中说道, 最佳的单例实现模式就是枚举模式. 利用枚举的特性, 让 JVM来帮我们保证线程安全和单一实例. 初次之外, 写法还特别简单.

enum Singleton {
    INSTANCE;
    public void doSomething() {
        System.out.println("do something ...");
    }
}

参考文章

  1. 单例模式的五种写法_absolute_chen的博客-CSDN博客
  2. 传统单例模式双重检查锁存在的问题 - 君奉天 - 博客园
  3. Java单例模式:为什么我强烈推荐你用枚举来实现单例模式 - happyjava - 博客园
posted @ 2020-09-17 18:44  九月清晨-_-  阅读(49)  评论(0编辑  收藏  举报