单例模式
目录
一、单例模式理论知识
1. 为什么要使用单例?:
- 保证一个类只会创建一个实例,以保证对某个属性或方法有全局唯一的访问入口,防止资源访问冲突
2. 优点:
减少维护对象的开销:创建、内存占用、回收
避免对资源的多重占用:例如写文件操作
3. 使用场景:
要求生成唯一序列号的场景
创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源
在整个项目中需要一个共享访问点或共享数据:例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的
4. 注意问题
线程安全:防止多个线程同时执行调用到了new方法,导致实例化出多个对象
指令重排:new实例的语句底层指令可能发生重排导致返回未实例化好的对象
避免外部用特殊方式创建实例
- 构造函数需要是 private 访问权限的,初步避免外部通过 new 创建实例
- 反射构造:私有构造函数抛异常 if (instance != null) throw new RuntimeException(“private constructor”);
- 反序列化:添加readResolve方法,直接return instance;
二、单例模式具体实践
实现方式:
- 饿汉式
- 懒汉式(双重检测)
- 静态内部类
- 枚举
(一)饿汉式(非懒加载)
public class Singleton1 {
/**
* 是否 Lazy 初始化:否
* 是否多线程安全:是
* 实现难度:易
* 描述:这种方式比较常用,但容易产生垃圾对象。
* 优点:没有加锁,执行效率会提高。
* 缺点:类加载时就初始化,浪费内存(但是避免了后期内存不够导致初始化失败,取舍需按实际情况分析)。
*
* 它基于 classloader 机制避免了多线程的同步问题,
* 不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,
* 在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,
* 这时候初始化 instance 显然没有达到 lazy loading 的效果。
*/
//单例对象 加载时就实例化
private static Singleton1 instance = new Singleton1();
//私有构造方法
private Singleton1(){}
//静态工厂方法
public static Singleton1 getInstance() {
return instance;
}
}
(二)懒汉式(双重检测 懒加载且绝对的线程安全)
/**
* 优点:延迟加载、支持高并发
*/
public class Singleton2 {
//私有构造函数
private Singleton2() {}
//单例对象 添加volatile关键字
private volatile static Singleton2 instance = null;
//静态工厂方法-获取单例对象
public static Singleton2 getInstance() {
Singleton2 localRef = instance;
if (localRef == null) { //双重检测机制
synchronized (Singleton2.class){ //同步锁
if (localRef == null) { //双重检测机制
instance = new Singleton2();
}
}
}
return localRef;
}
}
/**
* Using localRef, we are reducing the access of volatile variable to just one for positive usecase.
* If we do not use localRef, then we would have to access volatile variable twice - once for checking null and then at method return time.
* Accessing volatile memory is quite an expensive affair because it involves reaching out to main memory.
*/
(三)静态内部类方式(和饿汉式类似,但是是懒加载)
/**
* 单例模式例四(静态内部类方式) 懒加载
*
* 这种方式能达到双检锁方式一样的功效,但实现更简单。
* 对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
* 这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
*/
public class Singleton3 {
/**
* 1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
*
* 2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,
* 使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
*
*
*存在问题:无法防止利用反射来重复构建对象
* 复现第一步,获得单例类的构造器。
* 复现第二步,把构造器设置为可访问。
* 复现第三步,使用newInstance方法构造对象。
*
* <code>
* //获得构造器
* Constructor con = Singleton.class.getDeclaredConstructor();
*
* //设置为可访问
* con.setAccessible(true);
*
* //构造两个不同的对象
* Singleton singleton1 = (Singleton)con.newInstance();
* Singleton singleton2 = (Singleton)con.newInstance();
*
* //验证是否是不同对象,结果为false
* System.out.println(singleton1.equals(singleton2));
* </>
*
*
*/
//私有构造方法
private Singleton3(){}
//静态工厂方法-获取单例对象
public static Singleton3 getInstance() {
return LazyHolder.INSTANCE;
}
//静态内部类
private static class LazyHolder {
private static final Singleton3 INSTANCE = new Singleton3();
}
}
(四)枚举类方式(非懒加载)
/**
* 单例模式例五(枚举类方式) 非懒加载
* 目前此方式是所有单例模式中最推荐的模式?
*
*/
public class Singleton4 {
/**
* JVM会保证enum不能被反射并且构造器方法只执行一次
*
* 枚举实现可以防止反射构造对象,而且可以保证线程安全
* 缺点:并非使用懒加载,单例对象是在枚举类被加载的时候进行初始化的
*/
//私有构造方法
private Singleton4(){}
//静态方法
public static Singleton4 getInstance(){
return SingletonEnum.INSTANCE.getInstance();
}
private static enum SingletonEnum{
/**
* 单例对象
*/
INSTANCE;
private Singleton4 singleton4;
//JVM会保证此方法绝对只调用一次
SingletonEnum(){
singleton4 = new Singleton4();
}
public Singleton4 getInstance() {
return singleton4;
}
}
}
(五)反例:懒汉式(线程不安全,严格来说不算是单例)
/**
* 单例模式例一(线程不安全)懒汉式
*/
public class Singleton_1 {
/**
* 是否 Lazy 初始化:是
* 是否多线程安全:否
* 实现难度:易
* 描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
* 这种方式 lazy loading 很明显,不要求线程安全,在多线程下不能正常工作。
*
* 为什么说是线程不安全的
* 假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法
* 因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作
* 这样一来,显然instance被构建了两次。
*/
//私有构造函数
private Singleton_1() {}
//单例对象
private static Singleton_1 instance = null;
//静态工厂方法-获取单例对象的方法
public static Singleton_1 getInstance() {
if (instance == null) {
instance = new Singleton_1();
}
return instance;
}
}
(六)反例:懒汉式 (双重检测机制,不是绝对的线程安全版本少了volatile关键字,仅发生于低版本JDK中)
/**
* 单例模式例二(双重检测机制) 懒汉式
*/
public class Singleton_2 {
/**
* 为什么说不是绝对的线程安全
* 这里涉及到了JVM编译器的指令重排。
*
* 指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
*
*
* memory =allocate(); //1:分配对象的内存空间
* ctorInstance(memory); //2:初始化对象
* instance =memory; //3:设置instance指向刚分配的内存地址
*
*
* 但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
*
*
* memory =allocate(); //1:分配对象的内存空间
* instance =memory; //3:设置instance指向刚分配的内存地址
* ctorInstance(memory); //2:初始化对象
*
*
* 当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,
* 执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
*/
//私有构造函数
private Singleton_2() {}
//单例对象
private static Singleton_2 instance = null;
//静态工厂方法-获取单例对象的方法
public static Singleton_2 getInstance() {
//双重检测机制
if (instance == null) {
//同步锁 锁住整个类(注意,这里不能使用对象锁)
synchronized (Singleton_2.class){
//双重检测机制
//进入Synchronized 临界区以后,还要再做一次判空。
//因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证并马上获取到了锁,
//不做第二次判空的话,线程B以为还未初始化还是会再次构建instance对象。
if (instance == null) {
instance = new Singleton_2();
}
}
}
return instance;
}
}
三、单例模式经典案例
Spring中,项目加载时bean自动创建,而后每次调用bean的时候由IOC容器注入。对于IOC容器,实现了Singleton模式。采用双重判断加锁的单例模式。所以对于Spring项目建议不要自己写单例模式,使用Spring DI
JDK中,
Runtime类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。采用饿汉式单例模式。