Java设计模式总结:Singleton Pattern(单例模式)

概述

单例模式(Singleton Pattern)是最基础的设计模式,也是Java面试常考问题之一。单例模式提供了一种创建对象实例的方式。单例类只有一个实例,并为所有其他类提供一个获取实例的方法

为什么要用单例模式

优点

  • 开销低:内存中只有一个对象实例,降低了系统在创建和销毁对象实例时产生的内存消耗。
  • 效率高:只创建一次对象实例,GC 次数减少,提高 GC 工作效率

使用场景

  • 生成 UUID(Universally Unique Identifier)。
  • 计数器使用单例缓存,可以不必每次刷新都在数据库里加一次。
  • 需要控制实例数量,降低系统开销的场景。

单例模式的实现方式

单例模式的实现模式大致分为以下两类:

  • 懒汉式:全局单例实例在类加载时构建
  • 饿汉式:全局单例实例在被第一次调用时构建

饿汉式(线程安全)

实现单例最常用的方式。由于类加载时就实例化对象,保证了线程安全,且执行效率较高。但是如果没有用到该类,就会导致内存浪费。

1
2
3
4
5
6
7
8
9
10
public class Singleton {
// 类加载时实例化单例对象
private final static Singleton instance = new Singleton();
// 私有化构造函数
private Singleton() {}
// 提供其他类获取实例的静态方法
public static Singleton getInstance() {
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 Singleton getInstance() {
// 判断单例是否已存在,若存在则返回单例对象,不存在则创建实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

懒汉式(线程安全)

在上一种实现方式的前提下,为 getInstance 方法加锁 synchronized,这种做法保证了线程安全,但是很大程度上降低了运行效率,实际上大部分情况下是不需要同步的。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {  
private static Singleton instance;
private Singleton (){}
// 获取实例的方法加锁
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

双重校验锁式

双重校验锁 DCL(double-checked locking),通过 volatile 关键字两次判断空值的方式实现单例,这里的 volatile 关键字可以防止指令重排导致的DCL失效。这种方式可以保证线程安全,且调用 getInstance 方法没有同步锁,资源利用率高,但第一次加载会稍微慢一些。

DCL失效:假如线程A执行到 instance = new Singleton(),大致做了如下三件事:

  1. 给实例分配内存
  2. 调用构造函数,初始化成员字段
  3. 将 instance 对象指向分配的内存空间(此时 instance 不是 null
    如果执行顺序是1-3-2,那多线程下,A线程先执行3,2还没执行的时候,此时 instance!=null,这时候,B线程直接取走instance ,使用会出错,难以追踪。JDK1.5及之后volatile 解决了DCL失效问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
// volatile保证每次使用时,都从主存中取
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;
}
}

登记式/静态内部类(线程安全,最简单的单例实现)

能与双重校验锁实现相同的效果,且更加简洁,是面试官最想看到的答案。这种实现方式涉及 JVM 装载,装载内部类是线程安全的,实现了只有调用 getInstance 方法时,才装载 SingletonHolder 类,实例化单例对象(也称为懒加载),而且这种方式不会产生线程安全问题,内存消耗也低。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
// 私有化构造函数
private Singleton() {}
// 静态内部类不会在一开始被装载 所有没有内存消耗问题
// JVM在装载静态内部类是线程安全的 只有在使用内部类才会去装载 所以线程是安全的
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 提供其他类获取实例的方法
public static synchronized Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

枚举式(不常见)

这种方式是《Effective Java》 作者 Josh Bloch 提倡的方式。不仅不会产生线程安全问题,而且支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,但此方式目前还未被广泛使用,因此了解即可。

1
2
3
4
5
6
7
enum Singleton {
// 定义一个枚举元素,即 Singleton 的一个实例
INSTANCE;
public void enumSingleton() {
System.out.println("枚举式单例");
}
}

使用 Singleton.INSTANCE 即可获得所要实例

1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.enumSingleton();
}
}

Spring框架中使用单例模式

  • xml 配置单例

    1
    <bean id="singletonClass" class="com.wzc.SingletonClass" scope="singleton"/>
  • 注解声明单例

    1
    2
    3
    4
    @Scope("singleton")
    public class SingletonClass {
    //...
    }

总结

  • 单例类必须要将构造函数私有化
  • 通过静态方法获取唯一实例
  • 保证线程安全,以及单例类不会在其他类中被实例化第二次
  • 在单例模式的多种实现方式中,推荐使用双重校验锁、静态内部类、枚举






扫一扫,关注我的微信公众号↓

0%