👉本文章全部文字纯原创,若是须要转载,请注明转载出处,谢谢!😘java
哈喽,你们好,我是高冷就是范儿,又和各位见面了。😎从今天开始,我将正式开始设计模式系列文章的写做分享。今天要和你们分享的是GOF23模式中第一个模式——单例模式。这个模式号称是GOF23全部设计模式中最简单的一个设计模式。不过,等你看完这篇文章后会发现,这句话听听就好,别当真。😂单例模式简单吗?这是不存在的,要想吃透里面的细节并不容易,尤为是初学者。可是单例模式在实际生活和开发中,倒是大量的被使用到,所以,这个模式咱们是须要深刻学习掌握的。下面不废话直入主题。git
单例模式属于上篇说过的设计模式三大分类中的第一类——建立型模式,天然是跟对象的建立相关,并且听这名字——单例,也就是说,这个模式在建立对象的同时,还致力于控制建立对象的数量,是的,只能建立一个实例,多的不要。或者从这一方面讲,它确实是最简单的模式。每一个Java程序员都知道,Java中的对象都是使用new关键字来加载类并在堆内存中开辟空间建立对象,这是平时用到最多建立对象的方式。也知道每次new都会产生一个全新的对象。一直这样用着,好像历来没以为有啥很差,更没有怎么思考过,这玩意竟然还要去控制它的数量。程序员
👉那么问题来了,到底咱们为何要控制对象建立的个数?直接new一下多省事啊❓github
既然这个模式存在而且大量使用,说明有些场景下,没它还真不行。那么什么场景下会没它不行呢?我举个栗子🌰,好比咱们平时使用的Windows上的回收站,是否是只有一个?要是有多个,会发生什么?我刚把回收站清空了,换到另外一个回收站看垃圾还在,那这垃圾究竟是在,仍是不在?是否是很诡异了?另外好比博客上会有一个博客访问人数统计,这个东西要是否是单例的会有啥问题?今天统计了流量有100个,次日用了一个新的计数器,又回到0了从新开始统计,那这个统计还有意义吗?数据库
也就是说,有些场景下,不使用单例模式,会致使系统同一时刻出现多个状态缺少同步,用户天然没法判断当前处于什么状态设计模式
在技术领域,单例模式的场景更是不可胜数。缓存
好比XXX池的基本都是单例,为何呢?对象的建立是一个比较耗时耗费资源的过程,尤为是像线程、数据库链接等,都属于使用很是频繁,可是建立销毁又很是耗时的资源,若是不使用池来控制其数量和建立,会对性能形成极大的影响。另外,像线程池中的线程,可能会须要相互通讯,若是不是在同一个池中,对通讯也会有影响。安全
经过控制建立对象的数量,能够节约系统资源开销多线程
另外像应用中的日志系统,通常也会采用单例模式。这样全部的日志都会统一追加,方便后续管理。并发
读取配置的类通常会使用一个单例去统一加载读取。由于通常配置只会在应用启动时加载一次,并且会须要给整个应用全部对象共享。
全局数据共享
还有在各大主流开源框架以及JDK源码当中,也是大量使用到这种模式。后续我也会抛砖引玉给你们举两个例子。
正是存在上这些痛点,使得有时候咱们建立对象还真不能再简单任性直接new一下,须要对其作一些精细控制。那怎么才能控制只建立一个对象呢?
通过无数前人总结,通常有如下这些经典的解决方案,
啥叫饿汉式?饿了就马上想到吃,类比到建立对象也是如此,当类一初始化,该类的对象就马上会被实例化。
👉怎么实现?
代码以下:
public class HungrySingleton {
private HungrySingleton() {} ❶
private static HungrySingleton instance = new HungrySingleton();❷
public static HungrySingleton getInstance() {
return instance;
}
}
复制代码
当外部调用HungrySingleton.getInstance()
时,就会返回惟一的instance
实例。为何是惟一的?
这代码中有几个要点
private
修饰,防止外部手动经过new建立。后面的例子都须要这样,后面就不解释了。instance
使用static
修饰,而后调用new建立对象,咱们知道static
修饰的东西都属于类,并且在类加载阶段就已经被加载,而且只能被加载一次。就是类加载这种特性很好的保证了单例的特性,也自然防止了并发的问题。卧槽,单例模式竟然如此简单,这么轻松就完成一个,这就算完事了?呵呵......
这个代码确实实现了单例的效果,只要调用HungrySingleton.getInstance()
,你就算是神仙也造不出第二个对象......(其实后面会知道,仍是有办法的)
👉可是想一想,这个方法有啥问题没?
没错,一旦类初始化时就会建立这个对象,有人说,建立就建立呗,这有啥大不了的?大部分状况下确实是没啥问题,可是若是建立这个对象极其耗费时间和资源呢?这样必然会形成巨大的性能损耗。
另外还有一种状况,有的时候我只是想单纯的加载一下类,但并不想去用该对象,那这个时候这种模式就属于浪费内存了。什么意思?我举个栗子🌰,以下代码,其余代码和上面同样,就是加了❶行,而后我如今外部调用HungrySingleton.flag
,会发生什么?
public class HungrySingleton {
private HungrySingleton() {}
private static int flag = 1; ❶
private static HungrySingleton instance = new HungrySingleton();
public static HungrySingleton getInstance() {
return instance;
}
}
复制代码
学过Java类加载机制的都知道,当去访问一个类的静态属性的时候会触发该类初始化,这就致使,我明明只是想使用一下flag
属性,并不想用HungrySingleton
对象,但因为你访问了flag致使HungrySingleton
的初始化,从而致使instance
被实例化,形成内存泄露。
看来这种方案可行,但不是完美的,那有啥更好方案,既能保证只建立单个对象,又能够作到真正须要使用该对象时再建立它(延迟加载),历来达到节约系统资源目的?答案固然是有的!
和饿汉相反,懒汉天然是很懒,能不吃就不吃饭,等到实在饿得不行了(须要用该对象了)才去吃饭(建立对象)。
👉怎么实现?
代码以下:
public class LazySingleton {
private static LazySingleton instance = null; ❶
private LazySingleton() {
}
public static LazySingleton getInstance() { ❷
if(instance == null){ ❸
instance = new LazySingleton();
}
return instance;
}
}
复制代码
关注❶处,类加载时不会马上建立对象了,而后当LazySingleton.getInstance()
调用❷处方法时,经过判断instance == null
,若是有了就不建立了,没有就才会建立。
哈哈,既实现了延迟加载,节约资源,又保证了单例,貌似没毛病。飘了~😎
没错,在单线程下面确实如此,惋惜忽略了多线程场景。为何在多线程下会有问题?分析一下,如今有两个线程A和B,同时到达❸处,天然此时instance = new LazySingleton()
这一行没被调用过,天然❸处条件成立,而后A和B同时进入了if{}
代码块,后面的事情就知道了,A和B线程都会调用instance = new LazySingleton()
,从而建立多个对象,破坏了单例。
那怎么办?有并发问题?那就加锁同步呗......
public class LazySingleton {
private LazySingleton() {
}
private static volatile LazySingleton instance = null; ❶
public static synchronized LazySingleton getInstance() { ❷
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
复制代码
这代码中有几个要点
volatile
修饰,这主要是为了保证多线程下内存可见性。由于高速缓存关系,一个线程的修改并不必定要实时同步到另外一线程,volatile
能够用来解决这个问题。synchronized
同步锁,能够保证同一时刻只会有一个线程进入getInstance()
方法,天然只会有一个线程调用instance = new LazySingleton()
,单例天然就保证了。但同时这个带来了一个新问题,由于每一个线程无论instance有没有被建立过,都会去调用getInstance()
,由于if(instance == null)
是须要进入方法后才判断的,然而getInstance()
又是一个同步的,同一时刻只会有一个线程进入,其他线程都必须等待,这就会致使线程阻塞,致使性能降低。上述方法确实实现了延迟建立对象,可是性能低下的问题如何解决?聪明的攻城狮们又想到了新的方案......
👉怎么实现?
代码以下:
public class LazySingleton {
private LazySingleton() {
}
private static volatile LazySingleton instance = null; ❷
public static LazySingleton getInstance(){ ❶
if(instance == null){ ❸
synchronized (LazySingleton.class) { ❺
if(instance == null) ❹
instance = new LazySingleton();❻
}
}
return instance;
}
}
复制代码
这个代码看上去会比较复杂,讲几个关注点:
标注❶处方法内部和上面例子代码最大的区别在于,有❸❹两处if判断。为何要两次判断?
synchronized
将整个方法加同步锁,也就是说,无论外部(线程)在调用getInstance()
方法这一刻该对象是否已经被建立好,都须要阻塞等待。而❸处的if判断就使得,只有此时真的尚未建立出对象才会进入synchronized
代码块,若是已经建立了就直接return了,因此显然提升性能了。synchronized
缘故,只能有一个线程进入,假设A拿到了这把锁,进入synchronized
代码块,而后经过❻建立出了一个LazySingleton
实例,而后离开synchronized
代码块,而后把锁释放了,可是还没等到它return的时候,B线程拿到了这把锁,进入synchronized
代码块,此时要是没有❹处if判断,B线程照样能够来到❻处,以迅雷不及掩耳之势噼里啪啦一顿操做,又建立出一个LazySingleton
实例。显然此时,单例模式已经被破坏了。因此❹处的判断也不可省略。标注❷处看上去和懒汉式的代码没区别,可是这边volatile
语义已经发生改变,已经不单纯是为了内存可见的问题了,还涉及到指令重排序的问题。怎么理解?一切问题出在❻处。震惊!❻处看似日常的一行代码竟然会有问题。是的,下面我来详解。❻处会建立一个LazySingleton
实例,而且赋值给instance
变量,很遗憾,这一个动做在指令层面并不是原子操做。这个动做能够分为4步,
1.申请内存空间
2.初始化默认值
3.执行构造器初始化
4.将instance指向建立的对象
而有些编译器会对代码作指令重排序,由于3和4自己相互并不存在依赖,指令重排序的存在可能会致使3和4顺序发生颠倒。这会有什么问题?首先在单线程下并不会有什么问题,为何?由于指令重排序的前提就是不改变在单线程下的结果,不管先执行3仍是4,最后返回的对象都是初始化好后的。可是在多线程下呢?设想一种极端场景,如今假设A线程拿到锁进入到❻处,而后它完成了上面4步的1和2,由于如今指令重排序了,下面A线程会将instance
指向建立的对象,也就是说,此时instance != null
了!而后正当A要去执行构造器初始化对象时,巧得很,这时候B线程来到❸处,判断instance == null
不成立了,直接返回,独留A线程在原地骂娘“尼玛,我™还没初始化对象呢......”,由于返回了一个没有通过初始化的对象,后续操做天然会有问题。正是由于这个缘由,因此❷处volatile
不可省略,主要缘由就在于防止指令重排序,避免上述问题。
那是否是这样就万无一失了呢?很遗憾,上述如此严密的控制,仍是不能彻底保证出问题。What?那就是上述的作法有个前提,JDK必须是JDK5或更高版本,由于从JDK5才开始使用新的JSR-133内存模型规范,而在这个规范中才加强了volatile
这个语义......
卧槽,原来搞了大半天仍是有问题啊......心好累,并且说实话,就算不考虑JDK版本这个问题,这种方案的实现代码太过丑陋,自己看着就不是很爽,并且考虑的东西太多,稍有闪失就GG了。因此,这种方案虽然分析了这么多,可是其实没有实际意义,实际工做中强烈不建议使用。那还有没有好的方案啊?固然有啊!
根据类加载机制,外部类的初始化并不会致使静态内部类的初始化。
👉怎么验证?
以下代码:
public class Demo {
private static int a = 1;
private static class Inner{
static {
System.out.println("Inner loading ...");
}
}
public static void main(String[] args) {
System.out.println(Demo.a);
}
}
复制代码
经过Demo.a
引用外部类Demo
的静态变量a
,会致使外部类的初始化,若是Inner
被初始化了,必然会执行static
块,从而打印"Inner loading ..."
,然而很遗憾,这个代码执行结果只会打印出“1”
。这也印证了开始的结论。有了这个结论,咱们就能够利用它实现优雅的单例了。哈哈~😍
👉怎么实现?
代码以下:
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
private static class StaticInnerSingletonInstance { ❶
private static final StaticInnerSingleton instance = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() { ❷
return StaticInnerSingletonInstance.instance;
}
}
复制代码
讲几个关注点:
❶处StaticInnerSingletonInstance
是一个静态内部类,内部静态字段instance
负责建立对象。由于上面的结论,因此固然外部类StaticInnerSingleton
初始化时,并不会致使StaticInnerSingletonInstance
初始化,进而致使instance
的初始化。因此实现了延迟加载。
当外部调用❷处getInstance()
时,经过StaticInnerSingletonInstance.instance
对instance
引用才会致使对象的建立。因为static
的属性只会跟随类加载初始化一次,自然保证了线程安全问题。
这个方案算是完美解决了上述全部方案的问题,且保留了全部的优势。算是一个完美方案。
还有没有其它方案?必须的!
用枚举实现单例是最简单的了,由于,Java中的枚举类型自己就自然单例的,
👉怎么实现?
代码以下:
enum EnumSingletonInstance{
INSTANCE;
public static EnumSingletonInstance getInstance(){
return INSTANCE;
}
}
复制代码
惟一遗憾的是,这个方案和饿汉式同样,无法延迟加载。枚举类加载天然就会初始化INSTANCE
。
经常使用的单例模式的方案基本就是这些。那这样是否是就真的万无一失了呢?很遗憾的告诉你们,上述这些方法还不是绝对能保证只建立一个对象。mmp......我擦,我就想玩个单例咋这么累呢?心塞......😭是的,上面的方案除了枚举方案,其他方案均可以被破解。下面咱们来了解一下。
破解单例有两种方法,反射或者反序列化。下面我用代码作简单演示。以饿汉式为例,其他模式同理,你们能够自行测试。
👉怎么破解?
代码以下:
//饿汉式的代码省略,参考前面饿汉式章节
public static void main(String[] args) throws Exception {
System.out.println(HungrySingleton.getInstance());
System.out.println(HungrySingleton.getInstance());
System.out.println("反射破解单例...");
HungrySingleton instance1 = HungrySingleton.class.newInstance();
HungrySingleton instance2 = HungrySingleton.class.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
复制代码
输出结果如图,很清楚的看到单例被破解了。
👉如何防止?
很简单,由于Class.newInstance()
是经过调用HungrySingleton
无参构造器建立对象的,只要在构造器中加入有如下逻辑便可。这样,当类初始化时,第一次正常建立出实例并赋值给instance
。当再想经过反射想要破解单例时,天然会抛出异常阻止继续实例化。
//饿汉式的其它代码,参考前面饿汉式章节
private HungrySingleton() {
if (instance != null) {
try {
throw new Exception("只能建立一个对象!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
👉怎么破解?
另外,经过序列化和反序列化也能够破解单例。(前提是单例类实现了Serializable
接口)代码以下:
public static void main(String[] args) throws Exception {
System.out.println(HungrySingleton.getInstance());
System.out.println(HungrySingleton.getInstance());
System.out.println("反序列化破解单例...");
HungrySingleton instance1 = HungrySingleton.getInstance();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(instance1); //序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
HungrySingleton instance2 = (HungrySingleton) ois.readObject(); //反序列化
System.out.println(instance1);
System.out.println(instance2);
}
复制代码
输出结果如图,很清楚的看到单例也被破解了。
👉如何防止?
也很是简单,只须要在单例类中添加以下readResolve()
方法,而后在方法体中返回咱们的单例实例便可。为何?由于readResolve()
方法是在readObject()
方法以后才被调用,于是它每次都会用咱们本身生成的单实例替换从流中读取的对象。这样天然就保证了单例。
private Object readResolve() throws ObjectStreamException{
return instance;
}
复制代码
关于单例模式的所有内容就是这些,最后来作个总结,那么这么多的单例模式实现方案咱们到底须要选择哪一个呢?技术选型历来不是非黑即白的问题,而是须要根据你的实际应用场景决定的。不过从上述各类单例模式的特色,咱们能够得出如下结论:
从安全性角度考虑,枚举显然是最安全的,保证绝对的单例,由于能够自然防止反射和反序列化的破解手段。而其它方案必定场合下所有能够被破解。
从延迟加载考虑,懒汉式、双重检测锁、静态内部类方案均可以实现,然而双重检测锁方案代码实现复杂,并且还有对JDK版本的要求,首先排除。懒汉式加锁性能较差,而静态内部类实现方法既可以延迟加载节约资源,另外也不须要加锁,性能较好,因此这方面考虑静态内部类方案最佳。
👉通常选用原则
- 单例对象占用资源少,不须要延时加载:枚举式好于饿汉式。
- 单例对象占用资源大,须要延时加载:静态内部类式好于懒汉式。
前面也提到,单例模式在开源框架中被使用的很是之多,下面我就抛砖引玉挑选几处给你们讲解一下,
下面这个代码截取自Mybatis中,这是一个典型的使用静态内部类方式实现单例。
public abstract class VFS {
... //省略大量无关代码
private static class VFSHolder {
static final VFS INSTANCE = createVFS();
static VFS createVFS() {
... //省略建立过程
}
}
... //省略大量无关代码
public static VFS getInstance() {
return VFSHolder.INSTANCE;
}
... //省略大量无关代码
}
复制代码
在JDK底层也是大量使用了单例模式,好比,Runtime
类是JDK中表示Java运行时的环境的一个类,其内部实现也是采用单例模式,由于一个应用程序只须要一个运行时的环境便可,而且是采用饿汉式方式实现。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {
}
}
复制代码
在结束以前,我再多说一句,网上有些文章常常会用Spring的单例bean和Mybatis中的ErrorContext
类做为单例模式的例子,其实这是有问题的。
首先,Spring中的单例bean跟本文讲的单例模式并无关系,不是一回事,可能也就名字比较像,这也是容易混淆的地方,实现方式固然也就天差地别了。Spring的单例bean是经过将指定类首次建立的bean进行缓存,后续去获取的时候,若是设置为singleton
,就直接会从缓存中返回以前缓存的对象,而不会建立新对象。可是这个是有前提的,那就是在同一个容器中。若是在你的JVM中存在多个Spring容器,该类也就会建立多个实例了。因此这是不能算是真正的单例模式。本文上述描述的单例模式是指JVM进程级别的,也就是说,只要是在同一个JVM中,单例类只会存在一个对象。
Mybatis中的ErrorContext
类中采用的是ThreadLocal
机制保证同一个线程跟惟一一个ErrorContext
实例绑定,可是这个也是有前提的,那就是在线程范围内的,在每个线程内部,确实作到了只建立一个实例,可是从应用级别或者JVM级别依然不是单例,因此不能将其称之为单例模式。
一言以蔽之,真正的单例模式,是指JVM进程级别的!
好了,今天关于单例模式的技术分享就到此结束,下一篇我会继续分享另外一个设计模式——工厂模式,一块儿探讨设计模式的奥秘。我们不见不散。😊👏