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

案例1:处理资源访问冲突

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
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("/Users/wangzheng/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}
public void log(String message) {
writer.write(mesasge);
}
}
// Logger类的应用示例:
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());
}
}

通过上面的例子可以看出,我们在两个controller中创建了两个Logger对象,在多线程环境下,如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数,同时写入日志文件,则可能会导致日志互相覆盖的情况。

解决办法:

1、加锁

我们可以给Logger对象加一把类级别的锁,同一时刻只有一个线程可以访问Logger对象。

1
2
3
4
5
6
7
8
9
10
11
12
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("/Users/wangzheng/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}
public void log(String message) {
synchronized(Logger.class) { // 类级别的锁
writer.write(mesasge);
}
}
}

另外,分布式锁以及并发队列也可以解决这个问题。

2、单例模式

我们将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享 使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线 程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。

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
public class Logger {
private FileWriter writer;
private static final Logger instance = new Logger();
private Logger() {
File file = new File("/Users/wangzheng/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}
public static Logger getInstance() {
return instance;
}
public void log(String message) {
writer.write(mesasge);
}
}
// Logger类的应用示例:
public class UserController {
public void login(String username, String password) {
// ...省略业务逻辑代码...
Logger.getInstance().log(username + " logined!");
}
}
public class OrderController {
private Logger logger = new Logger();
public void create(OrderVo order) {
// ...省略业务逻辑代码...
Logger.getInstance().log("Created a order: " + order.toString());
}
}

案例2:表示全局唯一类

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

此处省略代码。

如何实现一个单例?

实现单例需要关注以下几个点:

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

1. 饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好 了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加 载(在真正用到 IdGenerator 的时候,再创建实例)。

1
2
3
4
5
6
7
8
9
10
11
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}

2. 懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}

缺点也很明显,我们加了锁,导致并发度之有1,即串行操作,如果频繁的被用到,则会影响性能。

3. 双重检测

该方法既支持延迟 加载、又支持高并发的单例实现方式。该方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再 进入到加锁逻辑中了。

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 IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}

这种实现在低版本的jdk中,会因指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。我们需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才 行。

但是在高版本的jdk不存在这个问题。

4. 静态内部类

它有点 类似饿汉式,但又能做到了延迟加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会 被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。

参考

《设计模式之美》