首页 单例模式
文章
取消

单例模式

什么是单例模式

保证整个系统中一个类只有一个实例,并且提供一个可以全局访问的入口,实现这种功能的方式就叫做单例模式

为什么要使用单例模式

节省公共资源

一些对象频繁的创建和销毁会非常耗费性能和资源,比如,频繁访问数据库或文件的对象(DruidDataSource、SqlSessionFactory)

这些操作往往很费时,如:查询数据库并对查到的数据做大量计算、加解密、解压缩和读取文件等

解决在多线程下资源访问冲突的情况

以日志管理为例:

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

单例模式配合线程安全的 writer(),能避免资源访问冲突问题

如果 Logger 不是单例,那么在多个线程同时操作文件 “/path/log.txt” 时可能出现线程安全问题,如下图所示:

有些数据在内存中只需保存一份,如配置信息类、Class<?> 类

如何实现一个单例

思路:

  1. 构造器私有
  2. 静态方法返回实例
  3. 是否支持延迟加载 ?懒汉 :饿汉

饿汉模式

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
29
30
31
32
/**
 * 声明时初始化
 */
public class SingletonDemo1 {
  private final static SingletonDemo1 INSTANCE = new SingletonDemo1();
  
  public static getInstance(){
    return INSTANCE;
  }
}

/**
 * 静态代码块初始化
 */
public class SingletonDemo2 {
  private final static SingletonDemo2 INSTANCE;

  static {
    INSTANCE = new SingletonDemo2();
  }

  public static getInstance(){
    return INSTANCE;
  }
}

/**
 * 巧用枚举,当只有一个枚举实例时,其实就是单例模式
 */
public enum SingletonDemo3 {
  INSTANCE;
}

上面的单例对象都是在类加载时就已经被创建了,所以不存在线程安全问题

懒汉模式

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
29
30
31
32
33
34
35
36
37
38
/**
 * 虽然是内部类但也是独立的,只有在使用到时才会加载
 */
public class SingletonDemo6 {

  private SingletonDemo6(){}

  /**
   * 静态内部类相对于外部类是独立的,只有在被使用时或者方法被调用时会被加载
   */
  private static class Inner{
    private static SingletonDemo6 INSTANCE = new SingletonDemo6();
  }
  public static SingletonDemo6 getINSTANCE() {
    return Inner.INSTANCE;
  }
}

/**
 * 双重检测(提高并发量)
 */
public class SingletonDemo6 {
  private static volatile SingletonDemo6 INSTANCE;
  private SingletonDemo6(){}

  public static SingletonDemo6 getINSTANCE() {
    //最外层检测避免每个线程都去获得/释放 class 的锁(耗时操作)
    if(INSTANCE == null){
      synchronized(SingletonDemo6.class) {
        if(INSTANCE == null){
          INSTANCE = new SingletonDemo6();
        }
      }
    }
    return INSTANCE;
  }

}

双重检测中的 volatile

在双重检查锁模式中为什么需要使用 volatile 关键字

new SingletonDemo6() 实际不是一个原子操作,在 JVM 中这条语句至少做了下面这 3 件事:

  1. 给 INSTANCE 分配内存空间
  2. 调用构造器初始化 INSTANCE
  3. 将 INSTANCE 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)

但是由于指令重排优化,创建对象的顺序不一定是 1 -> 2 -> 3,也可能是 1 -> 3 -> 2,这样有个问题:对象还未初始化,却已经不为空了,这样双重检查就会失效,那么 INSTANCE 的使用就会报错,具体如下图:

总结

使用了 volatile 之后,相当于是表明了该属性的更新可能是在其他线程中发生的,而 volatile 的意义主要在于它可以避免拿到没完成初始化的对象,从而保证了线程安全。

本文由作者按照 CC BY 4.0 进行授权

代理模式

原型模式