一、基本概念
定义: 确保一个类只有一个实例
优点:- 内存中只有一个实例,可减少内存使用,尤其是需要频繁创建、销毁对象的场景,单例模式比较有优势。
- 避免对资源的多重占用,比如读配置、写文件等操作,只有一个对象去操作资源,避免了多个内存对象对资源的同时操作。
- 单例可设置全局的访问点,共享资源访问。
缺点:
- 扩展比较困难,一般单例没有接口
- 对测试不力
- 单例模式跟单一职责原则冲突,单例会将多种业务逻辑放在一个类中。
使用场景:
- 项目中需要一个共享访问点或共享数据,例如全局配置文件、数据库管理类、网络请求管理类等。
- 创建对象需要消耗资源比较多,例如访问IO、数据库等资源时。 需要事项: 多线程操作单例模式可能存在的线程同步问题。
public class Singleton { private static Singleton singleton = null; private Singleton(){ } public static Singleton getInstance(){ if(singleton == null) { singleton = new Singleton(); } return singleton; }}复制代码
如果两个线程同时执行到singleton == null 的判断的时候,两个线程条件都满足,会出现创建两个对象的情况,违反了单例只创建一个对象的原则。
二、线程安全的单例形式
1、饿汉式
先将对象创建出来,并没有实际的调用,会造成不必要的内存消耗。
public class Singleton { private static Singleton singleton = new Singleton(); private Singleton() { } public static Singleton getInstance() { return singleton; }}复制代码
2、懒汉式(同步方法)
懒汉式:用的时候才进行创建,减少不必要的内存消耗。
public class Singleton { private static Singleton singleton = null; private Singleton(){ } public synchronized static Singleton getInstance(){ if(singleton == null) { singleton = new Singleton(); } return singleton; }}复制代码
getInstance通过synchronized关键字进行修饰,保证只有一个线程执行getInstance方法内的创建逻辑,保证创建出来的对象只有一个。
缺点:synchronized关键字存在效率问题,线程A执行到getInstance方法时,线程B只能处于等待状态,只有等线程A执行完getInstance方法,线程B才能继续执行。3、懒汉式(双重检查加锁)
public class Singleton { private static volatile Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}复制代码
尽量缩小synchronized关键字所修饰的范围
(1)外层singleton == null 判断为true,才执行synchronized所修饰的内部逻辑,否则直接获取singleton对象,保证需要有对象创建就不需要等待 (2)内层singleton == null 判断是为了再次检查对象是否创建。 为什么:线程A执行到synchronized修饰的代码时,这次线程B正在执行,所以线程A只能处于等待状态。线程B执行完毕并将对象创建出来了,而线程A是不知道的,会再次创建,所以需要再次判断对象是否为空。 (3)volatile: 为什么:singleton = new Singleton() 这段代码会编译成多条指令- 将对象new出来,给Singleton分配内存空间
- 调用Singleton构造函数,初始化成员变量
- 将singleton指向分配的内存,此时singleton就不为空了
但是2、3步执行顺序无法保证,就可能线程B执行了这段代码,singleton指向了内存空间,但是成员变量还没初始化完。此时如果线程A通过singleton == null进行判断,发现对象不为空,拿对象去使用,但是成员变量还没初始化完,就会出错。
Java内存模型规定所有的变量都是存在主存当中(共享内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。 被volatile关键字修饰,会保证修改的值立刻更新到主存中,其他线程取值也会去内存中读取新值(直接操作共享内存),保证singleton是最新的。
优点:- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序,保证了singleton = new Singleton()的指令执行顺序。
缺点: volatile屏蔽掉了VM必要的代码优化,所以在效率上比较低
4、静态内部类
public class Singleton { private Singleton() { } private static class SingletonInner { private static final Singleton SINGLETON = new Singleton(); } public static Singleton getInstance() { return SingletonInner.SINGLETON; }}复制代码
如何保证线程安全:利用了classloader的机制保证初始化时只有一个线程。虚拟机会保证静态类初始化的方法在多线程环境被正确加锁、同步,如果多个线程去出初始化,只会有一个线程(线程A)去执行初始化操作,其他线程(线程B)需要阻塞等待。线程A执行完毕后,线程B唤醒后不会进入初始化的方法。同一个类加载器下,只会初始化一次。
是否为延迟加载:内部类在被调用时才会被加载 虽然Singleton被加载了,但是内部类不需要立即加载,所以Singleton还没有实例化,也算是懒汉式的一种(延迟加载),只有主动调用getInstance方法时Singleton才会被实例化。 优点:跟上面几种单例比较,延迟加载,并且没有线程同步问题,效率更高。5、枚举
public enum Singleton{ SINGLETON;}复制代码
单例的枚举实现在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现Singleton的最佳方法。(不过在实际开发中很少见到) 原理:对枚举进行反编译后,SINGLETON被声明为static的,虚拟机保证一个静态变量的线程安全问题,所以枚举是线程安全的。