单例设计模式

介绍

单例模式确保一个类只有一个实例,并提供一个全局访问点,实现单例模式的方法是私有化构造函数,通过getInstance()方法实例化对象,并返回这个实例这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式、懒汉式、双重检测、静态内部类、枚举。

优缺点

优点:

  1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
  3. 提供了对唯一实例的受控访问。
  4. 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
  5. 允许可变数目的实例。
  6. 避免对共享资源的多重占用。

缺点: 可能存在线程不安全的问题 解决线程安全问题

  1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  2. 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  3. 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  4. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

适用场景

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

  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  3. 有状态的工具类对象。
  4. 频繁访问数据库或文件的对象。
  5. 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。
  6. 控制资源的情况下,方便资源之间的互相通信。如线程池等。

应用场景

1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.。

实现单例模式的原则和过程

  1. 单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例 。
  2. 单例模式分类:分别是「饿汉」、「懒汉(非线程安全)」、「懒汉(线程安全)」、「双重校验锁」、「静态内部类」、「枚举」和 容器类管理、静态块初始化。
  3. 单例模式要素:
    a.私有构造方法
    b.私有静态引用指向自己实例
    c.以自己实例为返回值的公有静态方法

代码实例

饿汉式

单例实例在类装载时就构建,急切初始化。(预先加载法)

优点:先天性线程安全、在类加载的同时已经创建好一个静态对象,调用时反应速度快

缺点:资源效率不高,可能getInstance()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化,如果饿汉式使用过多,可能会影响项目启动的效率问题。

/*
* @Date: 2021/9/3 8:56
* @author: kaico
* @Description //TODO 单例模式:饿汉式
*                 优点:先天性线程是安全的,当类初始化的 就会创建该对象
*                 缺点:如果饿汉式使用过多,可能会影响项目启动的效率问题
**/
public class HungryTypeV1 {
    private static HungryTypeV1 hungryTypeV1 = new HungryTypeV1();

    /**
     * 将构造函数私有化 禁止初始化
     */
    private HungryTypeV1() {

    }

    //提供对外的方法获取实例对象
    public static HungryTypeV1 getInstance() {
        return hungryTypeV1;
    }

    //用mian方法测试
    public static void main(String[] args) {
        HungryTypeV1 instance1 = HungryTypeV1.getInstance();
        HungryTypeV1 instance2 = HungryTypeV1.getInstance();
        System.out.println(instance1 == instance2);

    }
}

懒汉式(线程不安全)

单例实例在第一次被使用时构建,延迟初始化。

优点:避免了饿汉式的那种在没有用到的情况下创建事例,资源利用率高,不执行getInstance()就不会被实例,可以执行该类的其他静态方法。

缺点: 懒汉式在单个线程中没有问题,但多个线程同事访问的时候就可能同事创建多个实例,而且这多个实例不是同一个对象,虽然后面创建的实例会覆盖先创建的实例,但是还是会存在拿到不同对象的情况。解决这个问题的办法就是加锁synchonized,第一次加载时不够快,多线程使用不必要的同步开销大。

/*
* @Date: 2021/9/3 9:05
* @author: kaico
* @Description //TODO 懒汉式(线程不安全)
**/
public class LazybonesModeV1 {

    /**
     * 懒汉式 (线程不安全)
     */
    private static LazybonesModeV1 lazybonesModeV1;

    private LazybonesModeV1() {

    }

    /**
     * 在真正需要创建对象的时候使用...
     *
     * @return
     */
    public static LazybonesModeV1 getInstance() {
        if (lazybonesModeV1 == null) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
            lazybonesModeV1 = new LazybonesModeV1();
        }
        return lazybonesModeV1;
    }

    //用mian方法测试
    public static void main(String[] args) {
        // 1.模拟线程不安全
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    LazybonesModeV1 instance1 = LazybonesModeV1.getInstance();
                    System.out.println(Thread.currentThread().getName() + "," + instance1);
                }
            }).start();
        }

    }

}

懒汉式(线程安全)

在懒汉式(线程不安全的基础上加上锁 synchonized),能够解决线程安全问题,创建和获取实例时都上锁 ,效率非常低,所以推荐使用双重检验锁。

/*
* @Date: 2021/9/3 9:05
* @author: kaico
* @Description //TODO 懒汉式(线程安全)效率非常低
**/
public class LazybonesModeV2 {

    /**
     * 懒汉式 (线程不安全)
     */
    private static LazybonesModeV2 lazybonesModeV1;

    private LazybonesModeV2() {

    }

    /**
     * 在真正需要创建对象的时候使用...
     *  能够解决线程安全问题,创建和获取实例时都上锁 ,效率非常低,所以推荐使用双重检验锁
     *
     * @return
     */
    public synchronized static LazybonesModeV2 getInstance() {
        if (lazybonesModeV1 == null) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
            lazybonesModeV1 = new LazybonesModeV2();
        }
        return lazybonesModeV1;
    }

    //用mian方法测试
    public static void main(String[] args) {
        // 1.模拟线程不安全
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    LazybonesModeV2 instance1 = LazybonesModeV2.getInstance();
                    System.out.println(Thread.currentThread().getName() + "," + instance1);
                }
            }).start();
        }

    }

}

双重检验锁(DCL)

在懒汉式(线程安全)的基础上再升级

/*
* @Date: 2021/9/3 9:05
* @author: kaico
* @Description //TODO 懒汉式 双重检验锁
**/
public class LazybonesModeV3 {

