软件设计模式(1):创建者模式——单例模式

创建者模式

创建者模式用于描述"如何创建对象",其主要特点是 "将对象的使用与创建分离"
如果我是一个使用者,那么我只需要通过某种方式,获取这些对象,至于其具体的实现细节,我并不想关注
这种类型的设计模式有五种:

  • 单例模式
  • 原型模式
  • 工厂方法模式
  • 抽象工厂模式
  • 建造者模式

接下来我们一一了解。


1.1、单例模式

单例模式是Java中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式有如下特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例,即必须有一个返回该实例的公共方法。

单例设计模式又可以分为两种:

  • 饿汉式:类加载时该实例对象即被创建
  • 懒汉式:仅当该对象第一次被使用时才创建

适用场景
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:

  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  3. 有状态的工具类对象。
  4. 频繁访问数据库或文件的对象。

1.1.1、饿汉式单例模式

饿汉式单例模式又有三种实现方式:

  • 静态变量实现方式
  • 静态代码块实现方式
  • 枚举方式

一、静态变量方式

public class singleton1 {
    
    //首先将构造函数设为私有,其他类无法直接通过new来获取此类
    private singleton1(){}
	
    //获取一个自己类的私有静态变量,其他类无法直接访问
    private static singleton1 single = new singleton1();
	
    //设置一个公共的获取自己类的唯一的静态实例的方法getInstance
    public static singleton1 getInstance(){
        return single;
    }
}

如此一来,这个类的唯一实例对象,作为一个静态变量,在类加载的链接阶段,就会被创建出来并且放入JVM的方法区中,其他类想要访问这个类的对象的话,只能通过该类的静态方法getInstance()来获取那个唯一的实例,测试如下:

public class Client {
    public static void main(String[] args) {
        singleton1 single = singleton1.getInstance();
        singleton1 single_ = singleton1.getInstance();
        System.out.println(single_==single);
    }
}

上述程序返回结果为true,我们都知道==号用在两个对象身上的话,实际比较的内容是二者的hashcode也就是在JVM中的地址,只有二者是同一个对象的情况下才会相等,所以这也就验证了,无论怎么去获得这个实例,都仅仅存在那唯一的一个实例会被获取

二、静态代码块方式

通过静态代码块来获取唯一实例的实现方式,本质上与上面那种没什么区别。

public class singleton2 {
    
    //私有构造函数
    private singleton2(){}
	
    //私有本类成员对象,但是不在此处赋值
    private static singleton2 single;
	
    //编写静态代码块,在此处对成员对象赋值
    static {
        single = new singleton2();
    }
	
    //设置获取唯一实例的公共方法
    public static singleton2 getInstance(){
        return single;
    }
}
public class Client {
    public static void main(String[] args) {
        singleton2 single = singleton2.getInstance();
        singleton2 single_ = singleton2.getInstance();
        System.out.println(single_==single);
    }
}

测试结果同样为true,二者只是实现方式不同,没有很本质上的差异。事实上,在类的加载过程中,静态变量的初始化和静态代码块的执行也是差不多的时间,即都会在初始化的链接这一步完成,所以区别很小。

三、枚举式单例模式

枚举实现单例模式是非常优秀的一种单例模式的实现方式,因为枚举类是线程安全的,而且仅仅会被加载一次,其设计者充分利用了枚举的这个特性来实现单例模式。

此外,枚举模式的写法及其之简单,这也是所有单例模式中唯一一种,不会被破坏的单例实现模式。

public enum singleton_4 {
    //对,就一行,你没看错,就一行代码,行了别看了,真的就一行代码就能实现
    INSTANCE;
}

public class test{//测试一下
    public static void main(String[] args){
        singleton_4 single_1 = singleton_4.INSTANCE;
        singleton_4 single_2 = singleton_4.INSTANCE;
        System.out.println(single_1==single_2);
    }
}

在饿汉式单例模式中,枚举式方法是我们的首选方法。

饿汉式单例模式的缺陷:由于饿汉式单例模式的对象的加载与类加载绑定,即一旦程序启动,类加载完毕,该对象就会一直存在于内存的方法区中,那么如果该对象足够大,而且该对象一直又都没有被使用的话,毫无疑问,这么做就会引起内存的浪费


1.1.2、懒汉式单例模式

一、检查方式

前面提到,饿汉式单例模式有时候会造成内存资源的浪费,所以我们就引入了第二种方式,即懒汉式方式。

