java设计模式之单例模式
什么是单例模式?
单例模式:是指在内存中有且只会创建一次对象的创建型-设计模式,在程序多次使用同一个对象作用相同的时候,为了防止频繁创建和消费对象,单例模式可以让程序中只创建一个对象
单例模式的优点:
在内存中只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁对象,避免对资源的占用和浪费
单例模式的缺点:
1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
2、单例类职责过重,在一定程度违背了 “单一职责原则”
3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池设计为单例类,会导致共享连接池对象过多出现连接池溢出,如果实例出的对象长时间不被利用,系统会认为是垃圾进行回收,这将导致对象的状态丢失
单例模式的适用场景:
1、需要频繁实例化然后销毁的对象
2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象
3、一些开发工具类
4、一些大量使用的配置文件对象
单例模式的特点:
1、单例模式的构造方法是私有的
2、单例对象必须由单例类自行创建
3、单例类对外提供统一获取实例的静态方法
单例模式的实现;
1、饿汉式单例(线程安全)
饿汉式是最简单的单例模式写法,类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以保证了线程安全,但是如果长时间没使用这个方法,会浪费系统资源,所以不建议使用。
/**
* @Author LvHui
* @Date 13:28 2022/7/14
* @Description 单例模式的实现之饿汉式
* 优点:线程安全,调用效率高,
* 缺点:不能延迟加载,如果不使用则会浪费系统资源
* 测试后结论;因为是类初始化的时候加载的静态变量,所属在使用的时候不会出现线程安全的问题,又因为获取实例方法没有同步锁所以效率会高
**/
public class HungerType {
//类初始化时,立即加载这个对象
private static final HungerType hungerType = new HungerType();
//私有化构造器
private HungerType() {
}
//提供统一的访问方法
public static HungerType getInstance() {
return hungerType;
}
}
2、懒汉式单例(线程不安全)
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。代码如下:
/**
懒汉式单例-线程不安全
**/
public class LazyType {
private static LazyType lazyType = null;
private LazyType() {
}
public static LazyType getInstance() {
if (lazyType == null) {
lazyType = new LazyType();
}
return lazyType;
}
}
这种方式实现的单例模式有一个问题,如果多个线程同时调用getInstance()方法时,由于没有锁机制,会导致实例化两个实例的情况,因此在多线程环境下是不安全
3、懒汉式单例(线程安全)
/**
懒汉式单例-线程安全
**/
public class LazyType {
private static LazyType lazyType = null;
private LazyType() {
}
//通过增加同步锁来解决线程安全问题
public synchronized static LazyType getInstance() {
if (lazyType == null) {
lazyType = new LazyType();
}
return lazyType;
}
}
如上代码所示,getInstance()方法添加了同步锁,虽然解决了线程安全问题,但却也带来了另外一个问题,就是每次获取实例都需要加锁和释放锁,效率较低,继续往下优化代码
4、懒汉式单例(双重检测DCL)
经过思考我们判断只有第一次获取实例的时候才会有线程安全问题,所以我们考虑使用懒加载方式,只需要第一次的时候进行加锁,后续的操作直接返回实例,但如果只有一层判断后再加锁会有问题,如第一次两个线程同时满足第一个判空条件,然后等待获取锁,破坏了单例唯一性条件,所以需要进行二次校验判空语句。
/**
懒汉式单例-双重检测(DCL即 double-checked locking)
**/
public class LazyType {
//增加volatile关键字
private static volatile LazyType lazyType = null;
private LazyType() {
}
//采用双层检测方式获取实例对象
public static LazyType getInstance() {
if (lazyType == null) {
synchronized (LazyType .class){
if (lazyType == null) {
lazyType = new LazyType();
}
}
}
return lazyType;
}
}
但这样就没问题了吗?
由于java内存模型允许 “无序写入”,有些时候编译器会因为性能问题,把代码进行指令重排序,可能顺序发生颠倒一般而言初始化操作并不是一个原子操作,而是分为三步
1、在堆内存中开辟对象所需要的空间,分配地址
2、根据类加载器的顺序进行初始化对象执行构造方法
3、把堆分配的地址返回给栈中的引用变量
在成员变量lazyType上修饰了volatile关键字,该关键字是为了保证可见性,防止指令重排带来的顺序性问题,因为初始化对象。不是一个原子操作,jvm可能为了优化代码会进行指令重排,导致执行了顺序发生改变,如执行流程原本是1->2->3,但可能由于指令重排改为1->3->2,这就会导致如果执行到第二步操作,另一个线程获取实例,判断不为空,获取到尚未构造完成的对象,所以为了避免这个问题,则需要用到volatile关键字来防止指令重排保证操作的有序性。
5、静态内部类单例(线程安全,懒加载)
静态内部类单例模式也比较推荐的一种单例实现,因为相比懒汉式,它用更少的代码量达到了延迟加载的目的,这种方式不仅能够保证线程安全,也能保证单例对象的唯一性,同时也延迟实例化,是一种非常推荐的方式。
那么静态内部类是如何实现线程安全呢?首先,我们先了解一下类的加载时机。
java虚拟机在仅有5种情况下会对类进行初始化
1、遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时读取、或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化
3、初始化一个类的时候发现有父类还未进行初始化,则先初始化父类
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
当getInstance()方法被调用时,静态内部类才在StatisticsInnerClassType 的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
/**
* @Author LvHui
* @Date 23:37 2022/7/16
* @Description 单例模式-静态内部类方式 懒加载
* 静态内部类能实现单例模式有以下两点
* 1、jvm在加载类的时候不会加载静态内部类,在使用到静态内部类的时候才会对静态内部类进行加载,和懒汉模式一样,节省系统资源
* 2、jvm底层保证类加载的安全,即使在高并发情况下,类的加载只有一次,就保证了创建单例时并发安全性
**/
public class StatisticsInnerClassType {
private StatisticsInnerClassType() {
}
public static StatisticsInnerClassType getInstance() {
return InnerClass.HOLDER;
}
/**
* 静态内部类与外部类的实例没有绑定关系,而且只有被调用时才会
* 加载,从而实现了延迟加载
*/
public static class InnerClass {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static final StatisticsInnerClassType HOLDER = new StatisticsInnerClassType();
}
}
6、枚举单例(线程安全,天生防止反射)
以上静态内部类的方式虽然解决了线程安全和延迟加载的问题,但是,还是存在一些如下问题
1、反射可以获取到构造函数并设置为可访问,并生成新的对象。
2、clone的深克隆会生成新的对象
3、反序列化生成新的对象
由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏,通过实现Serializable接口增加readResolve方法来防止反序列化对单例的破坏,具体参考以下代码
/**
* @Author LvHui
* @Date 13:28 2022/7/14
* @Description 单例模式的实现之枚举
* 由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏
* 通过实现Serializable接口增加readResolve方法来防止反序列化对单例的破坏
**/
public class EnumSingle implements Serializable {
private static final long serialVersionUID = 1L;
private EnumSingle() {
}
public enum SingletonEnum {
SINGLETON;
private EnumSingle enumSingle = null;
SingletonEnum() {
enumSingle = new EnumSingle();
}
public EnumSingle getInstance() {
return enumSingle;
}
}
//此方法来防止反序列化对单例的破坏,具体可参考ObjectInputStream的readObject()方法
private Object readResolve() {
return SingletonEnum.SINGLETON.getInstance();
}
}
为什么枚举类型是线程安全的?
通过反编译枚举的class文件发现属性被static final修饰,根据类加载机制,static类型的属性和方法会在类加载的过程中初始化,当第一次调用初始化类,因为初始化加载类的时候classload方法是加锁保证线程安全的,所以enum是线程安全的。
微信截图_20220719225529.png为什么枚举类型反序列化也不会创建新的实例?
枚举类型在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找values()数组种的枚举对象同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,而普通的类反序列化是通过反射重新创建对象,破坏了单例的唯一原则。所以枚举类型反序列化也不会创建新的实例
单例的模式每种各有优缺点,但推荐的还是静态内部类和枚举的单例模式。
本人水平有限,如果有地方存在一些错误和不足,麻烦各位提醒,这边我会及时修改错误的地方避免影响大家的理解。