说到设计模式,面试排在第一位的十有八九是单例模式,这一定是大部分人从入门到面试工作都避不开的基础知识。
但单例模式不仅有懒汉模式和饿汉模式两种写法,往往我们掌握的都是最基础的写法,如果你有阅读过类似spring这样的知名框架源码,一定会发现他们的单例模式写法和你所掌握的完全不同。
本篇就给大家带来单例模式从基础->最优->额外推荐的写法,帮助你面试疯狂加分。
饿汉模式简单理解就是提前创建好了对象
优点:写法简单,没有线程同步的问题
缺点:因为要提前创建好对象,不管使用与否都一直占着内存
推荐:对象较小且简单则使用饿汉模式
public final class Singleton {
// 创建好实例
private static Singleton instance = new Singleton();
// 构造函数
private Singleton() {}
// 获取实例
public static Singleton getInstance() {
return instance;
}
}
懒汉模式简单理解就是在需要时才创建对象
优点:懒加载方式性能更高
缺点:要考虑多线程的同步问题
推荐:只要不符合上面饿汉的推荐使用条件则都使用懒汉模式
public final class Singleton {
private static Singleton instance = null;
// 构造函数
private Singleton() {}
// 获取实例
public static Singleton getInstance() {
// 为null时才实例化对象
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
上面介绍了懒汉的缺点是多线程同步的问题,那么马上就能想到使用同步锁来解决这个问题。
这里使用synchronized关键字且通过代码块来降低锁粒度,最大程度保证了性能开销,其实从java8以后,synchronized的性能已经有了较大提升。
public final class Singleton {
private static Singleton instance = null;
// 构造函数
private Singleton() {}
// 获取实例
public static Singleton getInstance() {
// 获取对象时加上同步锁
if (null == instance) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
上面虽然使用了同步锁代码块,勉强解决了线程同步的问题且性能开销做了最大程度的优化,可实际上在多线程环境下仍然存在线程安全问题。
当依然有多个线程进入到if判断里面时,这个线程安全问题还是存在,虽然这种情况并非一定出现,可极端情况下出现的几率非常大。
这个时候就需要使用面试中关于设计模式很喜欢问到的DCL即双重检查锁模式,听起来很高大上,其实就是多加了一层判断。
说白了,就是在进入同步锁之前和之后分别进行了检查,极大降低了线程安全问题。
public final class Singleton {
private static Singleton instance = null;
// 构造函数
private Singleton() {}
// 获取实例
public static Singleton getInstance() {
// 第一次判断,当instance为null时则实例化对象
if(null == instance) {
synchronized (Singleton.class) {
// 第二次判断,放在同步锁中,当instance为null时则实例化对象
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查锁方式是单例懒汉模式在多线程下处理安全问题的最佳方案之一,但上面依然不是最优写法。
这里就要引出一个「指令重排」的概念,这个概念是java内存模型中的,我这里用最简洁的方式帮你理解。
Java中new一个对象在内存中执行指令的正常顺序是:分配 -> 创建 -> 引用,而多线程环境下,JVM出于对语句的优化,有可能重排顺序:分配 -> 引用 -> 创建。
如果出现这种情况,那么上面的双重检查锁方式依然无法解决线程安全问题。
解决方式很简单,加个volatile关键字即可。
volatile关键字作用:保证可见性和有序性。
public final class Singleton {
// 加上volatile关键字
private volatile static Singleton instance = null;
// 构造函数
private Singleton() {}
// 获取实例
public static Singleton getInstance() {
// 第一次判断,当instance为null时则实例化对象
if(null == instance) {
synchronized (Singleton.class) {
// 第二次判断,放在同步锁中,当instance为null时则实例化对象
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
《Effective Java》是Java业界非常受欢迎的一本书,对于想要在Java领域深耕的程序员来讲,这本书没有不看的理由,相信很多Java程序员不管看过还是没看过,都有听过这本书。
而这本书的作者,所推荐的一种单例设计模式写法,就是枚举方式。
原理十分简单,在Java中枚举类的域在编译后会被声明为static属性,而JVM会保证static修饰的成员变量只被实例化一次。
public class Singleton {
// 构造函数
private Singleton() {
}
// 从枚举中获取实例
public static Singleton getInstance() {
return SingletonEnum.SINGLETON.getInstance();
}
// 定义枚举
private enum SingletonEnum {
SINGLETON;
private Singleton instance;
// JVM保证这个方法只调用一次
SingletonEnum() {
instance = new Singleton();
}
public Singleton getInstance() {
return instance;
}
}
}
最后这里稍微提一下,以免部分人对于设计模式感到些许负担。
单例模式其实很简单,饿汉模式和懒汉模式在许多开源框架中应用都比较广泛,甚至饿汉模式用的更多,比如Java的Runtime类中就这么干的,简单粗暴,有兴趣的可以自己看下源码。
难道这些框架的作者就意识不到本篇中讲述的问题吗,并非如此,用哪种方式编写单例模式往往视情况而定,一些理论上会发生的问题往往实际中可以忽略不计,此时更倾向于使用最简单直接的写法。
真正难的其实还是面试,不少关于单例模式的问题中喜欢问到它的几种写法、存在的问题以及最佳方案,说白了还是面试造核弹,进厂拧螺丝,目的是想知道你对设计模式的了解程度,从而评判你研究该学科的态度及造诣。
因此,大家看完本篇可以手动尝试着写一写,了解一些也就够了,没必要过分深究,因为Java领域需要花费精力的地方确实太多了。
最后说下我的一点心得,所谓设计模式固然能给Java代码本身带来更多优雅,但是写了很多年Java代码,我大体还是觉得Java本身的装饰实在太多,优雅换来的往往是代码本身的负担。
我参与过的研发团队中,几乎都能见到许多工程师编写的比较优雅的代码,一些设计模式也写的很好,可带来的问题也很明显,就是可读性越来越差,要求每个团员都对Java有较高的造诣,甚至在某些时候给人力资源带来压力,这从实际角度考虑是不妥的。
我更多的建议是,应对面试或学习好好领悟设计模式,百利而无一害,但实际工作中尽量少用复杂的设计模式,以简洁直接的代码为主,有利于整个团队后期维护,甚至加快人员变更后新成员对项目的适应度,因为工作说白了还是以绩效为主,怎么简单高效怎么来就行,你自己的个人项目你想怎么玩随便你。
本人原创文章纯手打,觉得有一滴滴帮助的话就请点个赞和收藏吧~
本人长期分享工作中的感悟、经验及实用案例,喜欢的话也可以进入个人主页关注一下哦~