设计模式之单例模式

单例设计模式理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫单例模式。

使用场景

处理资源访问冲突

下面的示例中如果每个类都创建一个 Logger 实例,就可能造成日志内容被覆盖的情况。

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
27
28
29
30
31
public class Logger {
private FileWriter writer;

public Logger() {
File file = new File("log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}

public void log(String message) {
writer.write(mesasge);
}
}


public class UserController {
private Logger logger = new Logger();

public void login(String username, String password) {
// ...省略业务逻辑代码...
logger.log(username + " logined!");
}
}

public class OrderController {
private Logger logger = new Logger();

public void create(OrderVo order) {
// ...省略业务逻辑代码...
logger.log("Created an order: " + order.toString());
}
}

表示全局唯一类

如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类,全局 ID 生成器等。

如何实现一个单例?

要实现一个单例,我们要考虑以下几点:

  • 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载;
  • 考虑 getInstance() 性能是否高(是否加锁)。

饿汉式

1
2
3
4
5
6
7
8
9
public class Singleton {
private static final Singleton instance = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return instance;
}
}

懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。但缺点也很明显,因为使用了synchronized关键字导致这个方法的并发度很低。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,就会导致性能瓶颈,这种实现方式就不可取了。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton instance;

private Singleton() {}

public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

双重检测

这是一种既支持延迟加载、又支持高并发的单例实现方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) { // 此处为类级别的锁
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

在 java1.5 以下instance = new Singleton();有指令重排问题,需要给instance成员变量加上volatile关键字,java1.5 之后不会再这个有问题。

静态内部类

这种方式利用了 Java 的静态内部类,有点类似饿汉式,但又能做到了延迟加载。

当外部类 Singleton 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private Singleton() {}

private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.instance;
}
}

枚举

这是一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

1
2
3
4
5
6
7
8
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);

public long getId() {
return id.incrementAndGet();
}
}

如何实现线程唯一的单例?

上面的单例类对象是进程唯一的,一个进程只能有一个单例对象。那如何实现一个线程唯一的单例呢?

假设 IdGenerator 是一个线程唯一的单例类。在线程 A 内,我们可以创建一个单例对象 a。因为线程内唯一,在线程 A 内就不能再创建新的 IdGenerator 对象了,而线程间可以不唯一,所以,在另外一个线程 B 内,我们还可以重新创建一个新的单例对象 b。

我们通过一个 ConcurrentHashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);

private static final ConcurrentHashMap<Long, IdGenerator> instances
= new ConcurrentHashMap<>();

private IdGenerator() {}

public static IdGenerator getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator());
return instances.get(currentThreadId);
}

public long getId() {
return id.incrementAndGet();
}
}