开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情
觉得对你有益的小伙伴记得点个赞+关注
后续完整内容持续更新中
希望一起交流的欢迎发邮件至javalyhn@163.com
本文打📌的方法是推荐用法
0. 前言🎇
大家在网上可以随处可见单例模式的说明,但是我为什么还要写一下这篇文章呢,因为我想把单例模式说得更透彻一些,加上一些考题,让大家知道在源码级别,框架里面哪里使用的单例模式,并且是下单例模式种的细节,我也想一并给大家解释清楚。
1. 单例模式定义
单例模式在设计模式中是一个比较简单的设计模式,它确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
2. 单例模式使用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式
- 要求生成唯一序列号的场景
- 在整个项目中需要一个
共享访问点和共享数据,例如一个Web页面上的计数器,可以不用每次刷新都记录到数据库中,使用单例模式保持计数器的值,并且确保是线程安全的 - 创建一个对象需要
消耗的资源过多,如需要访问IO数据和数据库等资源 - 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)
3. 单例模式8种实现方法
3.1 饿汉式(静态常量)
public class Singleton1 {
public static void main(String[] args) {
//测试
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2); // true
System.out.println(instance1.hashCode() == instance2.hashCode());//true
}
}
class Singleton{
//1 构造器私有化
private Singleton(){}
//2 在本类内部创建对象实例
private final static Singleton instance = new Singleton();
//3 提供一个公有的静态方法 返回实例对象
public static Singleton getInstance() {
return instance;
}
}
优缺点说明
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费这种方式基于
classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果结论:这种单例模式可用,可能造成
内存浪费
3.2 饿汉式(静态代码块)
public class Singleton1 {
public static void main(String[] args) {
//测试
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2); // true
System.out.println(instance1.hashCode() == instance2.hashCode());//true
}
}
class Singleton2{
//1 构造器私有化
private Singleton2(){}
//2 在本类内部创建对象实例
private static final Singleton2 instance;
static { //在静态代码块中 创建单例对象
instance = new Singleton2();
}
//3 提供一个公有的静态方法 返回实例对象
public static Singleton getInstance() {
return instance;
}
}
优缺点同上
3.3 懒汉式(线程不安全)
public class SingletonTest03 {
public static void main(String[] args) {
//测试
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2); // true
System.out.println(instance1.hashCode() == instance2.hashCode());//true
}
}
class Singleton {
private static Singleton instanse;
private Singleton(){};
//提供一个静态公有方法 当使用到该方法时 才去创建
//懒汉式
public static Singleton getInstance() {
if(instanse == null){
instanse = new Singleton();
}
return instanse;
}
}
优缺点说明
优点:起到了lazy loading的效果,但是只能在单线程下使用
缺点:如果在多线程下,一个线程进入了if(singleton==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式结论: 在实际开发中不要使用这种方法
3.4 懒汉式(线程安全)
class Singleton {
private static Singleton instanse;
private Singleton(){};
//提供一个静态公有方法 加入了同步处理的代码 解决线程安全问题
//懒汉式
public static synchronized Singleton getInstance() {
if(instanse == null){
instanse = new Singleton();
}
return instanse;
}
}
优缺点说明
优点:解决了线程不安全问题
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低结论: 在实际开发中,不推荐使用这种方式
3.5 懒汉式(同步代码块)
class Singleton {
private static Singleton instanse;
private Singleton(){};
//提供一个静态公有方法 加入了同步处理的代码 解决线程安全问题
//懒汉式
public static Singleton getInstance() {
if(instanse == null){
synchronized() {
instanse = new Singleton();
}
}
return instanse;
}
}
优缺点说明:
这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
但是这种同步
并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例结论:在实际开发中,不能使用这种方式
3.6 双重检查(DoubleCheck)📌
class Singleton {
private static volatile Singleton instanse;
private Singleton(){};
//提供一个静态公有方法 加入了双重检查代码 同时解决懒加载问题 解决线程安全问题
//同时保证效率 推荐使用
public static Singleton getInstance() {
if(instanse == null){
synchronized (Singleton.class) {
if(instanse == null) {
instanse = new Singleton();
}
}
}
return instanse;
}
}
推荐原因
Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,
也避免的反复进行方法同步.线程安全;延迟加载;效率较高
结论:
在实际开发中,推荐使用这种单例设计模式
3.7 静态内部类📌
class Singleton {
private static Singleton instanse;
//构造器私有化
private Singleton(){};
//写一个静态内部类 该类中有个静态属性Singleton
private static class SingletonInstance{
private static final Singleton Instance = new Singleton();
}
//提供一个静态公有方法 直接返回SingletonInstance.Instance
public static Singleton getInstance() {
return SingletonInstance.Instance;
}
}
有关用静态内部类为何是线程安全的会在文末说明
推荐原因
这种方式采用了
类装载的机制来保证初始化实例时只有一个线程。静恣内部类方式在singleton类被装载时
并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。类的
静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。优点: 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
结论:推荐使用.
3.8 枚举📌
public class SingletonTest {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
System.out.println(instance == instance1);//true
}
}
//使用枚举方式实现单例模式
enum Singleton {
INSTANCE;//属性
public void sayOk(){
System.out.println("ok~");
}
}
推荐原因
这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能
防止反序列化重新创建新的对象。这种方式是Effective Java作者Josh Bloch提倡的方式
结论:推荐使用
4. 单例模式的优点
- 由于单例模式在内存中
只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。 - 由于单例模式只生成一个实例,所以减少了系统的
性能开销,当个对象的产生需要比 较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生 一个单例对象, 然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要 注意JVM垃圾回收机制)。 - 单例模式可以避免对资源的
多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。 - 单例模式可以在
系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
5. 单例模式的缺点
- 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径。它为什么不能增加接口呢?
因为接口对单例模式是没有任何意义的。当然,在特殊情况下,单利模式可以实现接口,被继承等,需要在特点环境下考虑。 - 单例模式对
测试是不利的。在并行开发环环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式许你一个对象。 - 单例模式与单一职责原则有冲突。
一个类应该只实现一个逻辑,而不关心他是否是单例,是不是单例要取决于环境。单例模式把“要单例”和业务逻辑混合在了一起。
6. 问题补充
6.1 为什么静态内部类可以保证线程安全
class Singleton {
private static Singleton instanse;
//构造器私有化
private Singleton(){};
//写一个静态内部类 该类中有个静态属性Singleton
private static class SingletonInstance{
private static final Singleton Instance = new Singleton();
}
//提供一个静态公有方法 直接返回SingletonInstance.Instance
public static Singleton getInstance() {
return SingletonInstance.Instance;
}
}
静态内部类的优点是:当外部类被加载时,内部类不会被加载,则不会去初始化单例对象,因此不占内存,只有当第一次被调用时,才会导致虚拟机加载内部类。
那么他是如何实现线程安全的呢?
首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的<clinit>方法。
<clinit>方法: 这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的<clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>方法。然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>()方法。也就是说同一个加载器下,一个类型只会初始化一次。
那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个<clinit>代码,当我们执行getInstance方法的时候,会导致SinglenInstance类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的<clinit>代码也只会被执行一次,所以他只会有一个实例。
假设有一个类A,那么什么时候A会被初始化?
- T 是一个类,而且一个 T 类型的实例被创建;
- T 是一个类,且 T 中声明的一个静态方法被调用;
- T 中声明的一个静态字段被赋值;
- T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段;
- T 是一个顶级类(top level class,见 java 语言规范的§7.6),而且一个断言语句嵌套在 T 内部被执行。
6.2 为什么枚举可以实现线程安全
- 枚举类型T不可被继承
- T中所有属性都被
static final修饰,天然支持多线程,原因如下
static 类型的属性会在
类加载过程初始化, 当一个 Java 类第一次被真正使用到的时候静态资源被初始化、 Java 类的加载和初始化过程都是线程安全的( 因为虚拟机在加载枚举的类的时候, 会使用 ClassLoader 的 loadClass 方法, 而这个方法使用同步代码块保证了线程安全) 。 所以, 创建一个 enum 类型是线程安全的。
6.3 clone()问题
对象的复制情况我们也需要考虑到,在Java中,对象默认是不可以复制的,只有实现了Cloneable接口并且重写了clone方法才可以直接通过对象创建一个新对象。对象复制不依赖于构造函数,因此即使是私有的构造函数,对象仍然可以被复制。因此解决该问题的最好方法就是单例类不要实现Cloneable接口。
7. 面试问题
- 系统环境信息(System.getProperties())?
- Spring中怎么保持组件单例的?
- ServletContext是什么(封装Servlet的信息)?是单例吗?怎么保证?
- ApplicationContext是什么?是单例吗?怎么保证?
- ApplicationContext: tomcat:一个应用(部署的一个war包)会有一个应用上下文
- ApplicationContext: Spring:表示整个IOC容器(怎么保证单例的)。ioc容器中有很多组件(怎么保证单例)
- 数据库连接池一般怎么创建出来的,怎么保证单实例?
如果小伙伴们感兴趣,我会发文章讲解这些问题!!!