单例模式(Singleton Pattern)

1. 简介

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

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

单例模式具备典型的3个特点:

  1. 单例类只能有一个实例(构造器私有)
  2. 它必须自行创建这个实例(自己编写实例化逻辑)
  3. 它必须自行向整个系统提供这个实例(对外提供实例化方法)

2. 优缺点

优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
  • 避免对资源的多重占用

缺点:

  • 单例模式没有抽象层,扩展困难
  • 单例类违背了单一职责原则,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化

3. 使用场景

  • 需要频繁实例化与销毁的对象
  • 有状态的工具类对象
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象,比如I/O与数据库的连接等
  • 唯一序列号,计数器等,不用每次刷新都在数据库里加一次,用单例先缓存起来
  • 一些设备管理器,比如有两台打印机,不能打印同一个文件,可以设计为单例模式
  • 如果需要更加严格地控制全局变量, 可以设计为单例模式

4. 分类

单例模式有八种方式:

  • 饿汉式(静态常量)
  • 饿汉式(静态代码块)
  • 懒汉式(线程不安全)
  • 懒汉式(线程安全,同步方法)
  • 懒汉式(线程安全,同步代码块)
  • 双重检查
  • 静态内部类
  • 枚举
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

5. 饿汉式(静态常量)

5.1 代码

步骤如下:

  1. 构造器私有化 (防止 new )
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法。
public class Singleton {
    // 1、构造器私有化
    private Singleton() {
    }

    // 2、类的内部创建对象
    private static final Singleton instance = new Singleton();

    // 3、向外暴露一个静态的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}

5.2 优缺点

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题

缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费

这种方式基于 classloder 机制避免了多线程的同步问题。不过,instance 在类装载时就实例化,在单例模式中大多数都是调用getlnstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 Lazy loading 的效果。

6. 饿汉式(静态代码块)

6.1 代码

步骤如下:

  1. 构造器私有化
  2. 类的内部声明对象
  3. 在静态代码块中创建对象
  4. 向外暴露一个静态的公共方法
public class Singleton {
    // 1、构造器私有化
    private Singleton() {
    }

    // 2、类的内部声明对象
    private static Singleton instance;

    // 3、在静态代码块中创建对象
    static {
        instance = new Singleton();
    }

    // 4、向外暴露一个静态的公共方法
    public static Singleton getInstance() {
        return instance;
    }
}

这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。

6.2 优缺点

优缺点和上面是一样

7. 懒汉式(线程不安全)

7.1 代码

步骤如下:

  1. 构造器私有化
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法,当使用到该方法时,才去创建 instance
// 1、构造器私有化
private Singleton() {
}

// 2、类的内部声明对象
private static Singleton instance;

// 3、向外暴露一个静态的公共方法,当使用到该方法时,才去创建 instance
public static Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

7.2 优缺点

  1. 起到了 Lazy Loading 的效果,但是只能在单线程下使用
  2. 如果在多线程下,一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例

在实际开发中,不要使用这种方式

8. 懒汉式(线程安全,同步方法)

8.1 代码

步骤如下:

  1. 构造器私有化
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法,加入同步处理的代码,解决线程安全问题
public class Singleton {
    // 1、构造器私有化
    private Singleton() {
    }

    // 2、类的内部声明对象
    private static Singleton instance;

    // 3、向外暴露一个静态的公共方法,加入同步处理的代码,解决线程安全问题
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

8.2 优缺点

  1. 解决了线程不安全问题
  2. 效率低,每个线程在想获得类的实例时候,执行getlnstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低

在实际开发中,不推荐使用这种方式

9. 懒汉式(线程不安全,同步代码块)

9.1 代码

步骤如下:

  1. 构造器私有化
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法,加入同步处理的代码块
public class Singleton {
    // 1、构造器私有化
    private Singleton() {
    }

    // 2、类的内部声明对象
    private static Singleton instance;

    // 3、向外暴露一个静态的公共方法,加入同步处理的代码,解决线程安全问题
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

9.2 优缺点

