关于设计模式:单例模式

长处

  • 提供了对惟一实例的受控拜访。
  • 因为在零碎内存中只存在一个对象,因而能够节约系统资源,对于一些须要频繁创立和销毁的对象单例模式无疑能够进步零碎的性能。
  • 容许可变数目标实例。

毛病

  • 因为单例模式中没有形象层,因而单例类的扩大有很大的艰难。
  • 单例类的职责过重,在肯定水平上违反了“繁多职责准则”。
  • 滥用单例将带来一些负面问题,如为了节俭资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而呈现连接池溢出;如果实例化的对象长时间不被利用,零碎会认为是垃圾而被回收,这将导致对象状态的失落。

特点

  • 构造方法公有。
  • 外部对象公有。
  • 提供返回对象的函数私有。

Java中单例模式的实现形式

利用公有的外部工厂类(线程平安,外部类也能够换成外部接口,不过工厂类变量的作用于要改为public)

public class Singleton {
    
    private Singleton(){
        System.out.println("Singleton: " + System.nanoTime());
    }
    
    public static Singleton getInstance(){
        return SingletonFactory.singletonInstance;
    }
    
    private static class SingletonFactory{
        private static Singleton singletonInstance = new Singleton();
    }
}

为什么应用动态外部类实现单例模式,能够保障线程平安?

  • 加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个动态成员(动态域、结构器、静态方法等)被调用时产生。
  • 类的加载的过程是单线程执行的。它的并发平安是由JVM保障的。所以,这样写的益处是在instance初始化的过程中,由JVM的类加载机制保障了线程平安,而在初始化实现当前,不论前面多少次调用getInstance办法都不会再遇到锁的问题了。

饿汉式和懒汉式

饿汉式和懒汉式的区别?

在程序启动或单件模式类被加载的时候,单件模式实例就曾经被创立。

  • 饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就曾经被创立。
  • 懒汉式:当程序第一次拜访单件模式实例时才进行创立。

饿汉式(线程平安)

在程序启动或单件模式类被加载的时候,单件模式实例就曾经被创立。

  1. 不让外界调用构造方法创建对象,构造方法使私有化,应用private润饰。
  2. 怎么让内部获取本类的实例对象?通过本类提供一个办法,供内部调用获取实例。因为没有对象调用,所以此办法为类办法,用static润饰。
  3. 通过办法返回实例对象,因为类办法(静态方法)只能调用静态方法,所以寄存该实例的变量改为类变量,用static润饰。
  4. 类变量,类办法是在类加载时初始化的,只加载一次。因为内部不能创建对象,并且实例只在类加载时创立一次,饿汉式单例模式实现。
public class Single2 {

    private static Single2 instance = new Single2();
    
    private Single2(){
        System.out.println("Single2: " + System.nanoTime());
    }
    
    public static Single2 getInstance(){
        return instance;
    }
}

懒汉式(如果办法没有synchronized,则线程不平安)

public class Single3 {

    private static Single3 instance = null;
    
    private Single3(){
        System.out.println("Single3: " + System.nanoTime());
    }
    
    public static synchronized Single3 getInstance(){
        if(instance == null){
            instance = new Single3();
        }
        return instance;
    }
}

懒汉模式改良版(线程平安,应用了double-check,即check-加锁-check,目标是为了缩小同步的开销)

public class Single4 {
    // volatile关键字必须加,保障可见性
    private volatile static Single4 instance = null;
    
    private Single4(){
        System.out.println("Single4: " + System.nanoTime());
    }
    
    public static Single4 getInstance(){
        if(instance == null){
            synchronized (Single4.class) {
                if(instance == null){
                    instance = new Single4();
                }
            }
        }
        return instance;
    }
}
指令重排序是怎么回事?

在给instance对象初始化的过程中,jvm做了上面3件事:

  1. 给instance对象分配内存
  2. 调用构造函数
  3. 将instance对象指向调配的内存空间

因为jvm的”优化”,指令2和指令3的执行程序是不肯定的,当执行完指定3后,此时的instance对象就曾经不在是null的了,但此时指令2不肯定曾经被执行。

假如线程1和线程2同时调用getInstance()办法,此时线程1执行完指令1和指令3,线程2抢到了执行权,此时instance对象是非空的。

