0 序
- 此文系对最常见的设计模式------------单例模式的最全总结。
1 概述:单例模式

模式定义
- 单例模式:
- 保证1个类有且仅有1个实例,并提供1个访问它的全局访问点。
- 1个类有且仅有1个实例,并自行实例化向整个系统提供。
即 为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种通用方法。
-
如果一个类的实例应该在JVM初始化时被创建出来,应考虑使用: 饿汉式单例模式。
-
在使用懒汉式 单例模式时,应考虑到: 线程安全性问题。
-
通常,我们可以让1个全局变量,使得1个对象被访问,但它不能防止你实例化多个对象。
1个最好的办法就是,让类自身负责保存它的唯一实例。
这个类可以保证没有其它实例可以被创建。并且它可以提供1个访问该实例的方法。
模式的组成: 3要素

- 私有的静态的实例对象 instance
- 私有的构造函数 Singleton()
保证在该类外部,无法通过new的方式来创建对象实例
- 公有的、静态的、唯一的访问该实例对象的方法 getInstance()
模式特点
优点
- 允许可变数目的实例。
- 可保证1个类仅存在唯一的实例(保证没有其它实例可以被创建)
- 对唯一实例的受控访问:可严格地控制客户访问实例的方式和访问实例的时机。
- 可保证对单例类的所有实例化得到的都是同一个实例。
- 单例模式在实例化过程中具有一定的伸缩性。
类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 节约系统资源,避免对共享资源的多重占用。
当需要频繁创建和销毁的对象时,单例模式无疑可以提高系统的性能。
缺点
- 单例类的职责过重,在一定程度上违背了"单一职责原则"。
- 不适用于变化的对象
如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。 - 不易扩展,违背"开闭原则"。
单例类的扩展有很大的困难,因为:单例模式中没有抽象层。 - 滥用单例将带来一些负面问题:
例如:为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
适用场景
- 需要频繁实例化 ,然后销毁的对象。
- 创建对象时 耗时过多或者耗资源过多 ,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。
Eg:日志文件,应用配置等
- 控制资源的情况下,方便资源之间的互相通信。
Eg:线程池、数据库连接池等
案例实践
案例:简单的单例实现

