单例模式
是什么
创建对象的一种方式,该方式保证用户在程序运行阶段只创建一个该类实例。
做什么、为什么
适用于那些只能单实例的场景
- 日志收集:这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- 各种池管理技术,例如数据库连接池,线程池等;数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 配置文件读取,同日志类似,是共享的资源,保证只有一个实例访问。
如何实现
-
双重检查锁
public class Singleton{ private volatile Singleton singleton; public static Singleton getInstance(){ if(singleton == null){ synchronized(Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
-
为什么要双重锁
- 首先第一层判断,是为了减少线程争抢锁资源的次数,当有线程创建对象成功后,其余线程就不需要进入
synchronized
代码块了。 - 第二层判断,是为了保证对象不会重复创建,当
线程1
还未创建对象成功时,此时线程2
进入锁等待,当线程1
创建成功后,线程2
获取到锁,进入第二层判断,此时,判断不通过,不创建对象。
- 首先第一层判断,是为了减少线程争抢锁资源的次数,当有线程创建对象成功后,其余线程就不需要进入
-
为什么要加volatile
我们知道 被volatile修饰的变量其内存是可见的,也就是说每个线程所访问的该变量都是从主存内获取的,不存在内存屏障;并且最重要的是防止指令重排序,jvm在加载字节码文件时,会优化相关的指令,从而对指令进行重新排序。加了volatile修饰的变量不会被jvm重新排序。
-
new Singleton()
时,实际上jvm执行了三个主要指令 分别为为对象分配内存
,对象初始化
,-
将内存地址指向堆中对象
假设不加volatile修饰,可能会存在下面的场景,
线程1
创建对象时,jvm将第二步和第三步颠倒过来了,此时线程2
进入第一个判断,判断当前对象不为null(对象已经有内存地址了,并且指向了堆中的未进行初始化的对象),返回对象使用,但是此时对象还没有执行第二步初始化,所以会导致线程2
使用了一个未初始化对象。
-
-
静态内部类持有外部类对象,
该种情况满足了延迟加载和线程安全
public class Singleton{ private Singleton{} private static class InnerSingleton{ private static Singleton singleton = new Singleton() } public static Singleton getInstance(){ return InnerSingleton.singleton; } }
延迟加载原理:类在5种情况下会被初始化
- new对象
- 反射
Class.forName(xxx.class)
; - 调用类的静态变量或者静态方法 (此种情况就是调用
getInstance()
静态方法才会触发加载) - 初始化类时,该类有直接父类,先加载父类
- 用户指定的具有
main
方法的类
线程安全原理:
jvm
在加载类时,保证了类的cinit
方法是同步的,同一时刻,同一类加载器下只有一个线程加载类。 -
两种方案比较
静态内部类缺点:不能传参,双重检查锁可以传参
双重检查锁缺点:加了锁,效率会低点
结论:当无参数时,使用静态内部类,有参数时选择双重检查锁
知识延伸
synchronized
volatile 关键字
- 防止jvm指令重排序
- 线程见的内存可见性
jvm 类加载机制