定义
确保一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实例)。
类型
创建型
为什么要用单例模式?
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
使用单例模式的好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
单例模式的三个要点:
- 构造方法私有化;
- 实例化的变量引用私有化;
- 获取实例的方法共有
角色
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。
单例模式的实现方式:
1.饿汉式(线程安全)
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
只能通过getInstance()来获取已经new好的实例,所以没有线程安全问题
优点:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全
缺点:没有懒加载,启动较慢;如果从始至终都没使用过这个实例,则会造成内存的浪费。
1.1饿汉式变种(线程安全)
将类实例化的过程放在了静态代码块中,在类装载的时执行静态代码块中的代码,初始化类的实例。
public class Singleton {
private static Singleton singleton;
static{
singleton = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
2.懒汉式(线程不安全)
单重检查(线程不安全),只检查有没有被实例化
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:懒加载,启动速度快、如果从始至终都没使用过这个实例,则不会初始化该实例,可节约资源
缺点:多线程环境下线程不安全。if (singleton == null) 存在竞态条件,可能会有多个线程同时进入 if 语句,导致产生多个实例
测试,使用原子类AtomicInteger来统计实例化的个数,写在单例类的构造函数里面,每次调用构造函数,原子类都加一。
public class Singleton {
private static AtomicInteger atomicInteger = new AtomicInteger();
private static Singleton singleton;
private Singleton() {
atomicInteger.getAndAdd(1);
}
public static AtomicInteger getCount() {
return atomicInteger;
}
public static Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("出错啦!");
}
singleton = new Singleton();
}
return singleton;
}
}
Test类
public class Test {
private static final int THREAD_NUMBER = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREAD_NUMBER];
for (int i = 0; i < THREAD_NUMBER; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
}
});
threads[i].setName("线程"+i);
threads[i].start();
}
for (int i = 0; i < THREAD_NUMBER; i++) {
threads[i].join();
}
System.out.println("线程全部执行完成!");
System.out.println("创建了 "+Singleton.getCount()+" 次单例模式类实例");
}
}
如图所示,创建了10个实例,一个线程创建了一个。
只需要给getInstance()方法加synchronized,每次只有一个线程能够进入该代码块,就能解决重复创建问题,但是synchronized在多线性竞争下会升级成重量级锁,并发性能比较差。
public synchronized static Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("出错啦!");
}
singleton = new Singleton();
}
return singleton;
}
但是我们注意到,多线程引发的问题,只涉及到new Singleton()这个代码,所以,我们可以缩小synchronized同步代码的范围,不修饰整个方法,只修饰new Singleton()
public static Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("出错啦!");
}
synchronized (Singleton.class){
singleton = new Singleton();
}
}
return singleton;
}
但是这样还是会存在线程不安全问题,要通过如下方式解决
3.双重检查锁(线程安全)
用volatile修饰instance,再使用双重检查锁,这种单例模式类创建实例是线程安全的。
为什么需要用volatile修饰instance?
简单来说,一个new 方法背后会执行多个指令,由于 JVM 具有指令重排的特性,jvm执行指令时,会重排指令来加速运行速度, 在多线程环境下可能出现 singleton 已经赋值但还没初始化的情况,导致一个线程获得还没有初始化的实例。
volatile 关键字的作用:
- 保证了不同线程对这个变量进行操作时的可见性
- 禁止进行指令重排序
public class Singleton {
private static AtomicInteger atomicInteger = new AtomicInteger();
private static volatile Singleton singleton;
private Singleton() {
atomicInteger.getAndAdd(1);
}
public static AtomicInteger getCount() {
return atomicInteger;
}
public static Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("出错啦!");
}
synchronized (Singleton.class){
if(singleton==null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么加锁之后,还需要再判断一次是否为null呢,也就是为啥要用双重检查锁呢?
比如有两个或多个线程都通过了第一次判断==null,然后第一个线程获取锁,进入同步代码区,new 一个Singleton实例,第一个线程释放锁之后,然后第二个,第三个,第四个…线程进去,如果不设置第二次判断null,就会再new一个实例出来。所以必须有两次判断。
优点:线程安全;延迟加载;效率较高。
4.静态内部类式(线程安全)
/**
* @Author: codingXT
* @Date: 2021-12-04-22:34
* @Description: 静态内部类式实现单例模式
*/
public class Singleton {
private Singleton(){
}
private static class InnerClas{
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return InnerClas.singleton;
}
}
优点:线程安全,延迟加载,效率高。
静态内部类的方式利用了类装载机制来保证线程安全,只有在第一次调用getInstance方法时,才会装载InnerClas内部类,完成Singleton的实例化,所以也有懒加载的效果。
5.枚举式(线程安全)
/**
* @Author: codingXT
* @Date: 2021-12-04-23:19
* @Description: 枚举类实现单例模式
*/
public enum Singleton {
INSTANCE;
private Resource resource;
private Singleton(){
resource = new Resource();
}
public Resource getInstance(){
return resource;
}
}
Test类
public class Test {
public static void main(String[] args){
Resource resource = Singleton.INSTANCE.getInstance();
Resource resource2 = Singleton.INSTANCE.getInstance();
}
}
上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 Singleton.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
优点:通过JDK1.5中添加的枚举来实现单例模式,写法简单,且不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
《Effective Java》:单元素的枚举类型已经成为实现Singleton的最佳方法。
放一张总结图,图片来源于:https://www.pdai.tech/md/dev-spec/pattern/2_singleton.html
单例模式总结:
优点:
- 由于单例模式只生成了一个实例,所以能够节约系统资源、减少性能开销、提高系统效率
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
缺点:
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
适用场景:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器(比如数据库的主键不能重复,因此该序列号生成器必须具备唯一性,可以通过单例模式来实现。),或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式
Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。
Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码如下
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
}
}
}
我们上述的多种单例的创建方式有很多都存在漏洞,被攻击时会产生多个对象,破坏了单例模式。
- 序列化破坏单例模式以及如何防御
- 反射破坏单例模式以及如何防御
- 为什么要用枚举类实现单例模式(避免反射、序列化问题)
References:
-
https://whirlys.blog.csdn.net/article/details/85965063
-
https://blog.csdn.net/qq_37960603/article/details/104048354
-
https://blog.csdn.net/yy254117440/article/details/52305175
-
https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/singleton.html
-
https://javaguide.cn/systemdesign/framework/spring/Spring%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%80%BB%E7%BB%93/#%E5%B7%A5%E5%8E%82%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
-
https://www.pdai.tech/md/dev-spec/pattern/2_singleton.html