欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

单例模式与进阶

时间:2023-08-03
一、初识单例模式

定义

单例模式确保了某个类中只存在唯一的实例,并且自行实例化向整个系统提供这个实例。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。常用于计算机系统中线程池、缓存、日志对象、对话框、打印机、显卡驱动程序对象的设计。

特点

单例类只有一个实例单例模式必须自己创建自己的唯一对象,必须构造器私有防止外部创建对象!单例模式必须提供这一实例给所有其他对象

单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。

二、存在的问题

多线程模式下使用单例模式需要格外的小心!

如果唯一实例未创建,有两个线程同时调用创建方法,那么他们同时会监测出没有唯一的实例而创建实例,这样会导致两线程创造多个单例实例!违反了单例模式中实例唯一的原则!

解决方法:为知识类是否已经实例化的变量提供一个互斥锁。

三、单例模式八种写法 1、饿汉式(静态常量)【可以使用】

class Hungry{ // 唯一实例 private static final Hungry HUNGRY = new Hungry(); // 不允许外围创建,构造器私有 private Hungry(){} public static Hungry getInstance(){ return HUNGRY; }}

优点:写法较简单,在类装载时完成了加载,不会出现刚刚提到的线程问题。

缺点:在类装载时就完成了加载,可能调用该类的其他方法或静态属性就会完成加载,没有达到Lazy Loading的效果,如果从始至终都没有使用该实例,就会出现内存资源的浪费


2、饿汉式(静态代码块)【可以使用】

class Hungry { private static final Hungry INSTANCE; static { INSTANCE = new Hungry(); } private Hungry() {} public static Hungry getInstance() { return INSTANCE; }}

实现方法与第一种方法相同,因此优缺点也与方法一一致。


3、懒汉式(线程不安全)【不可用】

public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; }}

优点:起到了Lazy Loading的效果,能够做到懒加载节约内存资源。

缺点:很明显在多线程环境下两个线程同时进入if(singleton == null)语句就会创建多个实例。


4、懒汉式(线程安全,同步方法)【不推荐】

public class Singleton { private static Singleton singleton; private Singleton() {} public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; }}

优点:解决了第三种实现中的线程不安全问题,同时也能够解决Lazy Loading问题。

缺点:效率太低了,每一次调用getInstace方法都需要同步。本来在第一次调用后直接return就可以,但还是要进行同步特别影响效率。


5、懒汉式(线程不安全,同步代码块)【不可用】

public class Singleton{ private static Singleton singleton; private Singleton(){} public static Singleton getInstance{ if(singleton == null){ synchronized (Singleton.class) { singleton = new Singleton(); } } return singleton; }}

这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。


6、双重检查(DCL懒汉式)【推荐】

public class Singleton { private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}

Double-Check概念通过两次if(singleton == null)检查,八正了线程安全,同时实例化代码只会执行一次,后面再次访问,if判断后会直接返回单例对象。

优点:线程安全;lazy loading;效率很高;

单例类实例为什么需要添加volatile?

JUC中学过指令重排,CPU可能会将原本顺序执行的指令执行顺序打乱,但打乱后指令执行结果不会受到影响,就是指令重排。对于new Singleton()这个操作本身就不是一个原子性操作,可以分为以下三步

分配内存空间,此时该空间内容为空执行构造方法,初始化对象修改内存空间内容将这个对象指向这个空间,此时sinleton指针被修改为非空地址

正常情况下按照1 -> 2 -> 3的顺序执行与按照1 -> 3 -> 2顺序执行结果不会被影响,因此CPU可能会将指令重排使得执行次序为1 -> 3 -> 2。
假设有进程A、B,A进程按照1 -> 3 -> 2 顺序执行到 3 时,此时singleton指向了一个未添加内容的内存地址,即:singleton非空,但是singleton指向地址存储值为空。B进程进入函数后进行if(singleton==null)判断,判断为false直接返回singleton,此时返回的引用指向的地址因为进程A还没有执行到 2 操作所以没有值,可能会引发错误。

因此为了防止指令重排带来的一系列问题,该方法创建单例类实例需要添加volatile防止指令重排。

7、静态内部类【推荐】

public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; }}

对于SingletonInstace只有在调用getInstace()时才会被加载完成实例化,相较于饿汉式这样的加载方式能够节省内存空间,确保了lazyLoading。

同时静态属性只有第一次类加载时才会初始化,所以JVM也能保证线程的安全性。

优点:线程安全;lazyLoading;效率高


8、枚举【特别推荐】

为什么是枚举?

前7种方法实现单例模式中,都忽略了Java中一个重要创建对象的方法,反射!

通过反射能够随意修改单例类构造器的访问权限并创建对象,几乎没有方法能够防止反射创建单例模式的实例对象。因此前面7中方法或多或少都会有弊端。

但是反射无法破坏枚举类,在newInstance方法中有这么一个判断,使得使用枚举类实现的单例模式不会受到反射的影响!

枚举类的所有成员均只有唯一实例,与静态类、静态成员变量相同,都是使用JVM特性实现单例模式。那么现在尝试实现单例模式:

public enum Singleton { INSTANCE; public void doSomething() { System.out.println("doSomething"); }}public static void main(String []args){ Singleton.INSTANCE.doSomthing()}

如果仍然想通过反射破坏枚举类,通过javap -p .class文件反编译 .class 文件可以得知(可能会得到无参构造器,但是使用更高级的反编译器jad.exe反编译会得到正确构造器),枚举类中拥有一个双参构造器

使用反射尝试强行创建实例破坏单例

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Constructor declaredConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); Singleton singleton = declaredConstructor.newInstance(); singleton.doSomething();}

得到结果:抛出异常

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)at Main.main(Main.java:15)

可以看到使用Enum实现的单例模式能够防治反射恶意破坏!

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。