Java程序员从笨鸟到菜鸟(三十一)23种设计模式之单例模式

一、单例模式

背景:

对于某些系统来说,只有一个实例很重要,例如:一个系统中可以存在多个打印任务,但是只能有一个正在执行打印任务;售票窗口,共有100张票,可以多个窗口同时售票,但是不能超售(这里的余票就是单例);在Spring中创建的Bean实例默认都是单例模式存在的

概念:

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例.每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。总之,选择单例模式就是为了避免不一致状态,避免政出多头

必要性

有些对象的创建非常耗时和耗内存,但是签好这些对象在我们应用中只需要使用1个,如果不能得到控制,会造成资源浪费;
例如:办公室的打印机,可以4、5个人共用一台打印机,就没必要每人一台,例如线程池和数据连接池

写法种类:

  • 懒汉式单例
  • 饿汉式单例

特点:

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己唯一实例
  3. 单例类必须给所有其他对象提供这一实例

二、 懒汉式、饿汉式、登记式单例实现

懒汉式单例

// 懒汉式单例,在第一次调用时实例化自己
public class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton() {};
    public static LazySingleton getInstance() {
        if (instance == null) {
            // 在多线程的环境下,可能被执行多次,造成线程不安全
            instance = new LazySingleton();
        }
        return instance;
    }
}

LazySingleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,LazySingleton的唯一实例只能通过getInstance()方法访问。

在多线程的环境下验证懒汉式单例的线程安全

public class TestLaySingletonSecure {
    public static void main(String[] args) {
        // 使用 FixedThreadPool 生成 20 个线程
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 20; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() +":" + LazySingleton.getInstance());
                }
            });
        }
        executorService.shutdown();
    }
}

运行结果:

pool-1-thread-1:singleton.LazySingleton@eb4851
pool-1-thread-2:singleton.LazySingleton@2648c8
pool-1-thread-3:singleton.LazySingleton@2648c8
pool-1-thread-4:singleton.LazySingleton@2648c8
pool-1-thread-5:singleton.LazySingleton@2648c8
pool-1-thread-6:singleton.LazySingleton@2648c8
pool-1-thread-8:singleton.LazySingleton@2648c8
pool-1-thread-7:singleton.LazySingleton@2648c8
pool-1-thread-9:singleton.LazySingleton@2648c8
pool-1-thread-10:singleton.LazySingleton@2648c8
pool-1-thread-11:singleton.LazySingleton@2648c8
pool-1-thread-12:singleton.LazySingleton@2648c8
pool-1-thread-13:singleton.LazySingleton@2648c8
pool-1-thread-14:singleton.LazySingleton@2648c8
pool-1-thread-15:singleton.LazySingleton@2648c8
pool-1-thread-16:singleton.LazySingleton@2648c8
pool-1-thread-17:singleton.LazySingleton@2648c8
pool-1-thread-18:singleton.LazySingleton@2648c8
pool-1-thread-19:singleton.LazySingleton@2648c8
pool-1-thread-20:singleton.LazySingleton@2648c8

Process finished with exit code 0

分析:有一个实例是 singleton.LazySingleton@eb4851,说明返回的不是同一个实例,这就是线程安全问题
原因:在对 instance 实例是否为空判断的代码段中,如果此时有两个线程分别为 A 和 B,如果线程 A 读取到 instance 为 null,然而此时 CPU 资源被线程 B 抢去了,这时线程 A 还没有对 instance 实例化,因此线程 B 获取的 instance 仍然为 null,就会对 instance 进行实例化,然后 CPU 资源被 A 抢去了,由于之前线程 A 读取的 instance 为 null,并不知道线程 B 已经对 instance 进行实例化,就会再次对 instance 进行实例化,这样就对 instance 实例化了两次,线程 A 和 线程 B 返回的不是一个实例

懒汉式单例没有考虑线程安全问题,是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全

解决懒汉式线程不安全的问题

方式一:在getInstance方法上加同步

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

方式二:双重检查锁定

public static Singleton getInstance() {
    if (singleton == null) {
        synchronized (Singleton.class) {
           // 进行两次判断,加入两个线程A,B,线程A先进入实例化对象,如果不加if判断,B线程再次进入时又会实例化一次,这样单例就失效了
            if (singleton == null) {
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

方式三:静态内部类

private static class LazyHolder {
    private static final Singleton INSTANCE = new Singleton();
}
public static final Singleton getInstance() {
    return LazyHolder.INSTANCE;
}

资源加载和性能对比:
方式一:在方法上加同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟大部分情况下是不需要同步的
方式二:在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
方式三:利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种

饿汉式单例

// 饿汉式单例类,在类初始化时,已经自行实例化
public class Singleton1 {
    private Singleton1() {}
    private static final Singleton1 singleton = new Singleton1();
    // 静态工厂方法
    public static Singleton1 getInstance() {
        return singleton;
    }
}

在多线程的环境下验证饿汉式单例的线程安全性

public class TestSingletonSecure {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 20; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + Singleton.getInstance());
                }
            });
        }
        executorService.shutdown();
    }
}

运行结果:

pool-1-thread-1:singleton.Singleton@1201d0e
pool-1-thread-3:singleton.Singleton@1201d0e
pool-1-thread-2:singleton.Singleton@1201d0e
pool-1-thread-7:singleton.Singleton@1201d0e
pool-1-thread-6:singleton.Singleton@1201d0e
pool-1-thread-10:singleton.Singleton@1201d0e
pool-1-thread-11:singleton.Singleton@1201d0e
pool-1-thread-5:singleton.Singleton@1201d0e
pool-1-thread-4:singleton.Singleton@1201d0e
pool-1-thread-14:singleton.Singleton@1201d0e
pool-1-thread-8:singleton.Singleton@1201d0e
pool-1-thread-12:singleton.Singleton@1201d0e
pool-1-thread-9:singleton.Singleton@1201d0e
pool-1-thread-15:singleton.Singleton@1201d0e
pool-1-thread-13:singleton.Singleton@1201d0e
pool-1-thread-16:singleton.Singleton@1201d0e
pool-1-thread-17:singleton.Singleton@1201d0e
pool-1-thread-18:singleton.Singleton@1201d0e
pool-1-thread-20:singleton.Singleton@1201d0e
pool-1-thread-19:singleton.Singleton@1201d0e

Process finished with exit code 0

分析:返回的都是同一个实例,在多线程环境下不会穿线线程不安全的问题
原因:饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的。

三、懒汉式和饿汉式的区别

懒汉式:比较懒,只有当调用getInstance的时候,才回去初始化这个实例
饿汉式:类一旦被加载,就把单例初始化完成,保证getInsatnce的时候,单例是已经存在的

线程安全:

懒汉式:线程不安全,但是可以通过上述方式一、二、三来实现线程安全
饿汉式:线程安全,可以直接用于多线程而不会出现问题

资源加载和性能

懒汉式会延迟加载,第一次调用的时候才会实例化对象,第一次调用做初始化,如果要做的工作比较多,性能会有延迟加载现象
饿汉式:在类创建的时候就实例化一个静态对象,不管之后会不会使用单例,都会占一定的内存,但是第一次调用的时候会很快,因为其资源已被初始化完成。

更多设计模式23种设计模式
原文传送门https://blog.csdn.net/jason0539/article/details/44956775

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。、可私 6信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 、可私信6博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 、可私信6博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值