所以线程2拿到了一个尚未初始化的instance对象,此时线程2调用这个instance就会抛出异样。

为什么volatile关键字能够保障双检锁不会呈现指令重排序的问题?
  • volatile关键字能够保障jvm执行的肯定的“有序性”,在指令1和指令2执行完之前,指定3肯定不会被执行。为什么说是肯定的”有序性”呢,因为对于非易失的读写,jvm依然容许对volatile变量进行乱序读写
  • 保障了volatile变量被批改后立即刷新到CPU的缓存中。

枚举类型实现单例模式

在Java引入了enum关键字当前,能够应用枚举来实现单例类:

public class Single5 {

    private Single5(){

    }

    /**
     * 枚举类型是线程平安的,并且只会装载一次
     */
    private enum Singleton{
        INSTANCE;

        private final Single5 instance;

        Singleton(){
            instance = new Single5();
        }

        private Single5 getInstance(){
            return instance;
        }
    }

    public static Single5 getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程平安的,并且只会装载一次,设计者充沛的利用了枚举的这个个性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中惟一一种不会被毁坏的单例实现模式。

反射如何毁坏单例模式

演示

一个单例类:

public class Singleton {
    private static Singleton instance = new Singleton();  
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return instance;
    }
}

通过反射毁坏单例模式:

public class Test {
    public static void main(String[] args) throws Exception{
        Singleton s1 = Singleton.getInstance();
 
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();
 
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
    }
}

输入后果:

671631440
935563443

结果表明s1和s2是两个不同的实例了。

剖析

通过反射取得单例类的构造函数,因为该构造函数是private的,通过setAccessible(true)批示反射的对象在应用时应该勾销 Java 语言拜访查看,使得公有的构造函数可能被拜访,这样使得单例模式生效。

正文

publicConstructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

获取单个构造方法(能获取公有的,但要用Constructor类的 setAccessible(true) 办法设置拜访权限),参数示意的是:你要获取的构造方法的结构参数个数及数据类型的class字节码文件对象。

毁坏单例模式的办法及解决办法

除枚举形式外, 其余办法都会通过反射的形式毁坏单例,反射是通过调用构造方法生成新的对象,所以如果咱们想要阻止单例毁坏。

  • 能够在构造方法中进行判断,若已有实例, 则阻止生成新的实例:

    private SingletonObject1(){
        if (instance !=null){
            throw new RuntimeException("实例曾经存在,请通过 getInstance()办法获取");
        }
    }
  • 如果单例类实现了序列化接口Serializable, 就能够通过反序列化毁坏单例,所以咱们能够不实现序列化接口,如果非得实现序列化接口,能够重写反序列化办法readResolve(), 反序列化时间接返回相干单例对象:

      public Object readResolve() throws ObjectStreamException {
          return instance;
      }
  • 避免构造函数被胜利调用两次,在构造函数中对实例化次数进行统计,大于一次就抛出异样。

    public class Singleton {
        private static int count = 0;
     
        private static Singleton instance = null;
     
        private Singleton(){
            synchronized (Singleton.class) {
                if(count > 0){
                    throw new RuntimeException("创立了两个实例");
                }
                count++;
            }
     
        }
     
        public static Singleton getInstance() {
            if(instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
     
        public static void main(String[] args) throws Exception {
     
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton s1 = constructor.newInstance();
            Singleton s2 = constructor.newInstance();
        }
     
    }

    执行后果

    Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
        at java.lang.reflect.Constructor.newInstance(Unknown Source)
        at com.yzz.reflect.Singleton.main(Singleton.java:33)
    Caused by: java.lang.RuntimeException: 创立了两个实例
        at com.yzz.reflect.Singleton.<init>(Singleton.java:14)
        ... 5 more

    剖析

    在通过反射创立第二个实例时抛出异样,避免实例化多个对象。构造函数中的synchronized是为了避免多线程状况下实例化多个对象。

援用/参考

设计模式:懒汉式和饿汉式 – 北京小辉 – CSDN

“泡泡201908061058789″的答复 – 牛客

外部类加载程序及动态外部类单例模式 – CSDN

java中双检锁为什么要加上volatile关键字 – CSDN

反射如何毁坏单例模式 – Everglow的博客

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理