单例模式是一种常见的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点。在Java中,有多种实现单例模式的方式,每种方式都有其优缺点,特别是在并发环境下。以下是几种常见的单例模式实现方式及其并发问题的解决方案:

1. 饿汉加载(Eager Initialization)

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;
}
}

优点:

  • 实现简单。
  • 线程安全,无需额外的同步机制。

缺点:

  • 实例在类加载时创建,可能会浪费资源,尤其是当实例化成本较高且实际使用频率较低时。

并发问题:

  • 由于实例在类加载时创建,不存在并发问题。

2. 懒汉加载(Lazy Initialization)

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

private Singleton() {}

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

优点:

  • 实例在第一次使用时创建,节省资源。

缺点:

  • 不是线程安全的,多个线程同时调用getInstance()时可能会创建多个实例。

并发问题:

  • 多线程环境下可能会创建多个实例。

解决方案:

  • 加锁:
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;
}
}
  • 双重检查锁定(Double-Check Locking):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static volatile Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
  • 使用volatile关键字确保instance的可见性,避免指令重排序问题。
  • 双重检查确保在多线程环境下只创建一个实例,同时减少锁的开销。

3. 静态内部类(Static Inner Class)

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的类加载机制。

并发问题:

  • 由于实例在类加载时创建,不存在并发问题。

4. 枚举(Enum)

1
2
3
4
5
6
7
public enum Singleton {
INSTANCE;

public void someMethod() {
// 实现方法
}
}

优点:

  • 实现简单。
  • 线程安全,无需额外的同步机制。
  • 防止反序列化重新创建新的对象。

缺点:

  • 枚举的使用场景有限,不适合需要继承的场景。

并发问题:

  • 由于枚举的特性,不存在并发问题。

5. 反序列化和反射问题

  • 反序列化: 如果单例类实现了Serializable接口,反序列化时可能会创建新的实例。
  • 解决方案: 在单例类中添加readResolve方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;

private static final Singleton INSTANCE = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return INSTANCE;
}

protected Object readResolve() {
return INSTANCE;
}
}
  • 反射: 通过反射可以调用私有构造函数,创建新的实例。
  • 解决方案: 在构造函数中添加检查逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static final Singleton INSTANCE = new Singleton();

private Singleton() {
if (INSTANCE != null) {
throw new IllegalStateException("Already initialized");
}
}

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

总结

  • 饿汉加载:简单且线程安全,但可能浪费资源。
  • 懒汉加载:节省资源,但需要解决并发问题,推荐使用双重检查锁定。
  • 静态内部类:简单、线程安全且节省资源。
  • 枚举:简单、线程安全且防止反序列化问题,但使用场景有限。

根据实际需求选择合适的单例模式实现方式,并注意解决并发问题。