单例模式以及五种实现方式

目录

 

什么是单例

使用单例模式有什么好处

应用场景

有哪几种实现方式

其他的注意事项


什么是单例

    单例模式根据名字可知就是一个类只能有一个单独的实例并且对外提供一个访问该实例的全局访问点。

使用单例模式有什么好处

单例模式只生成一个实例,减少了系统性能开销。

当一个实例的创建需要较多资源时,比如读取配置文件、产生其他依赖对象等,可以通过启动应用时直接产生一个永驻内存的单例对象来避免重复创建对象产生的性能消耗。

应用场景

- 项目中读取配置文件的类

- 数据库连接池

- Spring中的Beans默认是单例

- Windows的任务管理器

等等~~

有哪几种实现方式

单例模式的实现一共有以下五种方式,总的代码步骤大多都分为三步

a- 私有化构造器

b- 创建一个private static的本类实例

c- 提供一个public的公共方法对外提供实例

1-饿汉式

实现代码如下

/**
 * 饿汉式单例
 * 线程安全,调用效率高但是存在资源浪费,不能延时加载
 */
public class SingletonDemo {

    //static对象在类加载时被创建,类加载的过程中是天然的线程安全的模式
    private static SingletonDemo instance = new SingletonDemo();
    private SingletonDemo(){

    }
    //instance已经在线程安全的状态下创建过了,所以getInstance方法不需要加syncohronized关键字就是线程安全的.
    public static SingletonDemo getInstance(){
        return instance;
    }
}

优点:线程安全,调用效率高

缺点:存在资源浪费,不能延时加载

2-懒汉式

实现代码如下

/**
 * 懒汉式延时加载
 * 线程安全,资源利用率高但getInstance被加锁了,并发效率低.
 */
class SingletonDemo02{
    //类加载时不创建对象
    private static SingletonDemo02 instance ;
    private SingletonDemo02(){}
    //由于多线程调用getInstance时存在线程A和线程B同时执行到if(instance == null)部分,会导致两个线程分别创建一个对象,违反单例的初衷,所以加锁.
    public static synchronized SingletonDemo02 getInstance(){
        //当用到这个对象时getInstance方法被调用,才会创建对象,避免了资源浪费.
        if (instance == null){
            instance = new SingletonDemo02();
        }
        return instance;
    }
}

优点:线程安全,资源利用率高,延时加载

缺点:并发效率低。懒汉式因为getInstance加了锁,是所有实现方式中效率最低的。

3-双锁检测

实现代码如下

/**
 * 双重检测锁机制
 * 将synchronized块放到了方法内
 */
class SingletonDemo03{
    private static SingletonDemo03 singleton;
    private SingletonDemo03(){}

    public static SingletonDemo03 getInstance(){
        if(singleton == null){
            synchronized(SingletonDemo03.class){
                if(singleton == null)
                    singleton = new SingletonDemo03();   //1
            }
        }
        return singleton;
    }
}

双锁检测机制是对懒汉式的一种改进,想要通过锁定局部代码不锁方法的形式改良懒汉式并发访问效率差的问题

理想很丰满,但是因为JVM内部结构的问题,该方法容易出错,不能使用。

4-静态内部类

实现代码如下

/**
 * 静态内部类方式
 * 线程安全,调用效率高,懒加载
 */
class SingletonDemo04{
    //main方法中测试了创建外部类对象时静态内部类不会被加载
    public static void main(String[] args) {
        SingletonDemo04 s = new SingletonDemo04();
    }
    //静态内部类在SingletonDemo04加载的时候不会同其他static对象一起加载,在用到的时候才加载
    public static class SingletonInstance{
        static {
            System.out.println("inner class loaded");
        }
        public static final SingletonDemo04 instance = new SingletonDemo04();
    }

    private SingletonDemo04(){}

    public static SingletonDemo04 getInstance(){
        return SingletonInstance.instance;
    }
}

静态内部类的实现方式基本上集合了所有懒汉式和饿汉式的优点。最推荐使用的实现方式。

5-枚举

/**
 * 通过枚举创建单例对象
 */
enum SingletonDemo05{
    //本身就是单例对象
    INSTANCE;

    //用户可添加其他操作
    public static void singletonOperation(){

    }
}

枚举是通过JVM内部机制实现的,比较纯天然。在效率上略低于静态内部类的方式,也不能懒加载。没有其他缺点。

其他的注意事项

单例的意义在于只能创建一个该类的实例,但是在实际使用时还是存在一些漏洞可以打破单例的限制(即便构造器是private的)。

1- 反射漏洞

反射通过Class.forName()是可以创建任何一个类的构造器的,而构造器又可以通过setAccessible(true)来访问该类的private成员和方法,导致单例模式被打破。解决办法如下:

    //若通过反射漏洞多次访问构造方法,一定会在instance不为null时另外创建其他对象,在构造方法中增加判断条件即可.
    //注意,虽然构造方法是私有的,但是通过Constructor对象的setAccessible(true)是可以跳过私有检查的.
    private SingletonDemo06(){
        if (instance != null){
            throw new RuntimeException("Instance is not null");
        }
    }

在构造器中设置一个instance是否为空的检测。

因为单例模式是在instance为空时创建对象,不为空时直接返回现有的对象。也就是说构造器实际上只会在instance为空时调用一次 。方法中的判断条件可以有效防止反射漏洞。

2-反序列化漏洞

反序列化通过ObjectInputStream的readObject方式来加载被序列化的对象。(为什么readObject可以跳过private的权限检查不确定,读了几行源码感觉应该是先创建了ObjectInputStream对象然后强转为被序列化的类型,不能引导错大家,此处还请大佬指点)

解决办法

在被序列化的类中重写readResolve()方法

    //定义了readResolve方法在反序列化过程中不会创建新的对象.
    public Object readResolve() throws ObjectStreamException{
        return instance;
    }

需要注意,反射和反序列化的漏洞都对枚举的单例不起作用。因为枚举类型是绝对安全的。

猜你喜欢

转载自blog.csdn.net/weixin_39445556/article/details/106387219