目录
一、概念
二、单例模式的优点
三、单例模式的缺点
四、关键代码
五、单例模式的使用场景
六、单例模式的实现方式
6.1、饿汉式和懒汉式区别
6.2、饿汉式
6.2.1、反射会破坏单例模式
6.3、懒汉式
6.3.1、线程不安全
6.4、双重校验锁DCL--安全懒汉式
6.4.1、为什么需要使用两个 if 进行判断呢?
6.4.2、为什么要加 volatile?
6.5、静态内部类懒汉式
七、JDK中的单例模式
7.1、Runtime
7.2、java.awt.Toolkit
八、单例模式的扩展
九、最佳实践
单例模式是设计模式中最简单也是最常用的设计模式之一,单例顾名思义就是系统中只有唯一实例,这个唯一实例的获取方式就是通过一个方法的调用获得,而不是通过正常流程中的new实例化。
1、由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建、销毁时,而且创建或销毁时性能无法优化,单例模式的优势就非常明显。
2、由于单例模式生成一个实例,所以减少了系统性能的开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接生产一个单例对象,然后用永远驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
3、单例模式可以避免对一个资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
4、单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
1、单例模式一般没有接口,扩展很难,若要扩展,除了修改代码基本没有第二种途径可以实现。
单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求"自行实例化”,并且提供单一实例,接口和抽象类是不可能被实例化的。当然在特殊情况下,单例模式可以实现接口、被继承等,需要从系统开发中根据环境判断。
2、单例模式对测试是不利的。在并行开发中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个独享。
3、单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要取决于环境,单例模式把"要单例"和业务逻辑融合在一个类中。
构造函数是私有的。
1、为了保证只有一个对象,不能new对象,所以设置构造方法私有。
2、只能通过方法或者属性获取对象,如果通过属性获取,这个属性是可以修改的,所以属性只能
是私有的。所以只能通过方法获取。
3、由于我们不能new对象,所以获取对象的方法定是静态的。属性也得是静态的,因为不是静态的,静态的方法访问不到
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现"不良反应",可以采用单例模式,具体的场景如下:
单例模式的写法有好几种,主要有三种:懒汉式单例、饿汉式单例、登记式单例。
(1)初始化时机与首次调用:
- 饿汉式是在类加载时,就将单例初始化完成,保证获取实例的时候,单例是已经存在的了。所以在第一次调用时速度也会更快,因为其资源已经初始化完成。
- 懒汉式会延迟加载,只有在首次调用时才会实例化单例,如果初始化所需要的工作比较多,那么首次访问性能上会有些延迟,不过之后就和饿汉式一样了。
(2)线程安全方面:饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,需要通过额外的机制保证线程安全。
线程安全 |
访问速度 |
性能 |
|
饿汉式 |
安全 |
快 |
差 |
懒汉式 |
不安全 |
慢 |
好 |
特点:
- 在类加载后就提前创建好了唯一实例, 并不是用到时才创建,而是提前创建
- 因为 唯一实例是在静态代码块里面,静态代码的执行处于类生命周期中的初始化阶段,由虚拟机保证其原子且安全执行。所以不用考虑这里的线程不安全问题,所以,饿汉式 线程安全。
- 就是占内存,有性能损耗
public class Singleton1 implements Serializable {
//1.私有的构造器:构造器不是私有的话,其他类会调用你的构造器,来创建实例对象,那就有可能有多个实例了,就不是单例模式了
private Singleton1() {
//防止 反射强制获取到私有构造器 创建了实例对象 破坏了单例模式
if(INSTANCE != null){
throw new RuntimeException("单例对象不能重复创建");
}
}
//2.静态的成员变量:用私有构造器创建出的唯一实例
private static final Singleton1 INSTANCE = new Singleton1();
//3.公共的静态方法:获得实例对象
public static Singleton1 getInstance() {
return INSTANCE;
}
}
虽然你的构造方法是私有的,但是反射可以强制获取私有的构造方法,然后再用构造方法创建实例,这样实例就不是唯一的了,单例模式就被破坏了。
解决方法
在构造器中加一个判断
//防止 反射强制获取到私有构造器 创建了实例对象 破坏了单例模式
if(INSTANCE != null){
throw new RuntimeException("单例对象不能重复创建");
}
需要的时候才会去创建对象
- 好处节省内存
- 坏处用的时候才创建稍微有点慢
例如下面代码,就是在调用getInstance()方法时才会创建
public class Singleton2 {
//1.私有的构造器:构造器不是私有的话,其他类会调用你的构造器,来创建实例对象,那就有可能有多个实例了,就不是单例模式了
private Singleton2() {}
//2.静态的成员变量:用私有构造器创建出的唯一实例
//懒汉式 先设置null,调用的时候再创建
private static Singleton2 single=null;
//3.公共的静态方法:获得实例对象
public static synchronized Singleton2 getInstance() {
if (single == null) {
single = new Singleton2();
}
return single;
}
}
当创建了100个线程,调用getInstance方法时,可能会有多个线程同时看到没有new,就会执行多次new,调用多次构造方法(也就是会进行多次初始化)
解决方法
线程安全 给getInstance方法加synchronize锁
之前的 懒汉式单例中,为了解决线程不安全的问题,我们选择加锁,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的。
我们可以选择不给getInstance方法加synchronize锁,而是在这个方法里面去进行加synchronize锁,因为方法锁的范围太广,其他线程阻塞的范围就大,时间就长。
并且,我们加两个锁,在synchronized块外面加一个,里面加一个
之前的方式:锁加在方法上
public static synchronized Singleton2 getInstance() {
if (single == null) {
single = new Singleton2();
}
return single;
}
现在的方式:锁加在方法内部
public class Singleton3 {
//1.私有的构造器:构造器不是私有的话,其他类会调用你的构造器,来创建实例对象,那就有可能有多个实例了,就不是单例模式了
private Singleton3() {}
//2.静态的成员变量:用私有构造器创建出的唯一实例
private static volatile Singleton3 single=null;
//3.公共的静态方法:获得实例对象
public static Singleton3 getInstance() {
if (single == null) {
synchronized (Singleton3.class){
if (single == null){
single = new Singleton3();
}
}
}
return single;
}
}
我们加了两个锁,在synchronized块外面加一个,里面加一个
假设高并发下,线程A、B 都通过了第一个 if 条件。若A先抢到锁,new 了一个对象,释放锁,然后线程B再抢到锁,此时如果不做第二个 if 判断,B线程将会再 new 一个对象。使用两个 if 判断,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗。
为什么两个if判断null:
场景:有可能多个线程同时进入了第一个if,都读到对象null了,如果第一个线程加上锁创建了对象之后,释放锁之后,如果不进行再次判断null的话,就会再次进行创建对象(以第一个判断null为准),两次就是为了防止多次创建对象
第一个if:对象为null的时候才进入下面if进行创建对象,如果不为null就return
第二个if:判断在第一个为null的多线程情况下阻止多创建对象的。
示例:如果只有一个if,也就是上面的那个
volatile 解决共享变量的可见性问题、有序性问题,在这里主要是解决有序性问题。
原因:可能导致对象还没创建成功(只分配了空间地址,没有数据)就返回句柄,空指针异常)、static(不必创建对象)
加了volatile,会使在赋值语句之后加上一个内存屏障,阻止之前的一些操作越过屏障,可以阻止代码或者说指令的重排序
volatile 的作用主要是禁止指定重排序。假设在不使用 volatile 的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行 singleton = new Singleton(),但由于构造方法不是一个原子操作,编译后会生成多条字节码指令,由于 JAVA的 指令重排序,可能会先执行 singleton 的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后 singleton 便不为空了,但是实际的初始化操作却还没有执行。如果此时线程B进入,就会拿到一个不为空的但是没有完成初始化的singleton 对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。
两全其美的方式:既有 饿汉式 线程安全 的特点,又有 懒汉式 没有性能损耗的 特点
只要方法静态代码块里面,线程就是安全的,所以我们想办法将创建实例的操作放到静态代码块里面
内部类的特点
- 内部类可以访问外部类的私有变量和方法
- 内部类 是在静态代码块里面的,态代码的执行处于类生命周期中的初始化阶段,由虚拟机保证其原子且安全执行,也就是有饿汉式的优点:线程安全
- 并且内部类,你不用的时候不会加载,所以也有懒汉式的优点:性能高
利用了类加载机制来保证初始化 instance 时只有一个线程,所以也是线程安全的,同时没有性能损耗,这种比上面的方法都好一些,既实现了线程安全,又避免了同步带来的性能影响。
public class Singleton {
//静态内部类
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
Runtime类封装了Java运行时环境。每一个Java程序实际上都启动了一个JVM进程,那么每个JVM进程都对应一个Runtime实例,此实例由JVM为其实例化。每个Java应用程序都有一个Runtime实例,使应用程序能够与其运行的环境相连接。
由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。
懒汉式单例。不需要事先创建好,只要在第一次真正用到的时候再创建就可以了。因为很多时候并不常用Java的GUI和其中的对象。如果使用饿汉单例的话会影响JVM的启动速度。
如果要求一个类只能产生固定数量的实例,这种需要产生固定数量的模式叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定内存中有多少个实例,方便系统进行扩展,修正单例可能存在的问题,提供系统响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时可以快速响应
单例模式比较简单,应用也比较广泛,如在Spring中,每个Bean默认就是单例,这样做的优点是Spring容器可以管理这些Bean的生命周期,决定什么时候创建出来,什么时候销毁,销毁的时候如何处理,等等。如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期。