  1. 这种方式,本意是想对上一种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
  2. 但是这种同步并不能起到线程同步的作用。跟第7节实现方式遇到的情形一致,假如一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例

在实际开发中,不能使用这种方式

10. 双重检查

10.1 线程不安全

public class Singleton { 
    private static Singleton instance = null;

    private Singleton() {  
    }

    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized (instance) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    } 
}

我们看到有两个判空的逻辑,第二个判断逻辑就是为了解决上面写法,在等待synchronized锁的过程中有可能上一个线程已经创建了实例,从而导致创建多个实例的问题。

但是此种写法依然不是线程安全的,因为创建对象不是一个原子性操作。JVM创建新的对象instance = new Singleton()时,主要要经过三步(省略其他非主要步骤):

  • 分配内存
  • 初始化构造器
  • 将对象指向分配的内存的地址

而实际情况是,JVM会对以上三个指令进行调优,其中有一项就是调整指令的执行顺序,该操作由JIT编译器来完成。所以,在指令被重排序的情况下可能会出现问题。

假如2和3的步骤是相反的,先将分配好的内存地址指给instance,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为instance对象已经实例化了,直接返回一个引用。如果这时还没进行构造器初始化并且这个线程使用了instance的话,则会出现线程会指向一个未初始化构造器的对象现象,从而发生错误。

我们以A、B两个线程为例:

  • A、B线程同时进入了第一个if判断
  • A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton(),由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员,注意此时JVM没有开始初始化这个实例,然后A离开了synchronized块
  • B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序
  • 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了

针对这种情况,我们可以使用volatile关键字,禁止指令重排来实现线程安全。所以有了下面这种线程安全的双重检验锁模式。

10.2 volatile修饰的双重检验锁模式

步骤如下:

  1. 构造器私有化
  2. 类的内部创建对象,同时用volatile关键字修饰
  3. 向外暴露一个静态的公共方法,加入同步处理的代码块,并进行双重判断,解决线程安全问题
public class Singleton {
    // 1、构造器私有化
    private Singleton() {
    }

    // 2、类的内部声明对象,同时用`volatile`关键字修饰修饰
    private static volatile Singleton instance;

