查看原文
其他

回字有四种写法,那你知道单例有五种写法吗

作者 | RudeCrab

来源 | jianshu.com/p/540c1085c5de


上一篇:说实话,Hibernate 和 MyBatis 哪个更好用?


基本介绍


单例模式(Singleton)应该是大家接触的第一个设计模式,其写法相较于其他的设计模式来说并不复杂,核心理念也非常简单:程序从始至终只有同一个该类的实例对象。


举一个耳熟能详的例子,比如 LOL 中的大龙,一场游戏下来无论如何只有一只,所以该类只能被实例化一次。再举一个我们应用程序开发中常见的例子,Spring 框架中的 Bean 作用范围默认也是单例的。


我相信大家都知道单例的两种最基本的写法:饿汉式和懒汉式。但是这两种写法都有其弊端所在,除了这两种写法外其实还有几种写法。此时耳边仿佛听到孔乙己的声音:


“对呀对呀!......回字有四样写法,你知道么?”。 


我愈不耐烦了,努着嘴走远。孔乙己刚用指甲蘸了酒,想在柜上写字,见我毫不热心,便又叹一口气,显出极惋惜的样子........


大家先别着急走,回字的四样写法没必要知道,单例的五种写法还是有必要晓得滴,其他的不说,至少面试的时候还能和面试官吹下是不,况且这几种写法也不是纯吊书袋,了解过后还是能帮助我们理解其设计思想滴。所以接下来咱们由浅入深,从最容易的写法开始,一步一步的带大家掌握单例模式!


写法介绍


1. 饿汉式


话不多说,先直接上最简单的写法,然后咱再慢慢剖析:


public class Signleton01 { // 私有构造函数,防止别人实例化 private Signleton01(){} // 静态属性,指向一个实例化对象 private static final Signleton01 INSTANCE = new Signleton01(); // 公共方法,以便别人获取到实例化对象属性 public static Signleton01 getINSTANCE() { return INSTANCE; }}


单例模式三元素


一个单例模式就这样写完了,简直不要太简单。类里面一共就三个元素:


  1. 私有构造函数,防止别人实例化;

  2. 静态属性,指向一个实例化对象;

  3. 公共方法,以便别人获取到实例化对象属性。


这三个元素就是单例模式的核心,单例无论哪种写法,都离不开这三个元素


这三个元素也很好理解,别人想要用我这个类的实例对象就只能通过我提供的 getINSTANCE(),他想 new 也 new 不了第二个对象,自然而然就保证了该类只有唯一对象。我们可以做个试验,跑100个线程同时获取该类的实例对象,然后打印出对象的 hashCode,看看到底是不是获取的同一个对象:


public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Signleton01.getINSTANCE().hashCode()); }).start(); }}


结果如下:


...834649078834649078834649078834649078834649078...


嗯,全部都是同一个对象。


优缺点


  • 优点:写法简单,线程安全;

  • 缺点:消耗资源,即使程序从没有用到过该类对象,该类也会初始化一个对象出来。


所以为了解决饿汉式的这个缺点, 我们就引出了第二种写法,懒汉式!


关注顶级架构师公众号回复“架构整洁”,送你一份惊喜礼包。


2. 懒汉式


基本写法