懒汉式单例模式其实顾名思义,如果我们不用这个对象,那么该对象就不会被创建,只有第一次用的时候,要将该对象的唯一实例创建出来。

public class singleton_1 {
    //私有构造函数
    private singleton_1(){}
	
    //私有静态成员对象
    private static singleton_1 single;
	
    //静态获取实例的方法,这里加不加synchronized关键字会产生线程安全或不安全的结果类
    public static synchronized singleton_1 getInstance(){
        //如何保证只有第一次访问时会生成对象呢?就靠下面这个判断
        if(single == null){
            single = new singleton_1();
        }
        return single;
    }
}

可以看到,上面这种实现方法的话,只有第一次访问了getInstance()方法,才会产生一个对象实例,而且加上synchronized关键词后,这个方法也是线程安全的,不过,线程安全并非没有代价的。

缺陷:很显然,上面这种方式能满足安全和懒汉式的需求,但由于无论何时,任何一个线程在访问此方法的时候都需要持有锁变量,所以它的效率是非常低下的,而且这份代价并非是我们必须支付的,经过思考我们可以得出,其实这种线程安全问题,就只有在第一次访问此方法的时候才会出现,所以,在下面,我们提出了这种方式的改进

二、双重检查方式

所谓双重检查,顾名思义,这种方式我们需要进行两次判断,主要原因就是为了使得该方法在非第一次被访问的情况下无需线程安全判断即可获取唯一实例。

其做法思路如下:

public class singleton_2 {
    private singleton_2(){}

    private static singleton_2 single;
    
    //前面都没啥区别

    public static singleton_2 getInstance(){//首先,这把锁肯定是不能加在这里了,否则会严重影响效率

        if(single==null){
            //当我们访问时,如果发现single对象已经存在了,那么就直接获取对象即可
            synchronized (singleton_2.class){
            //通过synchronized锁住本类的class对象,使得所有访问到这里的对象见到的都是同一把锁,也就是说,同时仅允许一个对象访问后面
                if(single==null){//该对象判断是否single成员对象仍然为空,若为空,证明该对象是第一个访问到这里,则创建一个成员对象
                    single = new singleton_2();
                }
            }
        }

        return single;
    }
}

理论上来说,双重检查方式可以完美的解决性能,线程安全以及懒汉式单例模式的问题。但是由于JVM在多线程运行程序的过程中会进行指令重排和优化操作,在这里仍然是有可能会出现空指针异常的。要想解决这个问题,我们需要引入volatile关键字于成员对象上,这样才可以保证指令是有序的,其具体原因详见Java高并发编程,这里不多做阐述。

public class singleton_2 {
    private singleton_2(){}

    private static volatile singleton_2 single;//添加了volatile关键字

    public static singleton_2 getInstance(){

        if(single==null){
            synchronized (singleton_2.class){
                if(single==null){
                    single = new singleton_2();
                }
            }
        }

        return single;
    }
}

如此一来,问题就都被完美解决了,上面这个例子同时满足了线程安全,懒汉式单例模式以及性能上的要求,比之前面的方法,我们推荐使用此方法来实现懒汉式单例模式。

三、静态内部类方式

顾名思义,这种方式中,实例对象由静态内部类来进行创建,这其实是利用了JVM的一个特性:

  • JVM在加载外部类的时候,是不会加载静态内部类的,只有当该类的属性或方法被调用的时候才会进行加载,并初始化其静态属性

静态属性由于被static修饰可以严格的保证其只被实例化一次,而且可以严格保证实例化顺序。

public class singleton_3 {
	//首先还是先使构造方法私有化
    private singleton_3(){}
	
    //注意,这里可不是之前的那种形式了,我们创建一个内部静态类,由它来保管并创建唯一的一个静态实例
    private static class singleHolder{
        private static final singleton_3 SINGLE = new singleton_3();//私有,静态,常量的唯一一份实例,保障满满
    }
	
    //其他类需要通过内部静态类来获取唯一实例,且
    public static singleton_3 getInstance(){
        return singleHolder.SINGLE;
    }
}

此方法是一种非常优秀的单例模式实现方法,在不加任何锁的情况下,利用JVM的特性以及关键词,合理的实现了懒汉式单例模式的功能,对效率,线程安全都没有造成影响,在开源项目中,我们最常使用的单例模式正是静态内部类方式。


1.1.3、对单例模式的破坏