    /**
     * 懒汉式
     * volatile 禁止重排序和 提高可见性
     */
    private volatile static LazybonesModeV3 lazybonesModeV3;

    private LazybonesModeV3() {

    }

    /**
     * 在真正需要创建对象的时候使用...
     *  能够解决线程安全问题,创建和获取实例时都上锁 ,效率非常低,所以推荐使用双重检验锁
     *
     * @return
     */
    public static LazybonesModeV3 getInstance() {
        if (lazybonesModeV3 == null) {
            synchronized (LazybonesModeV3.class){
                if (lazybonesModeV3 == null) {
                    System.out.println("第一次开始创建实例对象....获取锁啦...");

                    try {
                        Thread.sleep(2000);
                    } catch (Exception e) {
                    }

                    lazybonesModeV3 = new LazybonesModeV3();

                }
            }

        }
        return lazybonesModeV3;
    }

    //用mian方法测试
    public static void main(String[] args) {
        // 1.模拟线程不安全
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    LazybonesModeV3 instance1 = LazybonesModeV3.getInstance();
                    System.out.println(Thread.currentThread().getName() + "," + instance1);
                }
            }).start();
        }

    }

}

静态内部类形式

静态内部方式能够避免同步带来的效率问题和有能实现延迟加载

优点:资源利用率高,不执行getInstance()不被实例,可以执行该类其他静态方法

缺点:第一次加载时反应不够快

/*
* @Date: 2021/9/3 9:25
* @author: kaico
* @Description //TODO 静态内部类形式
**/
public class StaticInsideClassModeV1 {

    private StaticInsideClassModeV1() {
        System.out.println("对象初始...");
    }

    public static StaticInsideClassModeV1 getInstance() {
        return StaticInsideClassModeV1Utils.singletonV5;
    }

    /**
     * 静态内部方式能够避免同步带来的效率问题和有能实现延迟加载
     */
    public static class StaticInsideClassModeV1Utils {
        private static StaticInsideClassModeV1 singletonV5 = new StaticInsideClassModeV1();
    }

    public static void main(String[] args) {
        System.out.println("项目启动成功");
        StaticInsideClassModeV1 instance1 = StaticInsideClassModeV1.getInstance();
        StaticInsideClassModeV1 instance2 = StaticInsideClassModeV1.getInstance();
        System.out.println(instance1 == instance2);
    }

}

枚举形式

枚举形式能够先天性,防止反射和序列化破解单例。

/*
* @Date: 2021/9/3 9:32
* @author: kaico
* @Description //TODO 单例模式(枚举形式)枚举形式能够先天性,防止反射和序列化破解单例。
**/
public enum EnumSingleton {

    INSTANCE;

    // 枚举能够绝对有效的防止实例化多次,和防止反射和序列化破解
    public void add() {
        System.out.println("add方法...");
    }

}
/*
* @Date: 2021/9/3 9:33
* @author: kaico
* @Description //TODO 单例模式(枚举形式)测试类
**/
public class EnumSingletonTest {

    public static void main(String[] args) throws Exception {
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        EnumSingleton instance2 = EnumSingleton.INSTANCE;
        System.out.println(instance1 == instance2);
        Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EnumSingleton v3 = declaredConstructor.newInstance();
        System.out.println(v3==instance1);
    }

}
枚举底层原理

1、首先如果使用java的反射机制破坏枚举单例,报错
在这里插入图片描述
2、使用java反编译技术,查看枚举类
在这里插入图片描述
从该图可以得出一个结论,枚举底层其实类。

3、枚举类底层原理分析
使用静态代码快方式,当静态代码快执行的时候初始化该对象,从而可以让开发者使用通过EnumSingleton.INSTANCE使用。
在这里插入图片描述
4、在该反编译源码中,定义了一个类继承了Enum 该类是中没有无参构造函数,所以反射机制调用无参构造函数是无法初始化的。

5、在该类中有一个只有一个有参构造函数

调用父类构造构造函数
在这里插入图片描述
Name 为定义调用对象名称,定义调用对象序号ordinal

6、为什么不能使用注入有参构造函数是否可以破解枚举呢?
这样是不行的,主要原因是 java的反射初始化对象中只要对象是是枚举是不会初始化的的
在这里插入图片描述

使用容器管理

这种使用SingletonManager 将多种单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

/*
* @Date: 2021/9/3 9:36
* @author: kaico
* @Description //TODO 使用容器管理单例模式
**/
public class SingletonManager {
    private static Map<String, Object> objMap = new HashMap<String, Object>();

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        {
            return objMap.get(key);
        }
    }

}

如何防止破坏单例

虽然单例通过私有构造函数,可以实现防止程序猿初始化对象,但是还可以通过反射和序列化技术破解单例。

使用反射技术破解单例

//使用反射技术破解单例
// 1. 使用懒汉式创建对象
LazybonesModeV3 instance1 = LazybonesModeV3.getInstance();
// 2. 使用Java反射技术初始化对象 执行无参构造函数
Constructor<LazybonesModeV3> declaredConstructor = LazybonesModeV3.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazybonesModeV3 instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);//两个对象并不相等

如何防止被反射破解

在构造函数那里增加对象是否存在的判断

private LazybonesModeV3() throws Exception {
        //防止反射破解单例模式
        synchronized (LazybonesModeV3.class) {
            if (lazybonesModeV3 != null) {
                throw new Exception("该对象已经初始化..");
            }
        }
    }

用反射获取对象时会报错
在这里插入图片描述

总结

一般采用饿汉式,若对资源十分在意可以采用静态内部类,不建议采用懒汉式及双重检测。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值