public class Singleton02 { // 私有构造函数,防止别人实例化 private Singleton02() {} // 静态属性,指向一个实例化对象(注意,这里没有实例化对象哦) private static Singleton02 INSTANCE; // 公共方法,以便别人获取到实例化对象属性 public static Singleton02 getINSTANCE() { if (INSTANCE == null) { INSTANCE = new Singleton02(); } return INSTANCE;    }}


懒汉式的和饿汉式最大的区别是什么呢,就是只有在调用 getINSTANCE 的时候,才会创建实例。如果你从来没调用过,那么就不实例化对象。这个就比饿汉式更加节约资源,不过这种写法并不是懒汉式的完善写法,它有一个非常大的问题,就是线程不同步!我们可以按照之前那种方式创建100个线程测试一下结果:


...18512616568689075009887624761031371881593800070...


可以看到这线程一同时拿,拿的都不是同一个对象,这完全就破坏了单例模式。因为很多线程在对象没有初始化前就进入到了 if (INSTANCE == null) 判断语句块里,自然而然就会 new 出不同的对象了。要解决这个线程不安全问题,就得上线程锁!


3. synchronized 写法


public class Singleton02 {    private Singleton02() {} private static Singleton02 INSTANCE; // 注意,这里静态方法加了synchronized关键字 public synchronized static Singleton02 getINSTANCE() { if (INSTANCE == null) { INSTANCE = new Singleton02(); } return INSTANCE; }}


当我们在静态方法加上 synchronized 关键字后,就可以保证这个方法在同一时间只会有一个线程能成功调用,也就顺理成章的解决了线程不安全问题。我们还是测试一下:


...12268803561226880356122688035612268803561226880356...


不管多少个线程,拿到的都是同一个对象,达到了单例的要求!


优缺点


懒汉式连基本的线程安全都不能保证,就不做讨论了,我们这里主要说的是 synchronized 写法:


  • 优点:写法简单,节约资源(只有需要该对象的时候才会实例化);

