一篇文章让你彻底搞懂设计模式之单例模式

1.什么是单例模式

单例模式就是确保一个类在任何情况下只有一个实例,并对外提供一个全局访问点,打个比喻来说 :
单例模式就是你,我只有你这一个对象,你是独一无二的。全世界就一个,每个人要的时候都抢的是同一个,这是单例的概念
单例模式同时也是23种设计模式中最简单的一种设计模式

2.单例模式的介绍及实现方式

在我们对单例模式有基本的想法和概念之后,我们来逐步深入的领略一下单例模式的魅力。
既然单例模式只允许有一个实例,那么主要解决的是什么问题呢?

大家应该可以猜到,既然限定只允许有一个实例,那么就是针对类频繁创建和销毁来设计的。当你想控制实例的数目,减少系统运行时消耗的资源时,可以使用这种设计模式来让我们的程序更健壮。

那么如何实现与控制呢??
就是判断系统是否已经有这个单例,如果有则返回,如果没有则创建。听起来是不是感觉用起来会很轻松呢

只写理论是不是容易看不明白,我们来在实践中理解一下吧

实现方式

首先请你在不参考任何资料情况下回想一下单例模式有多少种实现方式?
懒汉饿汉枚举双重检查锁内部静态类等等
你们有疑问了,为什么这么简单的模式会有那么多种实现方式,因为在实际情况下,会有不同的使用环境,每种实现方式都是支持多线程的,区别就是他们在性能上会有差异。
我们来把它们串一串,捋一捋。

首先我们来看看下面这一段代码,也是我们最常用的。

public class HungrySingleton{
	//类被加载的时候,这个单例对象就会被同步创建
	private static final HungrySingleton instance = new HungrySingleton();
	//我们让构造方法进行私有化,不对外暴露
	private HungrySingleton(){}
	//提供一个全局访问点
	public final static HungrySingleton getInstance(){
		return instance;
	}
}

这种方式就是俗称的饿汉模式,不管用不用都要创建
在类被加载的时候,创建这个实例对象,这种方式有什么优点呢
优点肯定是有的,比如说,这种方式能保证绝对的线程安全,同时,代码结构也很简单可读性也比较好
是不是没有缺点呢,当然也有,就是可能造成性能浪费反射或者克隆时候遭到破坏,大量创建饿汉式单例对象时,内存会被大量消耗浪费

那有没有办法不一次性创建这么多的单例对象呢,比如说用的时候咱再创建,不用的时候扔一边不去创建。
那么此时懒汉模式就出来了,我们看下面代码

public class LazySimpleSingleton{
	//类被加载的时候,单例对象为null,先不创建实例
	private static LazySimpleSingleton instance = null;
	//我们让构造方法进行私有化,不对外暴露
	private LazySimpleSingleton(){}
	//提供一个全局访问点,先判断是否为空再创建实例
	public static LazySimpleSingleton getInstance(){
		if(null == instance){
		instance = new LazySimpleSingleton();
		}
		return instance;
	}
}

这种方式实现起来也是非常的容易。是不是已经完美的解决单例创建的问题了呢。其实并没有
我们要怎么测试一下结果是怎么样的呢?看看是否能按照我们想象的去执行。
我们可以创建两个线程来模拟一下并发场景,测试一下是否这种方式真的能解决我们的问题。
获取实例的方式是这样的:
在这里插入图片描述
我们先创建两个线程并开启线程:
在这里插入图片描述
在这里插入图片描述
我们先执行一下:
在这里插入图片描述
在这里插入图片描述
我们发现在多次执行后会出现两种结果,此时我们想象一下会有哪些可能?
第一种: 打印结果一致

  1. 两个线程按顺序执行,一前一后
  2. 两个线程同时进入,后者覆盖了前者

第二种: 打印结果不一致

  1. 两个线程同时进入,打印执行顺序一前一后

此时我们得到了两种结果,我们打断点测试一下吧:

  1. 两个线程按顺序执行,一前一后
    在这里插入图片描述
    第一个线程执行时为空,会创建实例,执行完后我们再执行第二个线程在这里插入图片描述
    此时判断为空时直接跳过实例的创建,我们看下线程的状态:
    在这里插入图片描述
    两个线程都为running状态
    下一步我们让两个线程同时进入

在这里插入图片描述
在这里插入图片描述
我们会发现线程1为等待状态,后者的值覆盖了前者的值,两者地址一样。
此时我们让 两个线程同时进入,打印执行顺序一前一后
我们会发现此时的地址值是不一样的,会创建两个实例。
懒汉模式的优点是什么呢?
优点:实现了所谓的延迟加载,用的时候再创建,可以实现线程安全避免内存浪费等,
那有没有缺点呢?
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率
这种方式使用的相对来说没有那么的多。

那我想即延迟加载又想保证单例模式该怎么办呢???
那我们现加一把锁试试
在这里插入图片描述
我们在方法上加了一把锁,那么效果怎么样呢,我锁住类不放开,会不会绝对的单例呢,
此时我们再断点跑一下,控制两个线程同时进入
在这里插入图片描述
在这里插入图片描述
我们会发现创建实例会堵到加锁的方法上,也会发现两个线程同时进入加锁的方法会对实例重复赋值

那么我们要怎么办呢?此时我们已经离胜利不远了
如果我们在进入加锁的方法时判断一下实例是否为空,为空则进入创建实例,不为空则取到实例返回,此时可以避免对象的重复赋值问题,同时我们要在实例定义的时候加上volatile关键字保证原子的可见性。

private volatile static LazyDoubleCheckSingletin lazyDoubleCheckSingletin ;  

在这里插入图片描述
这种采用双锁机制,安全且在多线程情况下能保持高性能
这就是double-checked 双重检查锁的魅力
老样子,我们看一下这种方式的优缺点
优点保证了性能,解决了线程安全的问题
缺点:比较繁琐,可读性不是很好,不太优雅

那怎么样更优雅一点呢??
我们看下下面的代码:
在这里插入图片描述
和双重检查锁方式看起来哪种更优雅一些呢???
没错,是下面的这种,静态内部类模式
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程

它跟 饿汉模式不同的是:饿汉模式只要 类被加载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是类被装载了,instance 不一定被初始化。因为 LazyHolder类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 LazyHolder类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在类加载时就实例化,因为不能确保类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

大家理解了嘛???有没有变成小迷糊。。。

还是给大家大概总结一下吧

一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 延迟加载效果时,才会使用静态内部类方式。如果有其他特殊的需求,可以考虑使用双重检查锁方式。

好了,小伙伴们哪里不懂了或者有宝贵意见欢迎提出来呦,共同学习共同进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值