作者:孤独烟,来自:http://rjzheng.cnblogs.com/

引言

其实写这篇文章之前,我犹豫了一下,毕竟单例大家都知道,写这么一篇文章会不会让人觉得老掉牙。后来想想,就当一种记录吧。先来一副漫画吧,如下图所示

ok,我们回顾下小灰的遭遇,上述漫画所提出的那些问题主要有以下三点:

  1. 为什么静态内部类的单例模式是最推荐的?

  2. 如何在反射的情况下保证单例?

  3. 如何在反序列化中保证单例?

针对上述三个问题有了这篇文章,以一种循序渐进的方式,引出最后一种单例设计模式,希望对大家能够有所帮助。

单例设计模式

1、饿汉式

这种其实大家都懂,不多说,上代码。

package singleton;public class Singleton1 {    private static Singleton1 instance = new Singleton1();    private Singleton1 (){}    public static Singleton1 getInstance() {        return instance;    }}
优点就是线程安全啦,缺点很明显,类加载的时候就实例化对象了,浪费空间。于是乎,就提出了懒汉式的单例模式

2、懒汉式

(1)懒汉式v1

package singleton;public class LazySingleton1 {    private static LazySingleton1 instance;    private LazySingleton1 (){}    public static LazySingleton1 getInstance() {        if (instance == null) {            instance = new LazySingleton1();        }        return instance;    }

然而这一版线程是不安全的,于是乎为了线程安全,就在getInstance()方法上加synchronized修饰符,于是getInstance()方法如下所示

public static synchronized LazySingleton1 getInstance() {        if (instance == null) {            instance = new LazySingleton1();        }        return instance;    }

然而,将synchronized加在方法上性能大打折扣(syncrhonized会造成线程阻塞),于是乎又提出一种双重校验锁的单例设计模式,既保证了线程安全,又提高了性能。双重校验锁的getInstance()方法如下所示

public static LazySingleton1 getInstance() {        if (instance == null) {              synchronized (LazySingleton1.class) {              if (instance == null) {                  instance = new LazySingleton1();                  }              }          }         return instance;    }

(2)懒汉式v2

懒汉式v1的最后一个双重校验锁版,不管性能再如何优越,还是使用了synchronized修饰符,既然使用了该修饰符,那么对性能多多少少都会造成一些影响,于是乎懒汉式v2版诞生。不过在讲该版之前,我们先来复习一下内部类的加载机制,代码如下

package test;public class OuterTest {    static {        System.out.println("load outer class...");    }    // 静态内部类    static class StaticInnerTest {        static {            System.out.println("load static inner class...");        }        static void staticInnerMethod() {            System.out.println("static inner method...");        }    }    public static void main(String[] args) {        OuterTest outerTest = new OuterTest(); // 此刻其内部类是否也会被加载?        System.out.println("===========分割线===========");        OuterTest.StaticInnerTest.staticInnerMethod(); // 调用内部类的静态方法    }}

输出如下

load outer class...===========分割线===========load static inner class...static inner method

因此,我们有如下结论

  1. 加载一个类时,其内部类不会同时被加载。

  2. 一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。。

基于上述结论,我们有了懒汉式V2版,代码如下所示

package singleton;public class LazySingleton2 {    private LazySingleton2() {    }    static class SingletonHolder {        private static final LazySingleton2 instance = new LazySingleton2();    }    public static LazySingleton2 getInstance() {        return SingletonHolder.instance;    }}

由于对象实例化是在内部类加载的时候构建的,因此该版是线程安全的(因为在方法中创建对象,才存在并发问题,静态内部类随着方法调用而被加载,只加载一次,不存在并发问题,所以是线程安全的)。

另外,在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。当LazySingleton2类加载的时候,其静态内部类SingletonHolder并没有被加载,因此instance对象并没有构建。

而我们在调用LazySingleton2.getInstance()方法时,内部类SingletonHolder被加载,此时单例对象才被构建。因此,这种写法节约空间,达到懒加载的目的,该版也是众多博客中的推荐版本。

ps:其实枚举单例模式也有类似的性能,但是因为可读性的原因,并不是最推荐的版本。

(3)懒汉式v3

然而,懒汉式v2版在反射的作用下,单例结构是会被破坏的,测试代码如下所示

package test;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import singleton.LazySingleton2;/** * @author zhengrongjun */public class LazySingleton2Test {    public static void main(String[] args) {        //创建第一个实例        LazySingleton2 instance1 = LazySingleton2.getInstance();        //通过反射创建第二个实例        LazySingleton2 instance2 = null;        try {            Class<LazySingleton2> clazz = LazySingleton2.class;            Constructor<LazySingleton2> cons = clazz.getDeclaredConstructor();            cons.setAccessible(true);            instance2 = cons.newInstance();        } catch (Exception e) {            e.printStackTrace();        }        //检查两个实例的hash值        System.out.println("Instance 1 hash:" + instance1.hashCode());        System.out.println("Instance 2 hash:" + instance2.hashCode());    }}

输出如下

Instance 1 hash:1694819250Instance 2 hash:1365202186

根据哈希值可以看出,反射破坏了单例的特性,因此懒汉式V3版诞生了

package singleton;public class LazySingleton3 {    private static boolean initialized = false;    private LazySingleton3() {        synchronized (LazySingleton3.class) {            if (initialized == false) {                initialized = !initialized;            } else {                throw new RuntimeException("单例已被破坏");            }        }    }    static class SingletonHolder {        private static final LazySingleton3 instance = new LazySingleton3();    }    public static LazySingleton3 getInstance() {        return SingletonHolder.instance;    }}

此时再运行一次测试类,出现如下提示

java.lang.reflect.InvocationTargetException    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)    at test.LazySingleton3Test.main(LazySingleton3Test.java:21)Caused by: java.lang.RuntimeException: 单例已被破坏    at singleton.LazySingleton3.<init>(LazySingleton3.java:12)    ... 5 moreInstance 1 hash:359023572

这里就保证了,反射无法破坏其单例特性

(3)懒汉式v4

在分布式系统中,有些情况下你需要在单例类中实现 Serializable 接口。这样你可以在文件系统中存储它的状态并且在稍后的某一时间点取出。

让我们测试这个懒汉式V3版在序列化和反序列化之后是否仍然保持单例。

先将

public class LazySingleton3

修改为

public class LazySingleton3 implements Serializable 

上测试类如下

package test;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInput;import java.io.ObjectInputStream;import java.io.ObjectOutput;import java.io.ObjectOutputStream;import singleton.LazySingleton3;public class LazySingleton3Test {    public static void main(String[] args) {        try {            LazySingleton3 instance1 = LazySingleton3.getInstance();            ObjectOutput out = null;            out = new ObjectOutputStream(new FileOutputStream("filename.ser"));            out.writeObject(instance1);            out.close();            //deserialize from file to object            ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));            LazySingleton3 instance2 = (LazySingleton3) in.readObject();            in.close();            System.out.println("instance1 hashCode=" + instance1.hashCode());            System.out.println("instance2 hashCode=" + instance2.hashCode());        } catch (Exception e) {            e.printStackTrace();        }    }}

输出如下

instance1 hashCode=2051450519instance2 hashCode=1510067370

显然,我们又看到了两个实例类。为了避免此问题,我们需要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。

因此,我们提供懒汉式V4版代码如下

package singleton;import java.io.Serializable;public class LazySingleton4 implements Serializable {    private static boolean initialized = false;    private LazySingleton4() {        synchronized (LazySingleton4.class) {            if (initialized == false) {                initialized = !initialized;            } else {                throw new RuntimeException("单例已被破坏");            }        }    }    static class SingletonHolder {        private static final LazySingleton4 instance = new LazySingleton4();    }    public static LazySingleton4 getInstance() {        return SingletonHolder.instance;    }    private Object readResolve() {        return getInstance();    }}

此时,在运行测试类,输出如下

instance1 hashCode=2051450519instance2 hashCode=2051450519

这表示此时已能保证序列化和反序列化的对象是一致的

总结

本文给出了多个版本的单例模式,供我们在项目中使用。实际上,我们在实际项目中一般从懒汉式v2、懒汉式v3、懒汉式v4中,根据实际情况三选一即可,并不是非要选择懒汉式v4作为单例来实现。最后,希望大家有所收获。

推荐阅读:

技术:分布式唯一ID极简教程

职场:程序员职业规划

分享:2T架构师学习资料干货分享

觉得有帮助?请转发给更多人!

架构师小秘圈,聚集10万架构师的小圈子!不定期分享技术干货,行业秘闻!汇集各类奇妙好玩的话题和流行动向!长按左侧图片,扫码加入架构师微信群!

月薪5万程序员眼中的单例模式相关推荐

  1. #月薪1万程序员吐槽:领导从大厂挖来的前同事,月薪4万还不如我

    在很多人的眼中,程序员是一个非常不错的职业,不仅自身有着不可取代的高技术,还能拿到很多人都羡慕的高薪.其实他们的薪资和技术是相对应的,技术越好薪资越高.但是近日却有网友在某互联网论坛上发帖称: 对此, ...

  2. 月薪2万程序员面试,被HR直面吐槽:毕业生能值这个数?

    跳槽是员工涨薪的一条捷径,但却被老板视为对企业的不忠诚. 表面上看起来员工理亏,但实际上与忠诚没半点关系,试想一下,如果公司能给足够的发展平台以及薪水,谁愿意换工作呢?说到底,还是因为平台与员工自身的 ...

  3. 上海00后985毕业女生月薪1.2w,想找年薪40万程序员,网友表示很不理解

    各位学生变成大学生的身份,进入大学阶段开始学习之后,大学生的学习节奏就不用像高中阶段那么紧绷了,大学生在大学里的忙碌程度如何? 和大学生所学习的是什么专业有关,同时还和大学生自身的自律意识怎么样,也是 ...

  4. iOS程序员眼中的首次使用产品体验

    2017.11.23 一. 前言 首先想说一下为什么写这篇文章: <启示录>这本书曾提到:如果开发的产品没有市场价值,那么无论开发团队多么优秀也无济于事.那么同样的,在我们程序员费尽周折抓 ...

  5. iPhone开发入门(1)----程序员眼中的iPhone

    自去年 iPhone 面世以来,开创了移动设备内容服务的一种新的模式--程序商店(App Store).它极大地降低了移动设备应用程序开发的成本,即使普通人也能进入这个市场.就像在PC上开发应用程序一 ...

  6. iPhone开发入门(1)—-程序员眼中的iPhone

    http://blog.csdn.net/itudou_2010/article/details/5492272 iPhone开发入门(1)--程序员眼中的iPhone 博主:易飞扬 原文链接 : h ...

  7. 一年暴增1600万程序员!GitHub 2021年度报告发布:中国755万开发者排全球第二

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 来源丨新智元 编辑丨极市平台 导读 GitHub年度报告显示,去年全 ...

  8. 中国700万程序员不够用怎么办?我们去问了北大谢涛,顶会ASE最有影响力论文奖首批华人得主...

    金磊 梦晨 发自 凹非寺 量子位 报道 | 公众号 QbitAI 在自动驾驶领域,有着L1-L5的等级划分标准. 随着近年来自动驾驶火热,这一标准不断得到验证,已成为业界共识,也指导着行业发展. 人们 ...

  9. 【程序员眼中的统计学(12)】相关与回归:我的线条如何? (转)

    阅读目录 目录 1 算法的基本描述 2 算法的应用场景. 3算法的优点和缺点 4 算法的输入数据.中间结果以及输出结果 5 算法的代码参考 6 共享 相关与回归:我的线条如何? 作者 白宁超 2015 ...

最新文章

  1. vue避免重新渲染_小白也能懂的VUE的生命周期探寻
  2. LeetCode Linked List Random Node(蓄水池采样算法)
  3. C/C++ 语言中表达式的求值
  4. R - 一只小蜜蜂...(第二季水)
  5. redis延迟队列 如何确保成功消费_千万级延时任务队列如何实现,看美图开源的-LMSTFY...
  6. AV1挑起的Codec之战
  7. unc 隐藏共享文件夹_你真的了解任务栏吗?win10任务栏居然隐藏了这么多小窍门...
  8. [斯坦福]距离编码-更为强大的GNN
  9. java 定义动态接口_使用自定义annotation接口进行aspectj动态缓存
  10. css不继承上级样式_这个笔记《CSS基本概念》,让菜鸟轻松学会给网页穿外衣
  11. 堆排序(python实现)
  12. 关于苹果与摄影的事。
  13. 传统反病毒产品丧钟响起
  14. QT 以资源管理器打开文件夹
  15. C# WPF MVVM 实战 - 1
  16. Elastic Searchable snapshot功能初探 三 (frozen tier)
  17. 个人电脑网站的创建与发布
  18. 在虚拟机上调试网络时要注意的内容
  19. 磁盘存储链式的B树与B+树
  20. 支付宝当面付打shang系统源码分享

热门文章

  1. 【数论基础】模运算详解及其应用
  2. ssh远程工具_Rsync如何利用SSH加密隧道同步文件
  3. html下拉框由后端,select下拉框通过ajax获取后台的值
  4. 一种电子病历系统软件框架思想
  5. ES6精华:字符串扩展
  6. 在.net中Regex(正则)的应用
  7. 智能家居数据获得美好生活的6种方法
  8. 提高PHP编码的一些技巧
  9. 转载非原创:修改BB 的内容,回车后修改CC 的值
  10. 打造开发者的win7