单例模式
单例模式是一种常用的软件设计模式。
在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
简而言之,就是对象只创建一个实例,并且提供一个全局的访问点。
单例模式常见的有3种写法:
- 饿汉式
- 懒汉式
- 双重锁定式
饿汉式

package singleton; //饿汉式单例类 public class HungrySingleton { /** 通过静态变量初始化的类实例 */ private static final HungrySingleton hs = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return hs; } }
由Java语言类的初始化顺序可知,在这个类被加载时,静态变量会被初始化,此时类的私有构造子会被调用。这时候,单例类的唯一实例就被创建出来了。
也正因为如此造就饿汉式模式的软肋:如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用都一块儿创建。
懒汉式

package singleton; //懒汉式单例类 public class LazySingleton { /** * 此时静态变量不能声明为final,因为需要在getInstance()中对它进行实例化 */ private static LazySingleton instance; private LazySingleton() { } /** * synchronized关键字解决多个线程的同步问题 */ public static synchronized LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
静态getInstance()方法中synchronized关键字提供的同步是必须的,否则当多个线程同时访问该方法时,无法确保获得的总是同一个实例。
然而我们也看到,在所有的代码路径中,虽然只有第一次引用的时候需要对instance变量进行实例化,但是synchronized同步机制要求所有的代码执行路径都必须先获取类锁。在并发访问比较低时,效果并不显著,但是当并发访问量上升时,这里有可能会成为并发访问的瓶颈。
所以,懒汉式的优点是延时加载、缺点是需要同步
双重锁定式

package singleton; //双重锁定式单例类 public class DoubleLockSingleton { private static DoubleLockSingleton instance = null; private DoubleLockSingleton() { // do something } /**** * 这个模式将同步内容下方到if内部,提高了执行的效率, 不必每次获取对象时都进行同步, * 只有第一次才同步,创建了以后就没必要了。 * * @return */ public static DoubleLockSingleton getInstance() { // 第一次创建实例的时候进行同步 if (instance == null) { synchronized (DoubleLockSingleton.class) { if (null == instance) { instance = new DoubleLockSingleton(); } } } return instance; } }
双重锁定式单例确实降低线程同步的开销。
但是,还有更高明的写法。
延长初始化占位

package singleton; //延长初始化占位 public class LazyloadSingleton { private static class SingletonHolder { // 单例对象实例 static final LazyloadSingleton INSTANCE = new LazyloadSingleton(); } public static LazyloadSingleton getInstance() { return SingletonHolder.INSTANCE; } }
要理解上面这种单例类的写法,你需要先学习一些关于Java虚拟机如何初始化一个类的知识。

在java虚拟机中,类从被加载到虚拟机内存中开始,到卸载出内存为止, 它的整个生命周期包括了如下几个阶段: 加载(Loading) 连接(Linking) 验证(Verification) 准备(Preparation) 解析(Resolution) 初始化(Initialization) 使用(Using) 卸载(Unloading) 其中,验证、准备和解析三个部分统称为连接(Linking)。 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始, 而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也被称为动态绑定或晚期绑定)。 什么情况下需要开始类加载的第一个阶段:加载。虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。 但是对于初始化阶段,虚拟机规范则是严格规定了有且只有四种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始): 1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条字节码指令最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 这四种场景中的行为称为对一个类进行主动引用,除此之外所有引用类的方式,都不会触发类的初始化,被称为被动引用。 以下是三个被动引用的例子: 1)通过子类引用父类的静态字段,不会导致子类初始化。 2)通过数组定义来引用类,不会触发此类的初始化。 3)常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。 (以上摘自《深入理解Java虚拟机》)
从上面介绍的知识可以知道,JVM将推迟SingletonHolder类的初始化,直到第一个代码访问路径调用getInstance()方法。此时,由于SingletonHolder.INSTANCE是一个读取静态字段的主动引用,虚拟机将第一次加载SingletonHolder类,并且通过一个静态变量来初始化INSTANCE实例。而其他访问getInstance()方法的代码路径,并不需要同步。
不需要额外的同步,但是又能确保对象可见性的正确发布,这是由Java的虚拟机规范所决定的!上面这种单例模式的写法,体现出对虚拟机规范的深刻理解,实在是专家级别的写法。
用读写锁编写的单例模式
在阅读Struts2源码的时候,我发现一个有意思的单例类写法:LoggerFactory。这里和大家分享一下,先看一下源码。

package com.opensymphony.xwork2.util.logging; import com.opensymphony.xwork2.util.logging.jdk.JdkLoggerFactory; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Creates loggers. Static accessor will lazily try to decide on the best factory if none specified. */ public abstract class LoggerFactory { private static final ReadWriteLock lock = new ReentrantReadWriteLock(); private static LoggerFactory factory; public static void setLoggerFactory(LoggerFactory factory) { lock.writeLock().lock(); try { LoggerFactory.factory = factory; } finally { lock.writeLock().unlock(); } } public static Logger getLogger(Class<?> cls) { return getLoggerFactory().getLoggerImpl(cls); } public static Logger getLogger(String name) { return getLoggerFactory().getLoggerImpl(name); } protected static LoggerFactory getLoggerFactory() { lock.readLock().lock(); try { if (factory != null) { return factory; } } finally { lock.readLock().unlock(); } lock.writeLock().lock(); try { if (factory == null) { try { Class.forName("org.apache.commons.logging.LogFactory"); factory = new com.opensymphony.xwork2.util.logging.commons.CommonsLoggerFactory(); } catch (ClassNotFoundException ex) { // commons logging not found, falling back to jdk logging factory = new JdkLoggerFactory(); } } return factory; } finally { lock.writeLock().unlock(); } } protected abstract Logger getLoggerImpl(Class<?> cls); protected abstract Logger getLoggerImpl(String name); }
可以看到,在大多数的代码路径下,getLoggerFactory()方法用可重入的读锁来进行同步。
只在第一次访问时,使用了可重入的写锁来进行同步,进行factory对象的初始化。因为在写锁还没释放的时候,任何读锁的获取都会被阻塞,这样就保证了所发布的factory对象的可见性。
参考资料:
http://my.oschina.net/lichhao/blog/107766
http://www.iteye.com/topic/575052
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端