【设计模式】单例模式
一、简介
1、概述
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
- 使用时不能用反射模式创建单例,否则会实例化一个新的对象
- 使用懒单例模式时注意线程安全问题
- 饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
2、适用场景
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
3、应用场景举例
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗,避免并发操作冲突等。如应用程序的日志应用,共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;应用的配置对象的读取,配置文件是共享的资源;Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例(一个文件系统)来进行。
- 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,只能有一个PrinterSpooler,在输出的时候不能两台打印机打印同一个文件。
- 网站的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来,否则难以同步。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等,主要是节省打开或者关闭连接的效率损耗
- 多线程的线程池
4、优点
- 减少内存开销。只存在一个实例,针对对象需要频繁地创建销毁的场景节省内存开支;只创建一次实例,针对实例化对象产生需要比较多的资源时,能极大优化
- 提供了对唯一实例的受控访问。
- 避免对资源的多重占用(比如写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作)
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。
5、缺点
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层(没有接口,不能继承),因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
二、实现方式
1、饿汉式
静态变量直接实例化实例,确保在类加载的时候就创建唯一的实例。
优点:(1)线程安全。唯一一次实例化是在类加载的时候创建静态变量做的,以空间换时间,故不存在线程安全问题。
(2)在类加载的同时已经创建好一个静态对象,调用时反应速度快
(3)没有加锁,执行效率会提高。
缺点:(1)类加载时就初始化,可能getInstance()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化,资源利用率不高,浪费内存。
/**
* 饿汉式
*/
public class SingletonObject1 {
// 1、构造方法私有化:确保其它类无法调用构造方法,从而无法使用new进行实例化
private SingletonObject1 () {
}
// 2、静态的实例:由于是类的变量,因此该实例仅有1份;
// 类变量在加载类的时候就会初始化创建;
// 只要加载了该类,不管是否调用getInstance,该实例都会进行初始化(缺点)
public static SingletonObject1 singletonObject1 = new SingletonObject1();
// 3、外部调用获取实例的方法:注意要static
public static SingletonObject1 getInstance() {
return singletonObject1;
}
}
2、懒汉式
静态变量里不实例化,单例实例在第一次被使用时才构建,延迟初始化。
优点:避免了饿汉式的那种在没有用到的情况下创建事例,资源利用率高,不执行getInstance()就不会被实例,可以执行该类的其他静态方法。
缺点:如果getInstance不加sybchronited关键字,则为线程非安全版本,在多线程下无法保证单例。
添加了锁能确保多线程下线程安全,保证单例,但加锁会影响效率,只有该类第一次实例化时需要同步,实例化后不需要同步。
/**
* 懒汉式
*/
public class SingletonObject2 {
// 1、构造方法私有化
private SingletonObject2 () {
}
// 2、不在类加载时就初始化
private static SingletonObject2 singletonObject2 = null;
// 3、保证线程安全的关键:添加synchronized关键字,否则就不是线程安全,在多线程下就不是真实的单例了
// 引入缺点,获取实例串行化,多个线程同时调用getInstance的时候会排队获取,性能下降
public static synchronized SingletonObject2 getInstance() {
if (singletonObject2 == null) { // 当第一个进入getInstance时才会实例化对象,提高了资源利用率(懒加载)
singletonObject2 = new SingletonObject2();
}
return singletonObject2;
}
}
3、双重校验式
懒汉式要解决线程安全问题-->getInstance加锁-->带来效率低下问题,getInstance串行化执行,实际除第一次初始化需要保证同步,后续不需要
-->双重校验式去除getInstance方法中的锁,在if里面加锁同时再加一层判断-->引入可见性问题-->在静态实例中添加volatile关键字解决
优点:资源利用率高,不执行getInstance()就不被实例,可以执行该类其他静态方法;相对懒汉式效率高
缺点 :第一次加载时反应不快
/**
* 双重校验式
*/
public class SingletonObject3 {
// 1、私有构造
private SingletonObject3() {}
// 2、加static:唯一一个实例
// 置为null:类加载时不实例化,懒加载
// 加volatile关键字,避免因可见性带来的问题
private volatile static SingletonObject3 singletonObject3 = null;
public static SingletonObject3 getInstance() {
// 当singletonObject3未实例化时,线程才会进入到if里面
// 一旦有线程对singletonObject3进行了实例化,则后续再调用getInstance的线程都不会再进入if中
if (singletonObject3 == null) {
// 当多个线程进入了if里面准备对singletonObject3进行初始化,此时需要锁将这些线程串行化执行
synchronized (SingletonObject3.class) {
// 串行化下,先抢到锁的线程已经初始化完了,后面的线程进来需要再判断一下,避免重复实例化
if (singletonObject3 == null) {
singletonObject3 = new SingletonObject3();
}
}
}
return singletonObject3;
}
}
图解:
(1)初次有线程调用SingleObject3的getInstance方法,则由于singleObject3未被实例化,所以都进入了if,由于遇到了synchronized,于是接下来的代码变成串行化执行
(2)假设线程2抢到了锁,进入synchronized块完成singleObject3的实例化
(3)接着线程1抢到锁,进入synchronized块,由于线程2已经触发了singleObject3的实例化,因此判断到singleObject3不为空,跳过实例化(想一想,如果不加这重校验,是不是线程1和3都会继续对singleObject3进行实例化)
所以双重校验的关键点之一,在synchronized块里再加一个判断作用就是多个线程准备对singleObject3进行实例化时,避免后抢到锁的线程重复实例化
由于jvm存在乱序执行功能,因此也会出现线程不安全的情况。
具体分析如下:
singletonObject3 = new SingletonObject3();
实例化的步骤其实在jvm的执行分为三步:
a.在堆内存开辟内存空间。
b.在堆内存中实例化SingletonObject3里面的各个参数。
c.把对象指向堆内存空间。--此时对象就不为null了
假设线程2抢到锁执行new,然后线程4执行getInstance方法,此时线程2和线程4均处于可运行状态,由于jvm存在乱序执行功能,所以可能在b还没执行时就先执行了c,假如此时切换到线程4执行,那么线程4就会判断到对象不为空,直接拿去用了,此时就出现问题
解决办法,也是双重校验的关键点之一,singleObject3对象加volatile关键字,volatile会在编译时加lock,禁止了指令重排序,从而解决上述问题
(4)后续进来的线程,因为对象已经实例化了,则不再进入if,也就不会碰到synchronized,也就解决了懒汉式对象实例化后,调用getInstance非必要的抢锁串行化执行的问题
4、登记式(静态内部类)
静态内部类相当于外部类的非静态属性,加载外部类时不会触发静态内部类的加载,当显式调用 getInstance 方法时,才会显式装载 SingleHandle类,从而实例化 instance。
优点:资源利用率高,不执行getInstance()不被实例-->弥补了饿汉式的缺点,同时不需要像双重校验那样麻烦,且不像懒汉式那样效率低下
缺点:第一次加载时反应不够快;由于是静态内部类的形式去创建单例的,故外部无法传递参数进去
/**
* 登记式(静态内部类)
*/
public class SingletonObject4 {
// 1、私有构造
private SingletonObject4 () {}
// 2、私有静态内部类,在里面设定实例化的静态变量(与饿汉的不同之处)
private static class SingleHandle{
public static SingletonObject4 singletonObject4 = new SingletonObject4();
}
// 3、获取实例时,直接取静态内部类中的静态变量,即唯一实例
public static SingletonObject4 getInstance() {
return SingleHandle.singletonObject4;
}
}
静态内部类的方式能保证线程安全的原因就在于JVM默认静态字段初始化是加同步锁的
在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制, 但是在某些情况下,JVM已经隐含的为您执行了同步,这些情况下就不用自己再来进行同步控制了。 这些情况包括:
(1)由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时;
(2)访问final字段时;
(3)在创建线程之前创建对象时;
(4)线程可以看见它将要处理的对象时。
5、枚举
线程安全,非懒加载
/**
* 枚举类
*/
public enum Singleton {
INSTANCE;
public void showMsg () {
System.out.println("Hello World!");
}
}
6、简单校验多线程单例的方法
测试正确的单例,应该打印一样的类。假如用下面代码测试懒汉式,且getInstance不加synchronized关键字,则会出现打印不同的类
class MyTest{
public static void main(String[] args) {
IntStream.rangeClosed(1, 100).forEach(i->new Thread(){
@Override
public void run() {
System.out.println(SingletonObject2.getInstance());
}
}.start());
}
}
7、经验之谈
(1)一般情况用饿汉式
(2)不建议用懒汉式
(3)明确使用懒加载时,才用静态内部类的方式
(4)有传参需求,及懒加载需求,才用双重校验方式
- 点赞
- 收藏
- 关注作者
评论(0)