H5W3
当前位置:H5W3 > 其他技术问题 > 正文

Java中常用的线程安全的单例模式实现方式

1. 使用私有构造函数实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EagerInitializedSingleton {

private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();

/**
* 使用 private 修饰,外部或子类不能创建实例
*/
private EagerInitializedSingleton() {

}

public static EagerInitializedSingleton getInstance() {
return INSTANCE;
}
}

以上代码中,私有构造函数只调用一次,用来初始化静态变量 INSTANCE ,由于缺少 publicprotect 属性,外部或子类不能创建实例,这就保证了 EagerInitializedSingleton 全局一致性。一旦 EagerInitializedSingleton 被实例化,只会存在一个 EagerInitializedSingleton 实例,即 INSTANCE

使用 public 向外提供 getInstance() 方法,当调用 EagerInitializedSingleton.getInstance() 方法时,都会返回同一个对象引用,所以永远不会创建其他的 EagerInitializedSingleton 实例。

在这种实现方式中,有一个缺点,就是程序中不使用这个对象也会创建实例,造成资源浪费。如果你对性能要求不是很高,这种方式可以使用,这也是《Effective Java》推荐的单例模式实现方式之一

2. 使用静态代码块实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StaticBlockSingleton {

private static StaticBlockSingleton INSTANCE = null;

static {
try {
INSTANCE = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("创建实例出错");
}
}

public static StaticBlockSingleton getInstance() {
return INSTANCE;
}
}

由于静态代码块在使用实例之前就已经创建实例,这种方式不推荐。

3. 加锁的懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MySingleton {

private static MySingleton INSTANCE = null;

/**
* 使用 synchronized 修饰,使全局访问方法同步,每一次只能有一个线程执行此方法
* @return
*/
public static synchronized MySingleton getInstance() {
if (null == INSTANCE) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
}

这种实现方式保证了线程安全,在调用 getInstance() 方法时才会创建对象,起到了延迟加载的作用。但由于使用了 synchronized 来同步,在一定程度上降低了程序的并发度,运行效率比较低。

为了避免每次额外的性能开销,我们使用 双重检查加锁 方式,在获取单例对象时不是每次都有必要去判断线程同步锁,如果单例对象不为空,那我们直接返回单例对象,不再去判断线程同步锁,减少了判断线程同步锁的次数,从而提高了效率。优化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MySingleton {

private static MySingleton INSTANCE = null;

public MySingleton() {
}

public static MySingleton getInstance() {
// 第一次判断(未获取锁)
if (null == INSTANCE) {
synchronized (MySingleton.class) {
// 第二次判断(已获取锁)
if (null == INSTANCE) {
INSTANCE = new MySingleton();
}
}
}
return INSTANCE;
}
}

但是,该实现方式还是有问题,引用 维基百科 的解释如下:

(1)线程A 注意到该值未初始化,因此它获得锁并开始初始化该值;

(2)线程A 在完成初始化之前,允许编译器更新共享变量以指向部分构造的对象。例如,在 Java 中,在构造函数初始化对象时会分配内存,调用构造函数时会立即更新共享变量。

(3)线程B 注意到共享变量已经初始化,则返回值,不再获取锁。此时,线程A 还没完成初始化或者已初始化对象部分值但还没传至内存中,如果 线程B 使用了对象,则程序就会崩溃。

结合以上代码来理解,就是 线程A 在对 INSTANCE 赋值,INSTANCE 可能返回了一个初始化部分的对象,此时恰好 线程B 执行到 if (null == INSTANCE) ,由于 线程B 注意到 INSTANCE 已经初始化,则直接返回。但其实 INSTANCE 获取到的对象并没有初始化完成,只是初始化了部分值的对象,最终导致程序崩溃。

不过,从 JDK 1.5 开始,这个问题已经被修复了,我们可以使用 volatile 关键字来确保多个线程正确处理单一实例。用 volatile 改进的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MySingleton {

/**
* 使用 volatile 确保多个线程正确处理单一实例
*/
private volatile static MySingleton INSTANCE = null;

public static MySingleton getInstance() {
MySingleton result = INSTANCE;
if (null == result) {
synchronized (MySingleton.class) {
result = INSTANCE;
if (null == result) {
INSTANCE = result = new MySingleton();
}
}
}
return result;
}
}

这段代码中使用到了一个变量 result,变量 result 似乎没有必要,这个变量的作用是确保 INSTANCE 在被初始化的情况下只读取一次(由于直接返回 result,而不是 INSTANCE),可以提升性能。

4. 内部静态类实现单例模式(常用)

1
2
3
4
5
6
7
8
9
10
11
public class MySingleton {

private static class MySingletonHolder {
private static final MySingleton INSTANCE = new MySingleton();
}

public static MySingleton getInstance() {
return MySingletonHolder.INSTANCE;
}

}

MySingleton 被 JVM 加载时,该类将进行初始化,但是内部静态类 MySingletonHolder 不会被加载到内存中。只有当调用 getInstance() 方法时,MySingletonHolder 类才会被加载并创建 INSTANCE 实例。这种方式使用很广泛,也是比较常用的一种单例实现方式,因为它不需要同步,并且由于初始化阶段是 INSTANCE 在顺序操作中写入静态变量中,所以后续调用 getInstance() 方法都会返回初始化的相同变量,INSTANCE 也不会产生其他同步的性能开销。

5. 枚举实现单例模式(最佳方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum EnumSingleton {

/**
* 实例
*/
INSTANCE;

public void test() {
System.out.println("lanweihong");
}

// 其他方法实现......
}

使用:

1
EnumSingleton.INSTANCE.test();

这是实现单例的最简单方式,而且保证了线程安全,此方法无偿提供了序列化机制,保证防止多次实例化,及时面对复杂的序列化或者反射攻击。《Effective Java》也推荐使用枚举来实现单例模式:

尽管此方法尚未被广泛采用,但是单元素枚举类型是实现单例的最佳方法。

总结

如果你的单例需要继承 Enum 以外的超类,则使用内部静态类实现单例模式;否则使用单元素枚举类型实现单例模式,这也是实现单例模式的最佳方法

参考:

  1. Java Singleton Design Pattern Best Practices with Examples

  2. Double-checked locking

  3. 《Effective Java》第三版

本文地址:H5W3 » Java中常用的线程安全的单例模式实现方式

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址