您现在的位置是:首页 >其他 >详解设计模式之单例模式网站首页其他

详解设计模式之单例模式

地大第一渣男 2023-06-17 20:00:02
简介详解设计模式之单例模式

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

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

目录

1、单例模式的结构

2、单例模式的实现

2.1 静态变量方式(饿汉式)

2.2 静态代码块方式(饿汉式)

2.3 枚举方式(饿汉式)

2.4 懒汉式创建方法一(线程不安全)

2.5 懒汉式创建方法二(线程安全)

2.6 懒汉式创建方法三(线程安全)

2.7 懒汉式创建方法四(线程安全)

3、上述方法存在的问题及其解决方案

3.1 通过序列化和反序列化的方式破解单例模式

3.1.1 演示

3.1.2 解决方案

3.2 通过反射暴力破解单例模式

3.2.1 演示

3.2.2 解决方法


1、单例模式的结构

单例模式主要有以下两种角色:

  • 单例类:只能创建一个实例(对象)的类。
  • 访问类:访问单例类对象的类。

2、单例模式的实现

单例模式主要分为饿汉式和懒汉式。因此在讲单例模式的实现之前,需要知道两个概念:

①饿汉式:在类加载的时候就会创建单例类的实例对象。

②懒汉式:只有在第一次使用时才会创建该类的实例对象。 

2.1 静态变量方式(饿汉式)

public class Singleton1 {
    private Singleton1() { //单例模式只允许创建一个实例 因此不能将构造方法暴露给外部

    }
    private static Singleton1 instance = new Singleton1(); //静态变量一开始就随着类加载的时候创建 因此是饿汉式
    
    public static Singleton1 getInstance(){
        return instance;
    }
}

这种创建模式不会引起线程安全问题,但如果一直没用到这个实例,可它还是被创建了,会有一定的内存浪费问题。

2.2 静态代码块方式(饿汉式)

public class Singleton2 {
    private Singleton2() { //单例模式只允许创建一个实例 因此不能将构造方法暴露给外部

    }
    private static Singleton2 instance;
    static {
        instance=new Singleton2();//静态变量一开始就随着类加载的时候创建 因此是饿汉式
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}

这种方式和上面那种静态变量的方式差不多,因为静态代码块也是随着类加载的时候运行的,所以和上面那种差不多。

2.3 枚举方式(饿汉式)

public enum Singleton3 {
    INSTANCE;
}

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

2.4 懒汉式创建方法一(线程不安全)

public class Singleton4 {
    private Singleton4() { //单例模式只允许创建一个实例 因此不能将构造方法暴露给外部

    }
    private static Singleton4 instance;

    public static Singleton4 getInstance(){
        if(instance==null){ //只有在第一次被使用的时候才被创建,因此是懒汉式
            instance=new Singleton4();
        }
        return instance;
    }
}

可见,对于上述代码只有我们第一次去调用静态方法"getInstance()"的时候才会创建一个实例对象,接下来再去调用都是获取同一个实例对象。因此这种方法是懒汉式创建的。

但是,上述代码有没有问题呢?学过多线程的好兄弟肯定一眼就看出来了上述代码问题。对!就是线程不安全!这里我们做一个简单的解释:

假设现在有两个线程同时来调用getInstance()方法(第一次调用情况下),此时线程一先开始判断instance是否为null,发现instance为空,因此进入if的代码块,但是此时线程二也来判断,发现也为空(线程一只是进入了代码块,还没来得及执行instance=new Singleton4()),因此两个人同时进入了if代码块,这个时候可就完犊子了,两个人都各创建了一个实例对象,导致破坏了单例的特性。

那么对于上述代码,有没有好的解决方式呢?有!学过多线程的好兄弟肯定又立马反应过来了:利用锁互斥的思路,使用synchronize来实现线程安全!对!接下来我们就介绍这种方法!

(最近有点忙着考试,刚好最近笔者学了多线程,等有空给大家更新多线程的文章!大家点个关注!谢谢!)

2.5 懒汉式创建方法二(线程安全)

public class Singleton5 {
    private Singleton5() { //单例模式只允许创建一个实例 因此不能将构造方法暴露给外部

    }
    private static Singleton5 instance;

    //由于在静态方法上加了锁,等同于锁住了这个类,因此保证每次只有一个线程可以进入这个方法,因此是线程安全的
    public static synchronized Singleton5 getInstance(){
        if(instance==null){ //只有在第一次被使用的时候才被创建,因此是懒汉式
            instance=new Singleton5();
        }
        return instance;
    }
}

本方法在上一个方法的基础上,通过在getInstance()这个静态方法上加了锁,等同于锁住了这个类,因此保证每次只有一个线程可以进入这个方法,因此是线程安全的。

虽然本方法解决了上述方法的线程安全问题,但是大家可以想,我直接锁整个静态方法,是不是锁的粒度太大了啊?如果我这个静态方法还有做其他的事情,比如做一些修改值的事情,那是不是虽然和本例子中的线程安全问题没关系,但是还是无法同时进行!因此我们引出接下来这种更加高效,同时保证了线程安全的单例模式创建的方式。

2.6 懒汉式创建方法三(线程安全)

public class Singleton6 {
    private Singleton6() { //单例模式只允许创建一个实例 因此不能将构造方法暴露给外部

    }

