设计模式系列之(1):单例模式
1 介绍:
单例模式是非常简单、非常基础的一种创建型设计模式。单例模式是在Java开发(不论是android开发,还是Java服务器端开发)中经常使用的一个设计模式。由于在Java服务器端开发中经常使用,所以单例模式的实现还涉及到了Java并发编程。在java面试中,常常被要求使用java实现单例模式,因此有必要熟练掌握。本文是自己学习java单例模式的一个总结,参考了网上的很多资料,大部分内容不是原创。在这里要向参考文献的作者表示感谢。
本文组织结构如下: 2 介绍单例模式的java实现到底有多少种;3-9分别介绍这些实现方式; 10 提出一些进一步的问题; 11 总结本文。
2 到底有多少种实现?
"【深入】java 单例模式"一文列出了5种实现方式,一种为GOF的、线程不安全的实现方式,剩下4种是线程安全(thread-safe)的实现方式,分别为:同步方法(synchronized function)方式、双重检查加锁(DCL,double-checked locking)方式、急切加载方式 和内部类方式。
"Java:单例模式的七种写法"一文列出了7种实现方式:线程不安全方式、同步方法方式、急切加载方式(及其变种)、静态内部类方式、枚举方式和双重校验锁方式。 但急切加载方式及其变种应该统一看做是一种实现。
所以本文的结论就是: java单例模式一共有下面将要介绍的5种实现方式。
4. 第一种: 线程不安全方式(GOF方式\经典方式) ( 懒加载)
这是GOF和HeadFirstDesignPattern中介绍的最经典的一种方式。这种方式在单线程情况下可以实现懒加载,但在多线程情况下会出问题。
1 public class Creation_singleton { 2 // private static instance, which is default null 3 private static Creation_singleton instance; 4 // private constructor 5 private Creation_singleton () { 6 } 7 // public get instance function 8 public static Creation_singleton getInstance ( ) { 9 if ( instance == null ) { 10 instance = new Creation_singleton(); 11 } 12 return instance; 13 } 14 }
5. 第二种:简单加锁方式 ( synchronized )
由第一种而来,很自然会想到,保证线程安全的方式可以是使用synchronized关键字。这种方式是线程安全,又是懒加载。但使用synchronized 会降低性能。
1 public class Creation_singleton { 2 private static Creation_singleton instance; 3 private Creation_singleton () { } 4 public static Synchronized Creation_singleton getInstance ( ) { 5 if ( instance == null ) { 6 instance = new Creation_singleton(); 7 } 8 return instance; 9 } 10 }
我们必须避免使用synchronization,因为它很慢。----上述论断在一定程度上已经不是事实了,因为在现代JVM上,如果是无竞争的同步的话,synchronized并不会很慢了。但是当在多线程环境中,多个线程同时竞争getInstance方法时,还是可能会导致性能下降。
之所以说上述论断不是事实,是与显式锁进行比较得出的结论。内置锁一度被认为比Lock显式锁的性能低很多,但随着JVM的优化,synchronized关键字的性能已经不比Lock低很多了。
6. 急切加载方式 ( 变种 )
1 class Singleton {
2 //私有,静态的类自身实例
3 private static Singleton instance = new Singleton();
4 //私有的构造子(构造器,构造函数,构造方法)
5 private Singleton(){}
6 //公开,静态的工厂方法
7 public static Singleton getInstance() {
8 return instance;
9 }
10 }
这种方式有一种变种,就是使用静态初始化块来初始化单例。
有人说这种方式有个缺点:其实跟全局变量是一样的: 不管该实例是不是到了实际被使用的时刻,也不管应用程序最终是不是使用该单例,在类加载 时都会创建该单例,所以叫“急切创建”, 但是这样一来,单例模式的延迟创建的优点就没有了。 所以该版本与全局变量的方式并没有区别(?yes) static 成员变量只有在类加载的时候初始化一次。类加载是线程安全的。 所以该方法实现的单例是线程安全的。 (static的全局变量也是线程安全的)
但是jvm的类加载也是懒加载的,并不是一开始启动jvm的时候就全部加载所有类。加载这个类,跟创建这个类的实例,二者的时机,在大部分时候,应该是同时的,也就是说,应该不存在很多跟创建这个类的实例无关的、需要加载这个类的情况。正是由于这个原因,这种方式,在NTS代码中使用的很多。
7. 双重检查加锁(dcl)方式
在生产环境中可以看到如下代码,是双重检查加锁的实现。双重检查加锁是简单加锁方式的一种“自作聪明”的改进,其实这种方式在多线程环境下会失败。
1 private static SocketFactory instance = null; 2 3 private static SocketFactory getInstance() 4 { 5 if (instance == null) 6 { 7 synchronized (SocketFactory.class) 8 { 9 if (instance == null) 10 { 11 instance = new SocketFactory(); 12 } 13 } 14 } 15 return instance; 16 }
双重检查加锁方式也可以正确地被实现,前提是对上述双重检查加锁方式做出一定的改进。这也让双重检查加锁方式成为了最复杂的实现方式。因此这种方式是不被推荐的。 双重检查加锁方式的正确实现由三种:volatile方式、 final方式、 temp instance方式。因为temp instance 方式非常的繁琐,在本文中就不再做介绍了,有兴趣的读者可以去参考文献中找答案。
7.1. 使用DCL+volatile
在Java5中,DCL实际上是可行的,如果你给instance域加上volatile修饰符。例如,如果我们需要给getInstance()方法传递一个database connection的话,那么以下代码是可行的:
1 public class MyFactory { 2 private static volatile MyFactory instance; 3 4 public static MyFactory getInstance(Connection conn) 5 throws IOException { 6 if (instance == null) { 7 synchronized (MyFactory.class) { 8 if (instance == null) 9 instance = new MyFactory(conn); 10 } 11 } 12 return instance; 13 } 14 15 private MyFactory(Connection conn) throws IOException { 16 // init factory using the database connection passed in 17 } 18 }
但是需要注意的是,上述代码仅仅在Java5及其以上版本中才可以,因为Java5对volatiel关键字的语义进行了修正。(Java4及其以前版本中Volatile关键字的语义都不是完全正确的)。当使用Java5获取一个volatile变量时,获取行为具有synchronization同步语义。换句话说,Java5保证了如下的Happen-before规则:对volatile变量的未同步的读操作必须在写操作之后才能发生,从而读线程将会看到MyFactory对象的所有域的正确值。
7.2 把instance域声明为final
从java 5 之后才有的新特性之一是,final 域的语义有了新的变化。这些域的值是在构造函数中被赋值的,JVM会确保这些值在这个对象引用自己之前被提交到主内存。
换句话说,如果其他线程可以看到这个对象,那么它们永远不可能看到这个对象的final域的未经初始化的值。所以在这种情况下,我们将不需要把这个instance的引用声明为volatile。
(问题:是否需要把所有域都声明为final? )
8. 静态嵌套类方式
1 public class Creation_singleton_innerClass { 2 private static class SingletonHolder{ 3 private static Creation_singleton_innerClass instance = new Creation_singleton_innerClass(); 4 } 5 private Creation_singleton_innerClass() { } 6 public static Creation_singleton_innerClass getInstance() { 7 return SingletonHolder.instance; 8 } 9 }
该方式与急切创建方式有些类似。 java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy), 说明内部类的加载时机跟外部类的加载时机不同。 而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance,以后不会再初始化。
该实现方式可以有一些小变化:instance可以加一个final修饰;静态嵌套类可以改为内部类。
9. 枚举方式
1 enum Singleton_enum { 2 INSTANCE; 3 void method () { 4 } 5 }
该方式是《effective java》中推荐的方法。 枚举类型是在java1.5中引入的,enum可以看做是一种特殊的class,除了不能继承。INSTANCE是Singleton_enum类的实例。
上述代码会被JVM编译为:
1 final class MySingleton { 2 final static MySingleton INSTANCE = new MySingleton(); 3 void method () { } 4 }
虽然enum的域是编译时常量,但他们是对应enum类型的实例,他们仅在对应enum类型首次被引用的时候被生成。当代码首次运行到访问INSTANCE处时,类MyStingleton才会被JVM装载和初始化。该装载和初始化过程只初始化static域一次。
10 进一步问题:
我在一次面试中,被问到如下问题:单例模式与static方法的区别是什么? 当时没回答上来,现在把答案整理如下:
10.1 单例模式与 static方法的简单比较
static 方法无法实现单例;
如果类与其他类的交互比较复杂,容易造成一些跟初始化有关的、很难调试的bug;
static是急切初始化。
static方法最大的不同就是不能作为参数传递给别的方法。单例可以把这个单例object作为参数传递给其他方法,并当做普通对象来使用。
一个静态类只允许静态方法。
11 参考文献:
http://www.cnblogs.com/coffee/archive/2011/12/05/inside-java-singleton.html
http://www.blogjava.net/kenzhh/archive/2015/04/06/357824.html
http://www.raychase.net/257
https://en.wikipedia.org/wiki/Singleton_pattern
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?