单例模式是最常用的设计模式之一,同时也是面试中最容易考到的设计模式。
下面介绍几种典型的单例模式实现。
一、懒汉单例模式 懒汉是指在第一次使用(调用getInstance())时才会创建单例对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class LazyManSingleton { private LazyManSingleton lazyManSingleton; private LazyManSingleton () {} public LazyManSingleton getInstance () { if (lazyManSingleton == null ){ lazyManSingleton = new LazyManSingleton(); } return lazyManSingleton; } }
二、饿汉单例模式 与懒汉不同,饿汉是在类中就已经创建好了这个类的单例对象,使用时全是调用该对象。
1 2 3 4 5 6 7 8 9 10 public class HungryManSingleton { private HungryManSingleton lazyManSingleton = new HungryManSingleton(); private HungryManSingleton () {} public HungryManSingleton getInstance () { return hungryManSingleton; } }
上述两种属于最简单的单例模式写法,各有优缺点:
在资源使用方面,懒汉单例是在外部调用到时才初始化创建对象;而饿汉式在类加载阶段就创建了对象,所以饿汉式的缺点是可能会浪费堆内存。
在多线程访问方面,饿汉式能保证线程安全;而懒汉式在多个线程第一次同时访问时,可能产生多个对象,所以缺点是线程不安全。
不过我们可以通过加同步锁保证懒汉式的安全性,下面介绍第三种单例模式写法:
三、volatile+双重检查锁单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton doubleCheckSingleton; private DoubleCheckSingleton () {} public static DoubleCheckSingleton getInstance () { if (doubleCheckSingleton == null ) { synchronized (DoubleCheckSingleton.class ) { if (doubleCheckSingleton == null ) { doubleCheckSingleton = new DoubleCheckSingleton(); } } } return doubleCheckSingleton; } }
这里比较关键的写法有3处,一是将单例对象设置为了volatile类型,二是两次判定doubleCheckSingleton == null
,三是同步锁synchronized锁住DoubleCheckSingleton.class。 讲解的时候倒过来讲。
synchronized
要锁住什么呢?synchronized
关键字可以锁方法、锁变量、锁代码块。锁方法getInstance()
可以吗?是可以的,但缺点是每次调用对象都会锁一次,效率很低。我们只需要在创建时锁住。 锁变量呢?synchronized(doubleCheckSingleton)
,这是不行的,由于已经判断出了doubleCheckSingleton == null
为真,所以这等价于synchronized(null)
,等于没有锁东西。
为什么需要两次判定? 因为第一个if判断拦不住多个线程,而synchronized锁只是在有线程占用的时候阻止了其他线程,当占用线程执行完毕,其他线程就能继续执行。如果没有第二个if拦截,依然会(顺序)创建多个对象。所以需要第二个if拦截调后面的线程。类似单片机编程中的防抖写法。
为什么将单例对象设置成volatile类型? 这里用到了 volatile 的“禁止指令重排”特性。因为赋值语句 doubleCheckSingleton = new DoubleCheckSingleton();
解析为字节码指令分为三步执行: (a) 分配新对象的堆内存 new (b) 初始化新对象 DoubleCheckSingleton() (c) 设置引用doubleCheckSingleton指向新分配的内存地址 在多线程情况下,jvm随时可能进行优化重排。假如(b)和(c)调换了位置,由于 synchronized 锁住的是类,即(b),而(c)是没有锁的,所以当线程1先执行了(a)(c)后暂停;而线程2判定doubleCheckSingleton不为空,直接返回了一个未被初始化的对象。所以需要使用volatile关键字禁止这样的指令优化重排。
爱思考的同学可能会想,既然懒汉式可以通过改进变成线程安全的,那么饿汉式能不能也改进克服资源浪费的缺点呢? 答案是可以的。下面介绍第四和第五种单例模式写法:
四、静态内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class InternalStaticSingleton { private InternalStaticSingleton () {} public static InternalStaticSingleton getInstance () { return InternalStatic.instance; } private static class InternalStatic { private static final InternalStaticSingleton instance = new InternalStaticSingleton(); } }
利用java对static的单例支持来实现单例模式。
五、内部枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class InternalEnumSingleton { private InternalEnumSingleton () {} public static InternalEnumSingleton getInstance () { return InternalEnum.INSTANCE.getInstance(); } private enum InternalEnum { INSTANCE; private InternalEnumSingleton singleton; InternalEnum() { singleton = new InternalEnumSingleton(); } public InternalEnumSingleton getInstance () { return singleton; } } }
单例模式实例应用 — Eureka源码项目 DiscoveryManager.java
DiscoveryManager 的作用是通过读取配置文件来配置 Discovery Client。这里 DiscoveryManager 作为管理员,只需要一个对象就可以了,所以源码中使用了饿汉式单例模式来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class DiscoveryManager { private static final DiscoveryManager s_instance = new DiscoveryManager(); private DiscoveryManager () { } public static DiscoveryManager getInstance () { return s_instance; } ...... }
考虑并发的情况 ConfigurationManager.java
在 DynamicPropertyFactory 中调用时加同步锁:
1 2 3 synchronized (ConfigurationManager.class ) { AbstractConfiguration configFromManager = ConfigurationManager.getConfigInstance(); }
单例模式典型应用 — 操作数据库 例:SPESC-Java-Contract-Service 项目 1 2 3 4 5 6 7 8 9 10 11 public class ContractDAO { private static final ContractDAO s_instance = new ContractDAO(); private ContractDAO () {} public static ContractDAO getInstance () { return s_instance; } ... }
使用:
1 2 3 4 5 private final ContractDAO contractDAO = ContractDAO.getInstance();public boolean addContract (Object contract) { return contractDAO.putContract((ContractTemplate) contract); }
例:JavaContract项目 以 LevelDB 为例,每次新建一个数据库操作对象的时候都会在磁盘中新建一个数据库。如果只操作一个数据库,那么程序中只能存在一个全局对象,这就可以利用单例模式的方法来编写程序。
1. 类的定义 类的定义没有什么特殊的地方。src/contracts/contracts_db_wrapper.h
2. 对象的声明 是通过全局对象来声明的。
src/chainparams/state.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 typedef struct mc_State { mc_State() { InitDefaults(); } ~ mc_State() { Destroy(); } ... ContractsDB *m_Contracts; void InitDefaults () { ... m_Contracts = NULL ; } void Destroy () { ... if (m_Contracts) { delete m_Contracts; } } }
src/contracts/contracts_db_wrapper.cpp
1 2 3 4 5 6 void ContractsItem::Zero() { ... contracts_DB = new mc_Database; mc_GetFullFileName(name, "contracts", ".db", MC_FOM_RELATIVE_TO_DATADIR|MC_FOM_CREATE_DIR, m_FileName); }
由此可以看出,单例模式是一种思想,具体实现不拘泥与教科书上的几种形式。
3. 对象的使用 在系统初始化的时候新建这个对象src/core/init-cold.cpp
1 2 3 4 5 if(mc_gState->m_Contracts->Initialize(mc_gState->m_Params->NetworkName())) { seed_error=strprintf("ERROR: Couldn't initialize contract database for blockchain %s. Please restart multichaind with reindex=1.\n",mc_gState->m_Params->NetworkName()); return InitError(_(seed_error.c_str())); }
单例模式经典应用 — 配置文件读取 参考文章:
用单例模式来讲讲线程安全
线程安全,饿汉式,懒汉式
synchronized到底锁住的是谁?
深入理解 Java 之 synchronized 到底锁住了什么
【死磕Java并发】—–深入分析synchronized的实现原理
Java并发编程:volatile关键字解析
同步方法
评论
shortname
for Disqus. Please set it in_config.yml
.