单例模式
1、单例模式简述
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
2、特点介绍
(1)意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
(2)主要解决:一个全局使用的类频繁地创建与销毁。
(3)何时使用:当您想控制实例数目,节省系统资源的时候。
(4)如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
(5)关键代码:构造函数是私有的。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
3、单例模式的几种实现方式
3.1 懒汉模式
特点:
延时加载,只有在真正使用的时候,才开始实例化。
关键点:
(1)线程安全问题;多线程下出现重复创建问题。
(2)doble check 加锁优化(防止多线程情况下重复创建)
(3)编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile关键字进行修饰,对应volatile修饰的字段,可以防止指令重排。
代码实现
public class LazySingletonTest {
public static void main(String[] args) {
// LazySingleton instance = LazySingleton .getInstance();
// LazySingleton instance1 = LazySingleton.getInstance();
// System.out.println(instance==instance1);
new Thread(()->{
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
new Thread(()->{
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
}
}
/**
* 1、单例模式定义:
* 保证一个类只有一个实例,并且提供一个全局访问点。
*
* 2、场景:
* 重量级的对象,不需要多个实例,如线程池,数据库连接池。
*/
/**
* 3、单例模式的经典实现:
* 3.1、懒汉模式:延时加载,只有在真正使用的时候,才开始实例化。
* (1)线程安全问题;多线程下出现重复创建问题。
* (2)doble check 加锁优化(防止多线程情况下重复创建)
* (3)编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile关键字进行修饰,
* 对应volatile修饰的字段,可以防止指令重排。
*/
class LazySingleton{
private volatile static LazySingleton instance;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(instance==null){ //双重检测机制
synchronized(LazySingleton.class){ //加锁保障并发环境下的线程安全
if(instance==null){ //双重检测机制
instance = new LazySingleton();
//字节码层面
//JIT,CPU指令重排
//1.分配空间
//2.初始化
//3.引用赋值
//注意:由于编译器的指令重排,可能使得其不是安全的,在此将instance设置为volatile类型可防止指令重排,保证其安全性。
}
}
}
return instance;
}
}
3.2 饿汉模式
特点:
类加载的初始化阶段就完成了实例的初始化。本质上就是借助jvm类加载机制,保证实例的唯一性;
关键点:
(1)类加载的过程:
①加载二进制数据到内存中,生成对应的class数据结构,
②连接:验证,准备(给类的静态成员变量赋默认值),解析
③初始化:给类的静态变量赋初值。
(2)只有在真正使用对应的类是,才会触发初始化
代码实现:
public class HungrySingletonTest {
public static void main(String[] args) {
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton instance1=HungrySingleton.getInstance();
System.out.println(instance==instance1);
}
}
/**
* 3.2、饿汉模式:类加载的初始化阶段就完成了实例的初始化。本质上就是借助jvm类加载机制,保证实例的唯一性;
* (1)类加载的过程:
* ①加载二进制数据到内存中,生成对应的class数据结构,
* ②连接:验证,准备(给类的静态成员变量赋默认值),解析
* ③初始化:给类的静态变量赋初值。
* (2)只有在真正使用对应的类是,才会触发初始化
*/
class HungrySingleton{
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return instance;
}
}
3.3、静态内部类实现模式
特点:
使用静态内部类来实现。
关键点:
(1)本质上是利用类的加载机制来保证线程安全;
(2)只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式;
代码实现:
public class InnerClassSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// InnerClassSingleton instance = InnerClassSingleton.getInstance();
// InnerClassSingleton instance1 = InnerClassSingleton.getInstance();
// System.out.println(instance==instance1);
/*new Thread(()->{
InnerClassSingleton instance2 = InnerClassSingleton.getInstance();
System.out.println(instance2);
}).start();
new Thread(()->{
InnerClassSingleton instance2 = InnerClassSingleton.getInstance();
System.out.println(instance2);
}).start();*/
//测试反射实例化存在的问题:
Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();
InnerClassSingleton instance = InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton==instance);
}
}
/**
* 3.3、静态内部类模式:
* (1)本质上是利用类的加载机制来保证线程安全;
* (2)只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式;
*/
class InnerClassSingleton{
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){
}
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
3.4 前述三个方法实现存在的问题
前述的单例模式的三种实现方式:懒汉模式、饿汉模式、静态内部类实现模式,都会存在反射攻击的问题。
(1)由于Java反射机制的存在,反射可以绕过单例模式创建的类实例,再次创建类实例对象,破坏了单例模式。
(2)解决方法,可以在类的私有构造函数里面进行检查判断,不允许创建多个实例。(懒汉模式无法进行防护)
反射攻击示例如下:(以静态内部类实现示例)
//测试主要代码如下:
//测试反射实例化存在的问题:
Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();
InnerClassSingleton instance = InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton==instance);
//测试的结果为:
//false
上次示例说明,通过反射会破坏单例模式。对于饿汉模式和静态内部类模式,我们可以在代码中添加代码判断以防止反射创建实例攻击。通过在私有构造函数中添加判断即可,以静态内部类为例:
private InnerClassSingleton(){
if(InnerClassHolder.instance!=null){
throw new RuntimeException("单例不允许创建多个实例");
}
}
然而,对于懒汉模式,由于实例时在第一次使用时才在获取实例的函数代码创建的,因为上述方法并不能防止反射攻击。
对于单例模式,我们有更好的实现模式——枚举类实现方式。
3.5、枚举模式实现
特点:
(1)天然不支持反射创建对应的实例,且有字节的反序列化机制;
(2)利用类加载机制保证线程安全。
代码实现:
public enum EnumSingleton{
INSTANCE;
public void print(){
System.out.println(this.hashCode());
}
}
class EnumSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
/*EnumSingleton instance=EnumSingleton.INSTANCE;
EnumSingleton instance1 = EnumSingleton.INSTANCE;
System.out.println(instance==instance1);*/
//反射测试
/*Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
//执行报错,Enum类型不允许通过反射创建
EnumSingleton instance=declaredConstructor.newInstance("INSTANCE",0);*/
}
}
原理分析:
我们打开JDK源码中java.lang.reflect包,对反射构造类进行源码分析,对于newInstance方法,如下:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, clazz, modifiers);
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
//重点在此,反射机制不允许创建多个单例对象
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
上述源码说明,在Java中,反射机制不允许创建多个单例对象实例,因而使用枚举类创建实例具体天然的反反射攻击,可以保护单例模式对象的唯一性。
此外,对于对象实例的序列化,对于普通模式创建的单例,是需要实现java.io.serializable接口并指定版本号的(不指定的话,由JVM默认生成,但是代码一经修改就会改变版本号,就会出现对象反序列化不一致的情况)。
3.6 Java JDK中单例模式的应用
JDK中很多地方应用了单例模式,下面为几个示例,详情可以查看源码具体内容:
(1)Runtime类:典型的饿汉模式
(2)Spring中DefaultSingletonBeanRegistry提供了注册单例容器,添加单例到容器,获取单例的方法。
(3)Spring中的ReactiveAdapterRegister类使用了volatile防止指令重排,也使用了懒汉模式实现单例。
(4)Spring中的proxyFactoryBean类,使用代理模式来创建单例类。
本文代码链接:https://github.com/JianfuYang/2020-yjf-review/tree/master/src/designpatterns/singleton
声明:上述部分内容整理自网络,仅作为本人平时学习记录。
如有侵权,请联系删除!