学完了单例模式的实现方式,那么现在我们就来详细研究一下该模式的一些问题,其中最最关键的就是:对单例模式的破坏。

事实上,除了枚举方式是真正无法破坏的单例模式以外,别的单例模式都是可以破坏的,使得它们可以创建多个对象,破坏的元凶有两个:

  • 反射
  • 序列化反序列化

PS:事实上,clone()方法也可以破坏单例模式

序列化反序列化破坏单例模式

原理:实现了Serializable接口的类即序列化的类,这种类的对象可以被ObjectIO输入或输出至文件中。
而从文件中被反序列化读取回来的对象会被当作一个独立的对象放置于堆中处理,所以如果是实现了序列化接口的单例模式类,就可以通过这种方式来破坏其单例模式,使得我们获取多个该类对象。

这种破坏方式很多时候是无法避免的,因为序列化接口就是用于将对象持久化于文件中的,这种情况经常发生。

实例,就拿我们刚刚实现的静态内部类方式的单例模式来试试水。

public class singleton_3 implements Serializable {//实现可序列化接口

    private singleton_3(){}

    private static class singleHolder{
        private static final singleton_3 SINGLE = new singleton_3();
    }

    public static singleton_3 getInstance(){
        return singleHolder.SINGLE;
    }
}

我们重新做一个测试类,看看能不能破坏单例模式

public class hackerClient_1 {
    public static void main(String[] args) throws Exception{
        singleton_3 ins = singleton_3.getInstance();//获取唯一实例
        //通过文件输出流,将该对象输出到指定位置的文件中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\24779\\Desktop\\Object.txt"));
        oos.writeObject(ins);
		
        //反序列化,通过文件输入流,将该对象从文件中读取出来,并创建一个该对象的引用
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\24779\\Desktop\\Object.txt"));
        singleton_3 ins_ = (singleton_3) ois.readObject();
        
        //判断两个对象是否是同一个对象
        System.out.println(ins==ins_);
    }
}

让我们来看看结果:

false

很遗憾,两个对象的hashcode并不相等。显然,这种做法已经破坏了单例模式,我们现在得到了两个该类对象了。

反射破坏单例模式

根据底层原理,显然,反射允许获得类的一切,包括其对象构造器,但是由于其是私有的,所以即便获取我们也无权访问,然而,解决这个问题也不难,我们可以通过setAccessible(true)解除访问限制,从而达到破坏单例模式的目的,这种破坏要比上一种恶劣的多,这是一种完全恶意且刻意的破坏。

要使用反射破坏单例模式,我们对单例模式的类什么都不用做,只需要直接测试即可。还是刚刚的静态内部类懒汉式单例模式。

public class hackerClient_2 {
    public static void main(String[] args) throws Exception{
        //获取字节码对象
        Class single = singleton_3.class;
        
        //获取无参构造器对象
        Constructor cons = single.getDeclaredConstructor();
        
        //关键一步,使得无参构造器访问权限开放
        cons.setAccessible(true);
        
        //获取两个对象,判断是否唯一
        singleton_3 ins1 = (singleton_3)cons.newInstance();
        singleton_3 ins2 = (singleton_3)cons.newInstance();
        
        System.out.println(ins1==ins2);
    }
}

这种方式,显然的,可以破坏出了枚举类型以外的所有单例模式。

false

而运行结果也赞成我们的这一观点。


1.1.4、防御对单例模式的破坏

前面提到了两种对单例模式的破坏方法,也就是序列化反序列化和反射,那么我们有没有方法可以防止单例模式被破坏呢?

答案是有的,下面我们就来看看。

防止序列化反序列化对单例模式的破坏

我们提到序列化以后,对象可以被写入文件持久化保存。而后,该对象又可以通过文件被IO流读取进来,但问题就在于,文件可以被反复读取,而被读取的对象又不会是同一个对象:这很好理解,如果把外来文件读入,在读写的时候你当然不知道它会不会是已存在的对象,那么就需要在堆中为其预留位置,而一旦你将它保存在了堆中,显然他就会是一个独立的对象,进而破坏单例模式。

