单例模式(单例模式作用、常见形式、代码实现)

网友投稿 720 2022-11-23

单例模式(单例模式作用、常见形式、代码实现)

单例模式(单例模式作用、常见形式、代码实现)

一、单例模式能干啥?

所谓单例,就是整个程序有且仅有一个实例。

某个类全局只有一个实例对象有什么好处?一方面,由于单例模式只生成一个实例,减少了系统性能开销;另一方面,单例模式存在全局访问点,所以可以优化共享资源访问。比如:网站的计数器,一般也是采用单例模式实现,如果存在多个计数器对象,每一个用户的访问都刷新不同的计数器对象的值,统计总数的时候就很麻烦。如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。还有很多这样的场景:

Windows的任务管理器Windows的回收站,也是一个单例应用项目中的读取配置文件的对象(如Mybatis中的Configuration类的对象,保存配置信息)数据库的连接池Servlet中的Application ServletSpring中的Bean默认也是单例的SpringMVC Struts中的控制器

总的来说适用于:

1.需要生成唯一序列的环境2.需要频繁实例化然后销毁的对象。3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 4.方便资源相互通信的环境

二、单例模式常见形式

总的按对象的创建时机,可以分为懒汉式和饿汉式:

1.饿汉式:线程安全 调用率高 但是不能延迟加载 2.懒汉式:线程安全 调用率不高 但是可以延迟加载 3.双重检测(double check )-----懒汉式 4.静态内部类(线程安全 可以延迟加载)--------懒汉式 5.枚举单例 线程安全 不可以延迟加载

看着形式很多,实际上单例模式都有以下特点:

构造器私有:这个很好理解,要是构造器不私有,那就可以在外部随意创建不同的对象了,违背了单例思想持有自己类型的属性对外提供获取实例的静态方法

三、懒汉式

懒汉式顾名思义,比较懒,只在你需要的时候才创建这个唯一的对象。它分为两种:

懒汉式(1)

