Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

再议单例模式和静态类

Posted on 03/31/201210/08/2024 by 四火

tool 单例模式还是静态类,这是一个老话题了,从我刚开始接触 Java 的时候就看到这样的讨论。在这里我总结一下,也添加一点点新东西。

首先要澄清和区别一些概念,“静态类” 和 “所有方法皆为静态方法的类”。

严格说来,Java 中的静态类,指的是 “static class” 这样修饰的类定义,语法上的要求,使得这样的类一定是内部类,换言之,“静态内部类” 是对它的完整定义。静态内部类最大的好处在于可以隐藏自己(只让自己被所在外层的类用到),同时又可以访问到所在外层类的属性。和 “非静态” 的内部类相比,它可以放置一些静态的成员变量和方法定义,而非静态类不可以;而且,静态内部类不能访问外层类非静态的属性。

但是,通常我们所说的 “静态类”,也是下文所述的 “静态类”,是指所有的方法、属性都是静态的类,同时,我们在使用它们的时候,直接调用它们的静态方法、访问其中的静态属性,而不需要对其实例化。这类所谓的 “静态类” 往往具备这样两个特点,一个是使用 final 修饰,它们往往没有子类;其二是构造器都被私有化了,不允许被构造实例。

1、单例模式便于 mock,可测性好。虽说静态方法也可以 mock(比如需要使用一些特殊的注解),但是毕竟相对还是麻烦一些,也没有那么灵活。

2、有人说单例模式可以做到 lazy load,但是静态类不行。这肯定是扯淡,静态类也完全可以做到第一次使用的时候再加载。不过,其中值得一提的是单例中的 Double Check Lock,这里做一个简单介绍。看下面的代码:

public Singleton {  
    private static Singleton instance;  
    private Number n = new Number();  
    public Number get() {  
        return this.n; //(1)  
    }  
    public static Singleton getInstance() {  
        if(instance == null) {  
            synchronized(Singleton.class) {  
                if(instance == null)  
                    instance = new Singleton();  
            } //(2)  
        }  
        return instance;  
    }  
} 

这是很常见的一种写法,不过,由于编译器的优化,允许出现主存和线程工作内存数据不一致问题,这就是 “DCL 失效” 的问题。上面的代码是其典型表现:

根据 JMM 规范,主存数据和工作内存的数据是允许存在不一致的。JDK1.2 之后,分配空间、对象初始化等等操作才都放到工作内存中进行了。由于 synchronized 关键字的关系,执行到语句 (2) 的时候,走出同步块时,JVM 会将主存和工作内存的 instance 引用的对象刷新到一致,即 instance 是 “可见” 的。但问题出在上面的 (1),没有 synchronized,也没有 volatile、final,没有人来保证调用 get 方法时获得的 n 是正确的值,即这个 n 未必是 “可见” 的。如果 n 比 instance 晚同步到主存,就存在一个时间间隙,这个间隙内获取到的 instance 是一个不健康的 instance,其中的 this.n 是取不到正确的 Number 对象的。

在 JSR133 中,对 JMM 做了一个修正,后引入或增强了 synchronized、volatile 和 final 关键字,通过它们的运用,上述问题能够得到解决。另外,还有一种解决的方法可以是使用静态的内部类:

private static class InnerInstance {  
   public static Instance instance = new Instance();  
}  
public static Instance getInstance() {  
   return InnerInstance.instance;  
}  

3、单例可以被继承,这是一个很大的好处,这便于用户 overwrite 其中的某方法,当然,继承单例的场景较少见;而静态类一般不被继承。关于单例的继承细节,这里暂不讨论,有几种办法,有兴趣的同学可以自行阅读 JDK 的 Calendar 类。

4、单例可以实现自某接口,可以继承自某类。静态类也可以继承自某类,但是就没法使用父类里面的 protect 成员了。推广来说,这一点和上一点都可以看做是面向对象带来的好处:封装、继承和多态,静态类不能很好地具备其中的后两点。

5、单例可以比较方便地扩展为有限实例。根据需要,我可以通过工厂,生产出两个内部状态不同的单例对象——这在静态类中是难以做到的。Spring 可以看做一系列大工厂,但其中的 bean 也只有 singleton 和 prototype 两种,生产不出 static 的新类型;当你的工具成为了对象,就能够保持良好的扩展性。

还有一个有趣的例子是 JDK 的 Calendar.getInstance() 方法,从方法看很像是获得一个单例,其实不是,每次都去创建了新的 Calendar 对象;同时,使用 abstract 修饰它自己,保证了无法使用 new 实例化,又开放了 getInstance 这样一个接口来获取默认实现,而获取的默认实现,又恰恰是 Calendar 的子类。这种形式可以看做是单例的一个变体。

6、有人说,单例在使用过程中申请的资源可以被及时释放并回收内存,但是静态类不行。这也是没有道理的,别忘了静态类也是可以存放状态的,在确定不再使用资源后,及时将资源的引用置为 null 就可以了。

7、如果希望在类加载的时候做复杂的操作,那么在静态类中,需要引入 static 块来初始化数据,如果期间抛出了异常,就可能发生一个 “ClassDefNotFoundError” 的诡异错误,这对问题定位是不利的。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

×Scan to share with WeChat

你可能也喜欢看:

  1. 从 DCL 的对象安全发布谈起
  2. 泛型传递
  3. 运行时动态增加枚举类型
  4. 看 JDK 源码,解几个疑问
  5. 一道随机数题目的求解

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Spark 互联网 亚马逊 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 眼界 程序员 管理 系统设计 缓存 编程范型 美股 英语 西雅图 设计 问题 面向对象 面试

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Machine Learning and Artificial Intelligence (6)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 聊一聊分布式系统中的时间
  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • Ticket: TRANSACTION 1.922915 BTC. Go to withdrawal >> https://yandex.com/poll/enter/BXidu5Ewa8hnAFoFznqSi9?hs=20bd550f65c6e03103876b28cabc4da6& on 倔强的程序员
  • panshenlian.com on 初涉 ML Workflow 系统:Kubeflow Pipelines、Flyte 和 Metaflow
  • panzhixiang on 关于近期求职的近况和思考
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme