• 作用:单例模式主要解决的是,防止一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。
  • 特点:单例模式有一个特点就是不允许外部直接创建,因此在默认的构造函数上添加了私有属性 private

懒汉式(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 懒汉式
*/
public class Lazy {

private static Lazy instance;

/**
* 私有构造方法,防止被实例化
*/
private Lazy(){
}

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

优点:延迟加载,真正用的时候才实例化对象,提高了资源的利用率

缺点:存在并发访问的问题(可能进行了多次new操作)

懒汉式(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 懒汉式
*/
public class Lazy {

private static Lazy instance;

private Lazy(){
}

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

拥有上面懒汉式的优点,同时也克服了其缺点,使用synchronized关键字同步加锁,保证了线程安全,但所有的访问都需要加锁,造成了资源的浪费。

饿汉式(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 饿汉式
*/
public class Hungry {

private static Hungry hungry = new Hungry();

/**
* 私有构造方法,防止被实例化
*/
private Hungry(){
}

public static Hungry getInstance(){
return hungry;
}
}

优点:static变量会在类装载时初始化,不存在并发访问问题,可以省略synchronized关键字

缺点:类初始化时就创建了对象,如果只是加载本类,而不是要调用 getInstance(),甚至永远没有调用,则会造成资源浪费

双重校验锁(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 双重校验锁
*/
public class DoubleLock {

private static volatile DoubleLock instance;

private DoubleLock(){
}

public static DoubleLock getInstance(){
if(instance != null)
return instance;
synchronized (DoubleLock.class){
if(instance == null)
instance = new DoubleLock();
}
return instance;
}
}
  • 双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。
  • 同时这种方式也满足了懒加载。

为什么使用volatile

采⽤ volatile 关键字修饰也是很有必要的, singleton = new Singleton(); 这段代码其实是分为三步执⾏:

  1. 为 singleton 分配内存空间

  2. 初始化 singleton

  3. 将 singleton 指向分配的内存地址

    但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1-3-2。指令重排在单线程环境下不会出 现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getInstance() 后发现 singleton 不为空,因此返回 singleton ,但此时 singleton 还未被初始化。 使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。

静态内部类(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 静态内部类
*/
public class InnerClass {

private static class inner{
private static InnerClass instance = new InnerClass();
}

private InnerClass(){
}

public static InnerClass getInstance(){
return inner.instance;
}
}
  • 既保证了线程安全又保证了懒加载,同时不会因为加锁的方式耗费性能。
  • 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。

CAS(线程安全)

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

private static final AtomicReference<CAS> INSTANCE = new AtomicReference<>();

private CAS(){
}

public static CAS getInstance(){
for( ; ; ){
CAS instance = INSTANCE.get();
if(instance != null){
return instance;
}
INSTANCE.compareAndSet(null, new CAS());
return INSTANCE.get();
}
}
}
  • java并发库提供了很多原子类来支持并发访问的数据安全性;AtomicIntegerAtomicBooleanAtomicLongAtomicReference
  • AtomicReference 可以封装引用一个V实例,支持并发访问如上的单例方式就是使用了这样的一个特点。
  • 使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
  • 当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。

枚举单例(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 枚举单例
*/
public enum EnumSingle {

INSTANCE;

public void test(){
System.out.println("枚举单例");
}

public static void main(String[] args) {
EnumSingle.INSTANCE.test();
}
}
  • 优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
  • 缺点:无延迟加载

参考资料

重学 Java 设计模式:实战单例模式「7种单例模式案例,Effective Java 作者推荐枚举单例模式」 - bugstack虫洞栈

彻底玩转单例模式