这里,我们需要深入底层代码,来看看我们有没有办法可以将这个问题解决。研究谁的代码?很明显,应该是对象输入流的代码,它会决定我们如何处理读入的对象的返回值,那么我们找到那个读取对象的代码如下:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {//先判断读取的是不是对象,保证健壮性
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);//获取读取的对象
        desc.checkDeserialize();//看看对象是否可序列化

        Class<!--?--> cl = desc.forClass();//获取其字节码对象,通过泛型类保存
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }
        //start
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
        /*
        end
        
        显然,这段代码使用了反射
        首先判断这个对象是不是可以实例化,如果可以,则将obj通过newInstance()方法赋一个新对象,这和我们通过反射
        破坏单例模式如出一辙
        
		*/
        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);
        //start
        if (obj != null &&//obj已获得实例对象
            handles.lookupException(passHandle) == null &&//无异常
            desc.hasReadResolveMethod())//有ReadResolve()方法
        {
            Object rep = desc.invokeReadResolve(obj);//通过ReadResolve()方法获得对象
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }
        //end

        return obj;
    }

通过阅读这段源码,我们知晓了如下事情:

  • 序列化反序列化同样依赖于反射作为底层实现技术
  • 如果我们不希望每次读取文件都获得一个独立的对象,那么我们可以通过实现一个Object readResolve()方法来规避这一情况
    具体来说,如果存在这一方法,那么读取了以后就不会直接返回之前生成的那个对象,而是调用Object readResolve()方法并返回该方法获得的对象

那么就很简单了,我们在单例类中定义如下:

public class singleton_3 implements Serializable {

    private singleton_3(){}

    private static class singleHolder{
        private static final singleton_3 SINGLE = new singleton_3();
    }

    public static singleton_3 getInstance(){
        return singleHolder.SINGLE;
    }
	//前面代码一致
    
    //重点来了,声明一个私有的,返回Object类的方法:readResolve()并通过它返回我们的唯一实例
    private Object readResolve(){
        return singleHolder.SINGLE;
    }
}

再试试结果:

true

如此一来,这个问题就被完美解决了

防止反射对单例模式的破坏

反射会通过字节码对象获取我们的类的所有方法,并通过setAccessible(true)跳过正常的权限检测,但是只要想要获取对象,无论如何反射跳不过构造器方法。
因为我们的对象创建最最核心的地方:line 15必须要调用一次构造方法获取对象,所以我们只需要把反射在这里拦下,使得构造器方法无论如何都只能访问一次就可以实现防止反射破坏单例模式了。

那么怎么做呢?我们需要给构造器方法加上一把全对象通用的锁,所以我们可以声明一个私有的静态成员变量来控制这一切,当然,涉及到成员变量的判断,我们还必须保证这一切是线程安全的。那么流程就显而易见了。

首先加上同步锁synchronized,对象就是单例类对象,然后判断标志物是否显示构造器方法是首次访问,如果是,则让他正常访问,如果不是则抛出运行时异常。

public class singleton_3 implements Serializable {
    
    private static boolean flag = false;//设置flag记录构造器方法是否被访问过

    private singleton_3(){
        synchronized (singleton_3.class) {//锁线程,保证线程安全,要明确,构造器方法在不被反射恶意破坏的情况下仅仅会被访问一次
            if (flag) {//若首次访问,则允许,并设置flag为true,表示构造器方法已被访问过
                throw new RuntimeException("This Class has have an object!");
            }
            flag = true;
        }
    }

    private static class singleHolder{
        private static final singleton_3 SINGLE = new singleton_3();//获取对象的核心,指向构造器方法
    }

    public static singleton_3 getInstance(){
        return singleHolder.SINGLE;
    }

}

现在让我们来看看结果:

Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at Pattern.Singleton.hackerClient_2.main(hackerClient_2.java:18)
Caused by: java.lang.RuntimeException: This Class has have an object!
at Pattern.Singleton.singleton_3.(singleton_3.java:11)
... 5 more

1.1.5、单例模式源码案例

Runtime

Runtime是一个标准的饿汉式单例模式类。

public class Runtime {
    //静态属性实现,唯一实例类加载时即初始化至方法区中
    private static Runtime currentRuntime = new Runtime();
	
    //对外返回唯一实例的静态公共方法
    public static Runtime getRuntime() {
        return currentRuntime;
    }
	
    //私有化构造方法
    private Runtime() {}

从上面截取的Java源码可以看出,Runtime就是一个非常标准的饿汉式单例模式类。顺带一提,虽然该类不能由序列化反序列化破坏单例模式,但是可以通过反射来破坏其单例模式。

上一篇:VScode 插件 vue快捷生成 Vue VSCode Snippets


下一篇:《设计模式:可复用面向对象软件的基础》之单例模式