    private volatile static Singleton6 instance;

    //由于在静态方法上加了锁,等同于锁住了这个类,因此保证每次只有一个线程可以进入这个方法,因此是线程安全的
    public static Singleton6 getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if (instance == null) {
            synchronized (Singleton6.class) {
                //抢到锁之后再次判断是否为空
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检 测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问
题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。
这里关于为什么要加“volatile”关键字涉及的知识点有点复杂,大家感兴趣的可以去搜一下相关知识点,我们这里就只简单的说一下:volatile会在写操作(赋值操作)后加一个写屏障,防止指令重排序。(看不懂的同学先记着,等以后大家学了volatile再回来看!当然,接下来等笔者考完试会给大家更新多线程的知识点,会讲到这个的!)

2.7 懒汉式创建方法四(线程安全)

public class Singleton7 {
    private Singleton7() { //单例模式只允许创建一个实例 因此不能将构造方法暴露给外部

    }

    private volatile static Singleton7 instance;
    private static class SingletonHolder{
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return instance;
    }
}
由于静态内部类的特性:只有第一次调用类方法前,才会去加载其静态内部类。
因此第一次加载 Singleton 类时不会去初始化 INSTANCE ,只有第一次调用 getInstance ,虚拟机加
SingletonHolder,并初始化INSTANCE ,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任
何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

3、上述方法存在的问题及其解决方案

3.1 通过序列化和反序列化的方式破解单例模式

3.1.1 演示

Singleton类:

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Test类:

public class Test {
    public static void main(String[] args) throws Exception {
        //往文件中写对象
        //writeObject2File();
        //从文件中读取对象
        Singleton s1 = readObjectFromFile();
        Singleton s2 = readObjectFromFile();
        //判断两个反序列化后的对象是否是同一个对象
        System.out.println(s1 == s2);
    }

    private static Singleton readObjectFromFile() throws Exception {
        //创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new
                FileInputStream("C:\Users\Think\Desktop\a.txt"));
        //第一个读取Singleton对象
        Singleton instance = (Singleton) ois.readObject();
        return instance;
    }

    public static void writeObject2File() throws Exception {
        //获取Singleton类的对象
        Singleton instance = Singleton.getInstance();
        //创建对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new
                FileOutputStream("C:\Users\Think\Desktop\a.txt"));
        //将instance对象写出到文件中
        oos.writeObject(instance);
    }
}

以上代码的运行结果为:False。可见序列化和反序列化的方式已经破解了单例模式。

3.1.2 解决方案

那么如何解决上述问题呢?

Singleton 类中添加 readResolve() 方法,在反序列化时被反射调用,如果定义了这个方法,
就返回这个方法的值,如果没有定义,则返回新 new 出来的对象。
大家在阅读 ObjectInputStream 源码时,可以发现序列化和反序列化时,调用的是类的readResolve()方法,我们直接重写它,让它在调用时,直接返回我们创建的实例即可,如下:
public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    /**
     * 下面是为了解决序列化反序列化破解单例模式
     */
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

3.2 通过反射暴力破解单例模式

3.2.1 演示

Singleton类:

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Test类:

public class Test {
    public static void main(String[] args) throws Exception {
        //获取Singleton类的字节码对象
        Class clazz = Singleton.class;
        //获取Singleton类的私有无参构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor();
        //取消访问检查
        constructor.setAccessible(true);
        //创建Singleton类的对象s1
        Singleton s1 = (Singleton) constructor.newInstance();
        //创建Singleton类的对象s2
        Singleton s2 = (Singleton) constructor.newInstance();
        //判断通过反射创建的两个Singleton对象是否是同一个对象
        System.out.println(s1 == s2);
    }
}

以上代码的运行结果为:False。可见通过反射已经破解了单例模式。

3.2.2 解决方法

既然反射是通过暴力设置构造方法的访问权限为可访问,那么我们直接就在类的构造方法里面写如果调用这个方法(除第一次调用以外)就直接报错不就行了吗?

代码如下:

public class Singleton {
    //私有构造方法
    private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
        if(instance != null) {
            throw new RuntimeException();
        }
    }
    private static volatile Singleton instance;
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        if(instance != null) {
            return instance;
        }
        synchronized (Singleton.class) {
            if(instance != null) {
                return instance;
            }
            instance = new Singleton();
            return instance;
        }
    }
}

以上是对于单例模式存在的问题及解决方法,值得注意的是:枚举创建单例模式的方法不会存在上述两个问题,道理很简单,留给大家思考。我们下期再见!

参考资料:黑马程序员笔记、《图解设计模式》

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。