Singleton
类,定义1个getInstance()操作,允许客户端访问它的唯一实例。
getInstance
是静态方法,主要负责创建自己类的唯一实例。
- Singleton
java
public class Singleton {
private static Singleton instance;//即 初始为null
private Singleton(){}
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
- Client
java
public class Client {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
//true (变量的对象引用地址一致,说明二者是指向同一实例化对象)
}
}
案例:线程池对象/连接池对象的创建
- 为了程序的高效率地使用多线程并发 ,然而是循环调用 ,可能导致创建线程数过多,考虑采用线程池 管理,这时候创建线程池仍然是处于循环调用中,也可能导致多个线程池,这时候就考虑使用单例模式。
java
/**
* 多线程下,线程安全的一种依赖于静态内部类实现的单例模式
*/
public class ThreadPoolFactoryUtil {//单例类
private ExecutorService executorService;
//在构造函数中创建线程池
private ThreadPoolFactoryUtil(){//私有的构造器
//获取系统处理器个数,作为线程池数量
int nThreads=Runtime.getRuntime().availableProcessors();
executorService = Executors.newFixedThreadPool(nThreads);
}
//获取本类对象
public static ThreadPoolFactoryUtil getThreadPoolFactoryUtil(){ //公有的、静态的、唯一访问实例对象的方法
return SingletonContainer.util;
}
/**
* 定义1个静态内部类,内部定义静态成员创建外部类实例
* 类级的内部类,即 静态的成员式内部类
* 该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到才会装载,从而实现了【延迟加载】
*/
private static class SingletonContainer{
//静态初始化器,由JVM来保证线程安全
private static ThreadPoolFactoryUtil util = new ThreadPoolFactoryUtil();
}
public ExecutorService getExecutorService(){ //ThreadPoolFactoryUtil的实例化对象的实例方法
return executorService;
}
}
懒汉式的单例模式(延迟加载)
定义
- 应用刚启动的时候,并不创建实例;
- 当外部调用该类的实例或者该类实例方法的时候,才创建该类的实例。(时间换空间)
特点
优点 | 缺点 |
---|---|
实例在被使用的时候才被创建,可节省系统资源; 体现了延迟加载的思想。 由于系统刚启动,且未被外部调用时,实例没有创建; | 若同一时间有多个线程同时调用 LazySingleton.getLazyInstance() 方法很有可能会产生多个实例 (即 多线程场景 下,单例失效故障) |
- 重要结论:
- 传统的懒汉式单例是【非线程安全】的
案例实践
java
public class Singleton {
private static Singleton instance = null; //【重点】初始时:私有属性 instance 为 null,不实例化(new)对象
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){ //非线程安全的一个显著原因是:可能会有多个线程同时进入 if (instance == null) { ... } 语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是【非线程安全】的。
instance = new Singleton();
}
return instance;
}
}
引申问题:多线程下,懒汉式单例模式的缺陷问题
问题描述
- 多线程的程序中,多个线程同时访问
Singleton
类,调用getInstance()
方法,会有造成创建多个实例的可能。
问题复现:非线程安全
LazySingleton
java
public class LazySingleton {
private static int count = 0;
private static LazySingleton lazyInstance=null; //初始时:私有属性instance为null,不实例化(new)对象
//↓为了易于模拟多线程下,懒汉式出现的问题:在创建实例的构造函数里,使当前线程暂停了50毫秒
private LazySingleton(){
try{
Thread.sleep(50);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("生成LazySingleton实例一次!"+(++count));
}
public static LazySingleton getLazyInstance(){
if(lazyInstance ==null){
lazyInstance = new LazySingleton();
}
return lazyInstance;
}
}

Client : 制造多个线程,同时创建单例对象
java
public class Client {
@Test
public void lazySingletonTest() {
for (int i = 0; i < 10; i++) {//创建10个线程调用
new Thread() {
@Override
public void run() {
LazySingleton.getLazyInstance();
}
}.start();//end Thread
}//end for loop
}//end method
}//end class
out:

- 单例模式失效,构造器创建/实例化了多个单例类的不同对象!
由此可见,懒汉式的单例模式 在多线程的并发场景下,会出故障。
问题分析
【分析】
- 多个线程同时访问上面的懒汉式单例,
- 现在有两个线程A和B同时访问LazySingleton.getLazyInstance()方法。
- 假设A先得到CPU的时间切片,A执行到if(lazyInstancenull)时,由于lazyInstance之前并没有实例化,所以lazyInstancenull为true,在还没有执行实例创建的时候此时CPU将执行时间分给了线程B,线程B执行到if(lazyInstancenull)时,由于lazyInstance之前并没有实例化,所以lazyInstancenull为true,线程B继续往下执行实例的创建过程,线程B创建完实例之后,返回。
- 此时,
CPU
将时间切片 分给线程A
,线程A
接着开始执行实例的创建,实例创建完之后便返回。 - 由此,看线程
A
和线程B
分别创建了1个实例(存在2个实例了),这就导致了单例的失效。
解决办法:怎么修改传统的懒汉式单例,使其线程变得安全?多线程并发环境下,如何利用Java特性实现线程安全的懒汉式单例?
方案1:(同步方法) public static synchronized LazySingleton getLazyInstance()
- 可在
getLazyInstance()
方法上加上synchronized
使其同步。
java
// 使用 synchronized 修饰,临界资源的同步互斥访问
public static synchronized LazySingleton getLazyInstance(){ ... }
但是这样一来,会降低整个访问的速度,而且每次都要判断。
方案2:(同步代码块) 双重检查加锁/双重锁定
- 那么,有没有更好的方式来实现呢?可以考虑使用"双重检查加锁"。
"双重检查加锁 "的方式来实现,就可以既实现线程安全 ,又能够使性能 不受到很大的影响。
具体代码如下:
java
public class LazySingleton {
private static int count = 0;
private static LazySingleton lazyInstance=null; //初始时:私有属性instance为null,不实例化(new)对象
//↓为了易于模拟多线程下,懒汉式出现的问题:在创建实例的构造函数里,使当前线程暂停了50毫秒
private LazySingleton(){
try{
Thread.sleep(50);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("生成LazySingleton实例一次!"+(++count));
}
//双重检查加锁 解决懒汉式单例模式的多线程不安全问题
public static LazySingleton getLazyInstance(){
if(lazyInstance==null){//先检查实例是否存在?(第1次) 若不存在时:才进入下面的同步块(即 第一重加锁处理)
//同步块,线程安全地创建实例
synchronized (LazySingleton.class){ //【重点】 使用 synchronized 块,临界资源的同步互斥访问
//再次检查实例是否存在?(第2次)如果不存在才真正地创建实例
if(lazyInstance==null){
lazyInstance=new LazySingleton();
}
}
}
return lazyInstance;
}
}

out:
``java
生成 LazySingleton 实例1次!
#### 方案3:静态内部类(实现 延迟加载和线程安全)
+ <span style="color:red">静态内部类 = 静态的成员式内部类</span>
+ 静态内部类无需依赖于外部类,它可独立于外部对象而存在。
+ 静态内部类,多个外部类的对象可【共享】同一个内部类的对象。
+ 使用**静态内部类**的【好处】: 加强了代码的封装性及提高了代码的可读性。
+ 普通内部类不能声明`static`的方法和变量
> 即 `final static` 修饰的属性还是可以的,而静态内部类形似外部类,无任何限制
> 可直接被用外部类名+内部类名获得
+ 具体代码:
```java
public class GracefulSingleton {//单例类
private GracefulSingleton() { //私有的构造方法
System.out.println("创建GracefulSingleton实例一次!");
}
public static GracefulSingleton getInstance() {//公有的、静态的、唯一的访问实例对象的方法
return SingletonHolder.instance;
}
/**
* 定义1个静态内部类,内部定义静态成员创建外部类实例
* 类级的内部类,即 静态的成员式内部类
* 该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到才会装载,从而实现【延迟加载】
*/
private static class SingletonHolder {//【重点】
//静态初始化器,由JVM来保证线程安全
private static GracefulSingleton instance = new GracefulSingleton();
}
}

方案4:(单例模式的变种)单例模式 与 ThreadLocal
- 借助于
ThreadLocal
,我们可以实现双重检查模式的变体 ------------核心思想:将临界资源instance
线程私有化(局部化)
具体到本例就是: 将双重检测 的第一层检测条件 if (instance == null) 转换为线程局部范围内的操作,对应的代码清单如下:
java
public class Singleton {
// ThreadLocal 线程局部变量,将单例instance线程私有化
private static ThreadLocal<Singleton> threadlocal = new ThreadLocal<Singleton>(); //【重点】
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
// 第一次检查:若线程第一次访问,则进入if语句块;否则,若线程已经访问过,则直接返回ThreadLocal中的值
if (threadlocal.get() == null) {
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:该单例是否被创建
instance = new Singleton();
}
}
threadlocal.set(instance); // 将单例放入ThreadLocal中
}
return threadlocal.get();
}
}
out
log
Output(完全一致):
1028355155
1028355155
1028355155
1028355155
1028355155
1028355155
1028355155
1028355155
1028355155
1028355155
饿汉式的单例模式(立即加载)
- 开发中,相较于懒汉式单例模式,饿汉式使用更多。
定义
- 应用刚启动的时候,不管外部有没有调用该类的实例方法,该类的实例就已经创建好了。(空间换时间)
特点
- 优点
- 写法简单;
- 在多线程下,也能保证单例实例的唯一性;
- 不用同步;
- 运行效率高
- 缺点
- 一直未被调用时,可能存在无意义地消耗系统资源 (资源浪费)
案例实践
java
public final class Singleton { //final阻止派生类,派生可能会增加实例
//在第1次引用类的任何成员时,就创建单例类实例。
private static Singleton instance=new Singleton(); //【重点】初始时:私有属性instance就实例化(new)对象
private Singleton(){}
public static Singleton getInstance(){//直接返回实例
return instance;
}
}

辨析对比:饿汉式单例 VS 懒汉式单例
总结
- 从速度/性能 、反应时间 角度来讲,饿汉式 (又称立即加载)要好一些;
- 从资源利用效率 上说,懒汉式 (又称延迟加载)要好一些。
案例与源码分析 | Runtime
类 : 饿汉式单例
java
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
Runtime
类封装了Java运行时的环境。
- 每一个java程序 实际上都是启动了一个JVM进程。
- 那么,每个JVM进程 都是对应这一个Runtime实例,此实例是由JVM为其实例化的。
- 每个 Java 应用程序 都有一个
Runtime
类实例,使应用程序能够与其运行的环境相连接。- 由于Java是单进程的。
所以,在一个JVM中,
Runtime
的实例应该只有一个。所以,应该使用单例来实现。
- 饿汉式单例模式
- 在
Runtime
类第一次被classloader
加载的时候,实例就被创建出来了。- 一般不能实例化一个
Runtime
对象,应用程序也不能创建自己的 Runtime 类实例;- 但可以通过
getRuntime
方法获取当前Runtime
运行时对象的引用。
案例与源码分析 | JDBC/DriverManager#getConnection(...): 懒汉式单例(双重检查加锁)
- 回忆一下: 基于 jdbc 获取连接
java
// 注册 JDBC 驱动
Class clz = Class.forName(JDBC_DRIVER);
// 打开链接
conn = DriverManager.getConnection(DB_URL,USER,PASS);
- DriverManager
java
@CallerSensitive
public static Connection getConnection(String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
// Worker method called by the public getConnection() methods.
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) { throw new SQLException("The url cannot be null", "08001"); }
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

案例与源码分析 | Integer.valueOf(int i)
:懒汉式单例模式(静态内部类)
- Integer
java
public static Integer valueOf(int i) {//懒汉式单例模式
if (i >= IntegerCache.low && i <= IntegerCache.high)//已实例化?是,返回IntegerCache数组的对应实例
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);//尚未实例化?是,返回新实例对象
}
- IntegerCache
注:
IntegerCache
是一个数组
java
private static class IntegerCache {//私有的静态内部类,用
static final int low = -128;
static final int high;
static final Integer cache[]; //待返回的单例(数组)实例对象(//静态内部类下静态成员,由JVM来保证线程安全)
}
案例与源码分析 | java.awt.Toolkit类#getDefaultToolkit() : 懒汉式单例
- 不需要事先创建好,只要在第一次真正用到的时候再创建就可以了。
- 因为很多时候并不常用
Java
的GUI
和其中的对象。 - 如果使用饿汉单例 的话,会影响
JVM
的启动速度。
Toolkit
java
public abstract class Toolkit {
private static Toolkit toolkit;
public static synchronized Toolkit getDefaultToolkit() {
if (toolkit == null) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
Class<?> cls = null;
String nm = System.getProperty("awt.toolkit");
try {
cls = Class.forName(nm);
} catch (ClassNotFoundException e) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
if (cl != null) {
try {
cls = cl.loadClass(nm);
} catch (final ClassNotFoundException ignored) {
throw new AWTError("Toolkit not found: " + nm);
}
}
}
try {
if (cls != null) {
toolkit = (Toolkit)cls.newInstance();
if (GraphicsEnvironment.isHeadless()) {
toolkit = new HeadlessToolkit(toolkit);
}
}
} catch (final InstantiationException ignored) {
throw new AWTError("Could not instantiate Toolkit: " + nm);
} catch (final IllegalAccessException ignored) {
throw new AWTError("Could not access Toolkit: " + nm);
}
return null;
}
});
loadAssistiveTechnologies();
}
return toolkit;
}
}
案例与源码分析 | spring 应用的单例多线程问题
spring 应用的对象创建方式
- Spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方。
即 如无特殊配置,Spring框架中默认的、大多数的类实例都是单例对象
例如:Controller层的实例、Service层的实例、DAO层的实例。
- 单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
- 当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求多对应的业务逻辑(成员方法)。此时就要注意了,如果该处理逻辑中有对该单例对象状态的修改(体现为该单列的成员属性),则必须考虑线程同步问题。
同步机制的比较
锁的同步机制 : 多个线程访问共享对象时需排队 (以时间换空间)
- 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。
这时该变量是多个线程共享 的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
ThreadLocal : 为每一个线程提供一个独立的变量副本 (以空间换时间)
ThreadLocal
和线程同步机制相比有什么优势呢?
ThreadLocal
和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。- 而
ThreadLocal
则从另一个角度来解决多线程的并发访问 。ThreadLocal会为每一个线程提供一个独立的变量副本 ,从而隔离 了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal
提供了线程安全的共享对象 ,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal
。- 由于
ThreadLocal
中可以持有任何类型的对象,低版本JDK 所提供的get()
返回的是Object
对象,需要强制类型转换 。但JDK 5.0
通过泛型 很好的解决了这个问题,在一定程度地简化了ThreadLocal
的使用- 概括起来说,对于多线程资源共享 的问题,同步机制 采用了"以时间换空间 "的方式,而
ThreadLocal
采用了"以空间换时间"的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring使用ThreadLocal解决线程安全问题
-
在一般情况下,只有无状态的Bean 才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
-
一般的【Web应用】划分为展现层 、服务层 和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。
在一般情况下,从接收请求 到返回响应所经过的所有程序调用都同属于一个线程。
ThreadLocal
是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本 解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal
比直接使用synchronized
同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
是否存在【线程安全】问题的判断思路
- 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。
- 如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 或者说:一个类或者程序所提供的接口对于线程 来说是原子操作 或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
即 线程安全问题 都是由全局变量 及静态变量引起的。
- 若每个线程中对全局变量 、静态变量 只有读操作 ,而无写操作。
一般来说,这个全局变量 是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步 ,否则就可能影响线程安全。
txt
1)【常量】始终是【线程安全】的,因为只存在读操作。
2)每次调用方法前都新建一个实例是【线程安全】的,因为不会访问共享的资源。
3)【局部变量】是线程安全的。因为每执行一个方法,都会在【独立的空间】创建【局部变量】,它不是【共享的资源】。
局部变量包括方法的参数变量和方法内变量。
- 有状态就是有数据存储功能。
有状态对象 (Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间,不保留任何状态。
- 无状态就是一次操作,不保存数据。
无状态对象 (Stateless Bean),就是没有实例变量的对象。不能保存数据,是不变类,是线程安全的。
参考文献
Strcut2 的线程安全问题
- 基于
Struts2
框架(类比 Java Web 框架: Spring MVC)的应用程序的Java对象,默认 的实现是Prototype
模式。
也就是每个请求 都新生成一个
Action
实例,所以不存在线程安全问题。
- 需要注意的是,如果由
Spring
管理action
的生命周期,scope
要配成prototype
作用域。
Z FAQ
Q: 与实用类(XxxUtils)的区别?
实用类 | 单例类 |
---|---|
不保存状态 仅提供一些静态方法或静态属性让客户访问 (Eg:Math类) | 保存状态 |
不能用于继承多态 | 允许有子类继承 |
一些方法、属性的集合 | 有着唯一的对象实例 |