  • 缺点:耗性能。


要知道每一次调用 getINSTANCE() 方法时都会上锁,这是非常耗性能的。那么为了解决这个好性能的问题,我们又引申出接下来的一种写法。


4. 双重检测


每一次调用 getINSTANCE() 方法都会上锁,这是完全没有必要的嘛,因为只有对象还没有实例化的时候我才需要上锁以保证线程安全。对象都实例化了,自然也不用担心后续的调用会 new 出新的对象。所以我们这个锁,可以加在 if (INSTANCE == null) 判断语句块里面:


public class Singleton03 {    private Singleton03() {} private static Singleton03 INSTANCE;
public static Singleton03 getINSTANCE() { if (INSTANCE == null) { // 只有在对象还没有实例化的时候才上锁 synchronized (Singleton03.class) { INSTANCE = new Singleton03(); } } return INSTANCE; }}


这样就能节约一些性能,但是这样并没有做到线程安全哦!因为很多线程进入到if  (INSTANCE == null) 判断语句后,虽说是因为锁不能同时 new 对象了,但是如果锁一旦释放,那么其他线程依然会执行到 INSTANCE = new Singleton03() 语句,从而破坏了单例。所以在 synchronized 代码块内还要加一层判断:


public class Singleton03 {    private Singleton03() {} // 注意,使用双重检验写法要加上volatile关键字,避免指令重排(有个印象就行,这不是本文的重点) private static volatile Singleton03 INSTANCE;
public static Singleton03 getINSTANCE() { if (INSTANCE == null) { // 只有在对象还没有实例化的时候才上锁 synchronized (Singleton03.class) { // 额外加一层判断 if (INSTANCE == null) { INSTANCE = new Singleton03(); } } } return INSTANCE; }}


synchronized 代码块外面一层判断,里面一层判断,就是有名的双重检测(DCL)了!里面的这一层判断加了之后呢,第一个线程的锁一旦释放也不用担心了,因为此时对象已经实例化,后续的线程也执行不了 new 语句,从而保证了线程安全!


优缺点


  • 优点:节约资源(只有需要该对象的时候才会实例化);

  • 缺点:写法复杂,耗性能(还是上了锁,还是耗性能)。


虽然双重校验比 synchronized 懒汉式写法减少了很多锁性能消耗,但毕竟还是上了锁,所以为了解决这个锁性能消耗问题了,又引申出下一种写法。


5. 内部类


话不多说,直接上代码:


public class Singleton04 { // 老套路,将构造函数私有化 private Singleton04() {} // 声明一个内部类,内部类里持有实例的引用 private static class Inner { public static final Singleton04 INSTANCE = new Singleton04(); } // 公共方法 public static Singleton04 getINSTANCE() { return Inner.INSTANCE; }}


这个写法非常像饿汉式写法,单例三元素还是那三元素,只不过多加了一个内部类,将实例引用放到内部类里而已。为啥要这样写呢?因为 JVM 保证了内部类的线程安全,即一个内部类在整个程序中不会被重复加载,并且如果你没有使用到内部类的话,是不会加载这个内部类的。这就非常巧妙的实现了线程安全以及节约资源的好处!


优缺点


  • 优点:写法简单、节约资源(只有调用了 getINSTANCE() 方法才会加载内部类,才会实例化对象)、线程安全(JVM 保证了内部类的线程安全);

  • 缺点:会被序列化或者反射破坏单例。


关注顶级架构师公众号回复“Java”,送你一份Java面试题和答案惊喜礼包。


这个缺点可以说是吹毛求疵,因为之前所有写法都会被序列化、反射破坏单例。虽然说是吹毛求疵,但咱们搞技术的还是得做到了解全部细节,我来演示一下怎样破坏这个单例。


通过反射破坏单例


public static void main(String[] args) throws Exception { // 创建100个线程同时访问实例 for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton04.getINSTANCE().hashCode()); }).start(); }
// 反射破坏单例 Class<Singleton04> clazz = Singleton04.class; // 拿到无参构造函数并将其设置为可访问,无视private Constructor<Singleton04> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); // 创建对象 Singleton04 singleton04 = constructor.newInstance(); System.out.println("反射:" + singleton04.hashCode());}


运行结果如下:


...21151472682115147268反射:107869478921151472682115147268...


如果是通过正常的访问实例方法,是完全可以做到单例的要求。但是如果用反射的形式来创建一个对象,则就破坏了单例,一个程序中就出现了多个不同的实例对象。那么为了解决这个吹毛求疵的问题,聪明的前辈们想到了一个完美的写法!


枚举


// 注意,这里是枚举public enum Singleton05 { // 实例 INSTANCE; // 公共方法 public static Singleton05 getINSTANCE() { return INSTANCE; }}


哎嘿,不是说所有单例都是那三元素吗,这里怎么只有两个元素呀!这是因为枚举就没有构造方法,自然而然就做到了私有化构造函数的效果,而且比私有化构造函数效果更好!因为都没有构造函数了,连序列化和反射都破坏不了这种写法的单例!


眼见为实,我们做个试验:


public static void main(String[] args) throws Exception { // 创建100个线程同时访问实例 for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton05.getINSTANCE().hashCode()); }).start(); }
// 反射破坏单例 Class<Singleton05> clazz = Singleton05.class; // 拿到无参构造函数并将其设置为可访问,无视private Constructor<Singleton05> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); // 创建对象 Singleton05 singleton05 = constructor.newInstance(); System.out.println("反射:" + singleton05.hashCode());}


运行结果如下:


...422057313422057313422057313422057313
Exception in thread "main" java.lang.NoSuchMethodException: Singleton05.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178)


当运行到反射那一块代码的时候,程序直接报错,原因就是我之前所说的一样,枚举没有构造方法,你自然就无法通过反射来创建对象了!


优缺点


此方法乃是最完美的方法,真是佩服想出这种写法的前辈!


总结


五个写法全部介绍完毕,每个写法都有其特点,根据自己的需求来写就好了!每种写法理解其特点后,写出来也就非常轻松。就像我一开始说的一样,理解这五种写法也不是吊书袋,每一种写法都有其背后的思考,有些写法思路真的让人叹服,至少我了解到内部类和枚举写法的时候我心里就是:我靠!这都能想出来,太牛逼了吧......


好的代码就是艺术作品,希望我们都能码出好的艺术出来!


公众号后台回复【架构】或者【架构整洁】有惊喜礼包!------END------

架构师交流群

 「顶级架构师」建立了读者架构师交流群,大家可以添加小编微信进行加群

扫描添加好友邀你进架构师群,加我时注明姓名+公司+职位】


版权申明:内容来源网络,版权归原作者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

猜你还想看

Spring Validation最佳实践及其实现原理,参数校验没那么简单!
详解SpringCloud中RabbitMQ消息队列原理及配置,一篇就够!
Docker 入门终极指南:边学边用
30 个高可用 Prometheus 架构实践中的踩坑集锦

长按识别图片二维码关注,订阅更多精彩

顶级架构师,企业架构、系统架构、网站架构、大规模分布式架构、高可用架构等架构讨论,以及结合互联网技术的架构调整。欢迎有想法、乐于分享的架构师交流学习

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存