共计 3127 个字符,预计需要花费 8 分钟才能阅读完成。
本文曾经收录到 Github 仓库,该仓库蕴含 计算机根底、Java 根底、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享 等外围知识点,欢送 star~
Github 地址:https://github.com/Tyson0314/…
单例模式
单例模式(Singleton),目标是为了保障在一个过程中,某个类有且仅有一个实例。
因为这个类只有一个实例,所以不能让调用方应用 new Xxx()
来创立实例。所以,单例的构造方法必须是private
,这样就避免了调用方本人创立实例。
单例模式的实现须要 三个必要的条件:
- 单例类的 构造函数 必须是 公有的,这样能力将类的创立权管制在类的外部,从而使得类的内部不能创立类的实例。
- 单例类通过一个 公有的动态变量 来存储其惟一实例。
- 单例类通过提供一个 公开的静态方法,使得内部使用者能够拜访类的惟一实例。
另外,实现单例类时,还须要思考三个问题:
- 创立单例对象时,是否 线程平安。
- 单例对象的创立,是否 延时加载。
- 获取单例对象时,是否须要 加锁。
上面介绍几种实现单例模式的形式。
饿汉模式
JVM 在类的初始化阶段,会执行类的静态方法。在执行类的初始化期间,JVM 会去获取 Class 对象的锁。这个锁能够同步多个线程对同一个类的初始化。
饿汉模式只在类加载的时候创立一次实例,没有多线程同步的问题。单例没有用到也会被创立,而且在类加载之后就被创立,内存就被节约了。
public class Singleton {private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton newInstance() {return instance;}
}
饿汉式单例的 长处:
- 单例对象的创立是 线程平安 的;
- 获取单例对象时 不须要加锁。
饿汉式单例的 毛病 :单例对象的创立,不是 延时加载。
懒汉式
与饿汉式思维不同,懒汉式反对延时加载,将对象的创立提早到了获取对象的时候。不过为了线程平安,在获取对象的操作须要加锁,这就导致了低性能。
public class Singleton {
private static final Singleton instance;
private Singleton () {}
public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();
}
return instance;
}
}
上述代码加的锁只有在第一次创建对象时有用,而之后每次获取对象,其实是不须要加锁的(双重查看锁定优化了这个问题)。
懒汉式单例 长处:
- 对象的创立是线程平安的。
- 反对延时加载。
懒汉式单例 毛病:
- 获取对象的操作被加上了锁,影响了并发性能。
双重查看锁定
双重查看锁定将懒汉式中的 synchronized
办法改成了 synchronized
代码块。如下:
public class Singleton {
private static volatile Singleton instance = null; //volatile
private Singleton(){}
public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();
}
}
}
return instance;
}
}
双重校验锁先判断 instance 是否曾经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
instance 应用 static 润饰的起因:getInstance 为静态方法,因为静态方法的外部不能间接应用非动态变量,只有动态成员能力在没有创建对象时进行初始化,所以返回的这个实例必须是动态的。
为什么两次判断instance == null
:
Time | Thread A | Thread B |
---|---|---|
T1 | 查看到 instance 为空 |
|
T2 | 查看到 instance 为空 |
|
T3 | 初始化对象A |
|
T4 | 返回对象A |
|
T5 | 初始化对象B |
|
T6 | 返回对象B |
new Singleton()
会执行三个动作:分配内存空间、初始化对象和对象援用指向内存地址。
memory = allocate(); // 1:调配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置 instance 指向刚调配的内存地址
因为指令重排优化的存在,导致初始化对象和将对象援用指向内存地址的程序是不确定的。在某个线程创立单例对象时,会为该对象调配了内存空间并将对象的字段设置为默认值。此时就能够将调配的内存地址赋值给 instance 字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用 getInstance,取到的是未初始化的对象,程序就会出错。volatile 能够禁止指令重排序,保障了先初始化对象再赋值给 instance 变量。
Time | Thread A | Thread B |
---|---|---|
T1 | 查看到 instance 为空 |
|
T2 | 获取锁 | |
T3 | 再次查看到 instance 为空 |
|
T4 | 为 instance 分配内存空间 |
|
T5 | 将 instance 指向内存空间 |
|
T6 | 查看到 instance 不为空 |
|
T7 | 拜访instance (此时对象还未实现初始化) |
|
T8 | 初始化instance |
双重查看锁定单例 长处:
- 对象的创立是线程平安的。
- 反对延时加载。
- 获取对象时不须要加锁。
动态外部类
它与饿汉模式一样,也是利用了类初始化机制,因而不存在多线程并发的问题。不一样的是,它是在内部类外面去创建对象实例。这样的话,只有利用中不应用外部类,JVM 就不会去加载这个单例类,也就不会创立单例对象,从而实现懒汉式的提早加载。也就是说这种形式能够同时保障提早加载和线程平安。
基于类初始化的计划的实现代码更简洁。
public class Instance {
private static class InstanceHolder {public static Instance instance = new Instance();
}
private Instance() {}
public static Instance getInstance() {return InstanceHolder.instance ; // 这里将导致 InstanceHolder 类被初始化}
}
如上述代码,InstanceHolder
是一个动态外部类,当外部类 Instance
被加载的时候,并不会创立 InstanceHolder
实例对象。
只有当调用 getInstance()
办法时,InstanceHolder
才会被加载,这个时候才会创立 Instance
。Instance
的唯一性、创立过程的线程安全性,都由 JVM 来保障。
动态外部类单例 长处:
- 对象的创立是线程平安的。
- 反对延时加载。
- 获取对象时不须要加锁。
枚举
用枚举来实现单例,是最简略的形式。这种实现形式通过 Java 枚举 类型自身的个性,保障了实例创立的线程安全性和实例的唯一性。
public enum Singleton {INSTANCE; // 该对象全局惟一}
最初给大家分享一个 Github 仓库,下面有大彬整顿的 300 多本经典的计算机书籍 PDF,包含 C 语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生 等,能够 star 一下,下次找书间接在下面搜寻,仓库继续更新中~
Github 地址:https://github.com/Tyson0314/…