    // 3、向外暴露一个静态的公共方法,加入同步处理的代码块,并进行双重判断,解决线程安全问题
    public static Singleton getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if (instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为空
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

需要注意,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。所以,在jdk1.5版本前,双重检测锁形式的单例模式是无法保证线程安全的,即使将变量声明为volatile也无法完全避免重排序所导致的问题。

10.3 优缺点

  1. Double-Check 概念是多线程开发中常使用到的,我们进行了两次检查,这样就可以保证线程安全了
  2. 这样实例化代码只用执行一次,后面再次访问时直接 return 实例化对象,也避免的反复进行方法同步
  3. 线程安全;延迟加载;效率较高

在实际开发中,推荐使用这种单例设计模式

11. 静态内部类

11.1 代码

步骤如下:

  1. 构造器私有化
  2. 定义一个静态内部类,内部定义当前类的静态属性
  3. 向外暴露一个静态的公共方法
public class Singleton {
    // 1、构造器私有化
    private Singleton() {
    }

    // 2、定义一个静态内部类,内部定义当前类的静态属性
    private static class SingletonInstance {
        private static final Singleton instance = new Singleton();
    }

    // 3、向外暴露一个静态的公共方法
    public static Singleton getInstance() {
        return SingletonInstance.instance;
    }
}

因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。另外由于静态变量只初始化一次,所以singleton仍然是单例的。

11.2 优缺点

  1. 这种方式采用了类装载的机制,来保证初始化实例时只有一个线程
  2. 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用getlnstance方法,才会装载Singletonlnstance 类,从而完成 Singleton 的实例化
  3. 类的静态属性只会在第一次加载类的时候初始化,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
  4. 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高

推荐使用

11.3 与双重检测锁模式比较

这种方式能达到双检锁方式双重检测锁模式一样的功能,但实现更简单。对静态域使用延迟初始化,应使用静态内部类模式。而对实例域需要延迟初始化时,使用双重检测锁模式。

11.4 与饿汉式比较

这种方式同样利用了classloader机制来保证初始化instance时只有一个线程。饿汉模式只要 Singleton类被装载了,那么instance就会被实例化,没有达到延迟加载的目的,而这种方式Singleton类被装载了,instance不一定被初始化。只有SingletonFactory通过显式调用 getInstance方法时,才会显式装载SingletonFactory类,从而实例化 instance。

静态内部类模式看起来已经非常完美了,但是它存在两个缺点:反射攻击和序列化攻击。

12. 枚举

12.1 代码

public enum Singleton {
    INSTANCE;

    public void sayHello() {
        System.out.println("Hello World");
    }
}

枚举的这种写法更简洁,自动支持序列化机制,并且是无法通过反射来生成新的实例,绝对防止多次实例化,因为枚举没有public构造方法。但是这种⽅式在存在继承场景下是不可⽤的。

一般情况下,不建议使用懒汉模式,建议使用饿汉模式,在有明确实现延迟加载时,建议使用静态内部类模式,如果涉及到反序列化创建对象时,可以使用枚举单例模式,如果有其他特殊的需求,可以考虑使用双重检测锁模式。

12.2 优缺点

  1. 这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
  2. 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式

推荐使用

13. CAS模式(线程安全)

java并发库提供了很多原⼦类来⽀持并发访问的数据安全性,所以,使用并发库实现单例模式也是可以的。如下AtomicReference可以封装引⽤⼀个单例实例,⽀持并发访问。

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE
                           = new AtomicReference<Singleton>();
     private static Singleton instance;
     
     private Singleton() {
     }

     public static final Singleton getInstance() {
         for (; ;) {
             Singleton instance = INSTANCE.get();
                 if (null != instance) return instance;
                 INSTANCE.compareAndSet(null, new Singleton());
                 return INSTANCE.get();
         }
 }

使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以⽀持较⼤的并发性。

CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中。

14. JDK 源码分析

我们JDK中,java.lang.Runtime就是经典的单例模式(饿汉式)

图片[1]-单例模式(Singleton Pattern)-深吸氧

单例模式注意事项和细节说明

  1. 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
  3. 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)

15. 单例模式如何防止反射攻击和反序列化攻击

枚举单例天生解决了危害单例模式安全性的两个问题:反射攻击和反序列化攻击,接下来,我们来看看其他单例模式是如何防止这两个问题的。

15.1 反射攻击

public class ReflectTest {

    public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        // 获取无参构造函数
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        // 使用构造函数创建对象
        constructor.setAccessible(true);
        Singleton reflectInstance = constructor.newInstance();
        log.info("result:{}", instance == reflectInstance);
    }

    // result:false
}

15.2 防止反射攻击

实现单例模式时增加一个标志变量,在构造函数中检查是否已被调用过,若已被调用过,则抛出异常,保证构造函数只被调用一次:

public class Singleton {
    private static Singleton instance = null;
    private static boolean isInstance = false;

    private Singleton() {
        synchronized(Singleton.class) {
            if (!isInstance) {
                isInstance = true;
            } else {
                throw new RuntimeException("单例模式受到反射攻击!");
            }
        }
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

15.3 反序列化攻击

public class DeserializeTest {

    public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        byte[] bytes = serialize(instance);
        Object deserializeInstance = deserialize(bytes);
        log.info("result:{}", instance == deserializeInstance);
    }

    // result:false

    private static byte[] serialize(Object object) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(object);
        return baos.toByteArray();
    }

    private static Object deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        return ois.readObject();
    }
}

15.4 防止反序列化攻击

增加一个readResolve方法并返回instance对象。在反序列化时,如果对象存在readResolve方法,则会调用该方法返回对象:

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

    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return instance;  
    }  
}

由于jdk的设计,为枚举防止了反射和反序列化攻击,所以我们可以直接使用枚举类型,不用担心枚举单例的安全性。其他模式,也可以根据上面提到的两种方式,解决单例模式的安全性问题。

© 版权声明
THE END
请撒泡尿证明你到此一游
点赞3 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容