单例模式及常用使用场景简析

设计模式-单例模式简介及使用场景

单例模式

顾名思义,单例模式(Singleton Pattern)就是保证一个类有且仅有一个实例,并且提供了一个全局的访问点。这就要求我们绕过常规的构造器,提供一种机制来保证一个类只有一个实例,客户程序在调用某一个类时,它是不会考虑这个类是否只能有一个实例等问题的,所以,这应该是类设计者的责任,而不是类使用者的责任。
单例类的特点总结如下:

1. 只能有一个实例
2. 构造方法应该由private修饰,也就是自己创建自己的唯一实例
3. 必须给其他对象提供这一实例

实现单例模式的几种方式

上文已经提到,单例类要实现有且仅有一个实例必须要设计者对该类进行处理,下面就简单介绍一下如何设计一个单例类。

1. 饿汉模式

public class MySingleton {

	//自创的单例
    private static final MySingleton instance = new MySingleton();

	//私有的单例构造方法,避免外部创建
    private MySingleton() {
    }

	//公有的静态方法,为外部提供唯一的单例类
    public static MySingleton getInstance(){
        return instance;
    }
}

从上面代码可以看出,饿汉模式在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。通过用private修饰构造方法,保证了外部不能对该单例类进行实例化。

2. 懒汉模式

public class MySingleton {

    private static MySingleton instance = null;
    
    // 私有构造
    private MySingleton() {}

	// 静态工厂方法
    public static MySingleton getInstance() {
        if(instance == null){
            instance = new MySingleton();
        }
        return instance;
    }
}

上面就是懒汉式单例的简单写法,可以看到在多线程情况下会产生线程安全问题。假设MySingleton类刚刚被初始化,instance为空,此时两个线程同时访问getInstance方法,因为instance为空,所以两个线程同时通过了条件判断,都new出了一个MySingleton实例,MySingleton类被构建了两次。为了解决这个问题,可以使用双重检测机制进行约束,代码如下:

public class MySingleton {

	 private static MySingleton instance = null;

    // 私有构造
    private MySingleton() {
    }

    //静态工厂方法
    public static MySingleton getInstance() {
        if (instance == null) {//双重检测
            // 等同于 synchronized public static MySingleton getInstance()
            synchronized (MySingleton.class) {//同步代码块
                // 注意:里面的判断是一定要加的,否则出现线程安全问题
                if (instance == null) {//双重检测
                    instance = new MySingleton();
                }
            }
        }
        return instance;
    }
}

上面代码为了防止线程安全问题,用synchronized同步锁对相关代码块进行了同步处理。如下图,假设线程A和线程B同时做完判空处理且线程A首先获得锁,执行完代码块并new了一个MySingleton实例后释放锁,此时线程B获取同步锁来到第二条判空语句时,此时instance不为空,返回线程A创建好的单例实例。
在这里插入图片描述
上面代码看似没任何问题,但当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法,如下图所示。此时线程A要么已经构建完instance(线程B判断时返回false),要么还未构建完instance(线程B判断时返回true),但事实并非如此,这里涉及到JVM编译器的指令重排,也就是java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:

memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址

这些指令的顺序并非一成不变,经过JVM和CPU的优化可能会改变顺序。这时会出现线程B获取到一个未初始化完成的instance对象,在instance对象前面增加一个修饰符volatile可以避免发生这种情况,volatile关键字保证了instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。
在这里插入图片描述
线程安全的单例类写法如下:

public class MySingleton {

	 private volatile static MySingleton instance = null;

    // 私有构造
    private MySingleton() {
    }

    //静态工厂方法
    public static MySingleton getInstance() {
        if (instance == null) {//双重检测
            // 等同于 synchronized public static MySingleton getInstance()
            synchronized (MySingleton.class) {//同步代码块
                // 注意:里面的判断是一定要加的,否则出现线程安全问题
                if (instance == null) {//双重检测
                    instance = new MySingleton();
                }
            }
        }
        return instance;
    }
}

3. 静态内部类实现

public class MySingleton {

	//内部类
    private static class InnerObject {
        private static final Singleton instance = new MySingleton();
    }

	//私有的单例构造方法,避免外部创建
    private MySingleton() {
    }

	//公有的静态方法,为外部提供唯一的单例类
    public static MySingleton getInstance(){
        return InnerObject.instance;
    }
}

上面方法需要注意的是instance对象并不是在单例类Singleton被加载的时候初始化的,而是在调用getInstance方法是通过静态内部类加载。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

4. static静态代码块实现

public class MySingleton {

	//自创的单例
    private static final MySingleton instance = null;

	//私有的单例构造方法,避免外部创建
    private MySingleton() {
    }
	
	static {
		instance = new MySingleton();
	}
	//公有的静态方法,为外部提供唯一的单例类
    public static MySingleton getInstance(){
        return instance;
    }
}

5. 枚举类实现

上述所说的单例类实现方式均可通过反射来访问单例类中的私有构造方法,从而构造出多个单例对象,这和单例对象的定义违背,我们可以通过设计内部枚举类来防止该类问题。枚举类单例不是懒加载,其单例对象是在枚举类被加载时初始化的。

public enum MySingletonEnum {
    instance;
}

单例模式的常见应用场景

单例模式应用的场景一般发现在以下条件下:

  1. 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
  2. 控制资源的情况下,方便资源之间的互相通信。如线程池等。

根据上诉条件列出以下常用的应用场景

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

好了,以上就是单例类的几种常见实现方式和用用场景,参考文章

https://zhuanlan.zhihu.com/p/33102022

https://www.cnblogs.com/restartyang/articles/7770856.html

  • 7
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值