Design patterns ── Singleton

单例模式是最常用的设计模式之一,同时也是面试中最容易考到的设计模式。

下面介绍几种典型的单例模式实现。

一、懒汉单例模式
懒汉是指在第一次使用(调用getInstance())时才会创建单例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Lazy man Singleton
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
// Hungry man Singleton
public class HungryManSingleton {
private HungryManSingleton lazyManSingleton = new HungryManSingleton();

private HungryManSingleton() {}

public HungryManSingleton getInstance() {
return hungryManSingleton;
}
}

上述两种属于最简单的单例模式写法,各有优缺点:

  1. 在资源使用方面,懒汉单例是在外部调用到时才初始化创建对象;而饿汉式在类加载阶段就创建了对象,所以饿汉式的缺点是可能会浪费堆内存。
  2. 在多线程访问方面,饿汉式能保证线程安全;而懒汉式在多个线程第一次同时访问时,可能产生多个对象,所以缺点是线程不安全。

不过我们可以通过加同步锁保证懒汉式的安全性,下面介绍第三种单例模式写法:

三、volatile+双重检查锁单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Double check Singleton
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。
讲解的时候倒过来讲。

  1. synchronized 要锁住什么呢?
    synchronized 关键字可以锁方法、锁变量、锁代码块。锁方法getInstance() 可以吗?是可以的,但缺点是每次调用对象都会锁一次,效率很低。我们只需要在创建时锁住。
    锁变量呢?synchronized(doubleCheckSingleton),这是不行的,由于已经判断出了doubleCheckSingleton == null为真,所以这等价于synchronized(null),等于没有锁东西。
  2. 为什么需要两次判定?
    因为第一个if判断拦不住多个线程,而synchronized锁只是在有线程占用的时候阻止了其他线程,当占用线程执行完毕,其他线程就能继续执行。如果没有第二个if拦截,依然会(顺序)创建多个对象。所以需要第二个if拦截调后面的线程。类似单片机编程中的防抖写法。
  3. 为什么将单例对象设置成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
// Internal static class singleton
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
// Internal enum class singleton
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
/**
* <tt>Discovery Manager</tt> configures <tt>Discovery Client</tt> based on the
* properties specified.
*
* <p>
* The configuration file is searched for in the classpath with the name
* specified by the property <em>eureka.client.props</em> and with the suffix
* <em>.properties</em>. If the property is not specified,
* <em>eureka-client.properties</em> is assumed as the default.
*
* @author Karthik Ranganathan
*
*/
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()));
}

单例模式经典应用 — 配置文件读取

参考文章:

  1. 用单例模式来讲讲线程安全
  2. 线程安全,饿汉式,懒汉式
  3. synchronized到底锁住的是谁?
  4. 深入理解 Java 之 synchronized 到底锁住了什么
  5. 【死磕Java并发】—–深入分析synchronized的实现原理
  6. Java并发编程:volatile关键字解析
  7. 同步方法
Chrome 浏览器插件推荐 UML diagrams --- from models to codes

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×