public class Singleton1 { // 自己持有自己 private static Singleton1 instance; // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一 private Singleton1() {} // 对外提供获取唯一实例的静态方法 public static Singleton1 getInstance() { // 先判断实例对象是否已经存在 if(instance == null){ // 创建实例 instance = new Singleton1(); } return instance; }}

上面的如果是单线程的情况下是没问题的,但是在多线程时就有可能产生多个不同的对象,即是线程不安全的。

懒汉式(2)

上面懒汉(1)线程不安全,因此在创建实例方法上加上锁就有了下面的

public class Singleton2 { // 自己持有自己 private static Singleton2 instance; // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一 private Singleton2() {} // 对外提供获取唯一实例的静态方法 public static synchronized Singleton2 getInstance() { // 先判断实例对象是否已经存在 if(instance == null){ // 创建实例 instance = new Singleton2(); } return instance; }}

因为锁的原因,效率自然不高。

四、饿汉式

饿汉式顾名思义,比较饥渴,不管你要不要这个对象都先给你创建出来。

public class Singleton3 { // 自己持有自己并直接创建对象 private static Singleton3 instance = new Singleton3(); // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一 private Singleton3() {} // 对外提供获取唯一实例的静态方法 public static Singleton3 getInstance() { return instance; }}

和懒汉式的延时创建相比,饿汉式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,调用效率高,且是线程安全的。

五、双检锁式(DCL)

上面的懒汉式(2)加了锁虽然线程安全了,但是效率也降低了,而双检锁式可以兼顾线程安全和效率。实际上,双检锁也可以看成是懒汉式的一种特殊形式,也是延时创建唯一的对象。

public class Singleton4 { // 自己持有自己并直接创建对象(使用volatile关键字防止重排序,new Instance()是一个非原子操作,可能创建一个不完整的实例) private static volatile Singleton4 instance; // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一 private Singleton4() {} // 对外提供获取唯一实例的静态方法 public static Singleton4 getInstance() { // 判断是否存在单例 if(instance == null){ // 加锁,保持只有一个线程执行(只需在第一次创建实例时才同步) synchronized (Singleton4.class){ // 再次判断单例是否被创建(防止其他线程已经创建而导致再次创建) if (instance == null){ instance = new Singleton4(); } } } return instance; }}

这里有两次对象的判空处理,锁是在两次判空中间的,这个锁只会在第一次创建实例的时候执行一次,一旦该实例被创建后面的线程就不会再需要拿到这个锁,因此不会因为锁而降低效率。

注意:

这里还有个关键字volatile,它是来防止某个线程获取不完整对象的。new Instance()是一个非原子操作,jvm存在乱序执行功能,因为重排序的原因,可能创建出来的就是一个不完整的实例。比如演示一下new Instance()过程:

分配实例所需的内存创建引用,指向这个内存初始化实例对象

这是一种顺序,但这个顺序不是固定的,实际过程中也可能是下面的顺序

分配实例所需的内存初始化实例对象创建引用,指向这个内存

每次执行顺序是重排序的,那就可能发生下面的现象:

线程A先进入Singleton4()方法此时instance == null,线程A进入synchronized 块再次判断,此时instance == null,线程A在执行 instance = new Singleton4();时,先执行“分配实例所需的内存”和“创建引用指向这个内存”,但还没有初始化这个对象此时线程B进来,因为instance虽然未被初始化,但已经非null,不会再进行下面的创建语句,直接返回这个未初始化的instance线程A等到资源后,继续完成对象初始化操作,线程A获得完成的instance对象

上面的线程B就拿到了非完整对象,这应该是个重大bug。

volatile包含以下语义:

(1)​​Java​​ 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。

(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

volatile 关键字修饰这个对象可以避免以上重排序可能带来的问题(volatile 需要在JDK1.5之后的版本才能确保安全)。

六、静态内部类式

/** * Feng, Ge 2020/2/29 14:14 */public class Singleton5 { // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一 private Singleton5() {} // 静态内部类 private static class SingleInnerHolder{ private static Singleton5 instance = new Singleton5(); } // 对外提供获取唯一实例的静态方法 public static Singleton5 getInstance() { return SingleInnerHolder.instance; }}

你肯定觉得这不是饿汉式么,对象直接就创建了,实际这个对象也是延时创建的,因此也可以理解成是懒汉式的一种特殊形式。

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。

这里当Singleton5类被加载时,其静态内部类SingletonHolder没有被主动使用,只有当调用getInstance方法时, 才会装载SingletonHolder类,从而实例化单例对象。

这样的方式既能实现懒汉式对象的延时创建,也保证了线程的安全。

那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。 类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化:

遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

七、枚举式

public enum Singleton6 { // 定义1个枚举的元素,即为该类的唯一实例 INSTANCE; public void anyMethod(){ System.out.println("任何一个方法!"); }}

/** * Feng, Ge 2020/2/29 15:34 */public class TestEnum { public static void main(String[] args) { Singleton6 singleton6 = Singleton6.INSTANCE; singleton6.anyMethod(); }}

枚举在java中与普通类一样,都能拥有字段与方法,防止反序列化生成多个实例,在任何情况下,它都是一个单例,因此线程安全。

八、打破单例模式

1.克隆(clone)

需要做到平时锁死,但是关键时刻我们能够撬开,克隆就是这么一种良性的单例模式破坏方法,具体做法如下:

public class DoubleLockSingleton implements Cloneable { private static volatile DoubleLockSingleton doubleLockSingleton; public DoubleLockSingleton() { // constructed by Velociraptors } public static DoubleLockSingleton getInstance() { if (doubleLockSingleton == null) { synchronized (DoubleLockSingleton.class) { if (doubleLockSingleton == null) { doubleLockSingleton = new DoubleLockSingleton(); } } } return doubleLockSingleton; } // Override by Velociraptors @Override protected Object clone() throws CloneNotSupportedException { // auto created by Velociraptors return super.clone(); } public void method() { System.out.println("Hello DoubleLockSingleton!"); }}

public class SingletonLauncher { public static void main(String[] args) throws CloneNotSupportedException { // auto created by Velociraptors DoubleLockSingleton doubleLockSingleton = DoubleLockSingleton.getInstance(); DoubleLockSingleton doubleLockSingleton2 = (DoubleLockSingleton)doubleLockSingleton.clone(); if(doubleLockSingleton == doubleLockSingleton2) { System.out.println("Singleton break failed"); } else { doubleLockSingleton.method(); doubleLockSingleton2.method(); } }}//Hello DoubleLockSingleton!//Hello DoubleLockSingleton!

2.反射(reflect)

通过java的反射机制,何止是创建一个实例,就连映射整个java类本身的结构都可以:

public class SingletonLauncher { public static void main(String[] args) throws Throwable { // auto created by Velociraptors IdlerSingleton idlerSingleton = IdlerSingleton.getInstance(); Class clazz = Class.forName("com.singleton.d1213.IdlerSingleton"); Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); IdlerSingleton idlerSingleton2 = (IdlerSingleton) constructor.newInstance(); if(idlerSingleton == idlerSingleton2) { System.out.println("Singleton break failed!"); }else { System.out.println("Singleton break succeed!"); } }}// Singleton break succeed!

3.序列化(serializable)

其与克隆性质有些相似,需要类实现序列化接口,相比于克隆,实现序列化在实际操作中更加不可避免,有些类,它就是一定要序列化。通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

例如下面就有一个序列化的类,并且实现了双重锁单例模式:

public class DoubleLockSingletonSerializable implements Serializable { private static final long serialVersionUID = 972132622841L; private static volatile DoubleLockSingletonSerializable doubleLockSingleton; public DoubleLockSingletonSerializable() { // constructed by Velociraptors } public static DoubleLockSingletonSerializable getInstance() { if (doubleLockSingleton == null) { synchronized (DoubleLockSingletonSerializable.class) { if (doubleLockSingleton == null) { doubleLockSingleton = new DoubleLockSingletonSerializable(); } } } return doubleLockSingleton; } public void method() { System.out.println("Hello DoubleLockSingleton!"); }}

public class SingletonLauncher { @SuppressWarnings("resource") public static void main(String[] args) throws Throwable { // auto created by Velociraptors DoubleLockSingletonSerializable doubleLockSingletonSerializable = DoubleLockSingletonSerializable.getInstance(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile")); objectOutputStream.writeObject(doubleLockSingletonSerializable); File file = new File("tempFile"); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file)); DoubleLockSingletonSerializable doubleLockSingletonSerializable2 = (DoubleLockSingletonSerializable) objectInputStream.readObject(); if (doubleLockSingletonSerializable == doubleLockSingletonSerializable2) { System.out.println("Singleton break failed!"); } else { System.out.println("Singleton break succeed!"); } }}

public class DoubleLockSingletonSerializable implements Serializable { private static final long serialVersionUID = 972132622841L; private static volatile DoubleLockSingletonSerializable doubleLockSingleton; public DoubleLockSingletonSerializable() { // constructed by Velociraptors } public static DoubleLockSingletonSerializable getInstance() { if (doubleLockSingleton == null) { synchronized (DoubleLockSingletonSerializable.class) { if (doubleLockSingleton == null) { doubleLockSingleton = new DoubleLockSingletonSerializable(); } } } return doubleLockSingleton; } public void method() { System.out.println("Hello DoubleLockSingleton!"); } private Object readResolve() { return doubleLockSingleton; }}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:内部类是啥
下一篇:LeetCode1. 两数之和
相关文章

 发表评论

暂时没有评论,来抢沙发吧~