写点什么

重学 Java 单例对象

作者:LamboChen
  • 2022 年 7 月 05 日
  • 本文字数:3401 字

    阅读完需:约 11 分钟

本文是对于 Java 单例对象的整理文档,内容并非原创,站在巨人肩上看更远。


单例 Singleton,即只有一个实例。

一、饿汉模式


饿汉模式即为对象创建、初始化时 立即加载 单例对象。由于在运行时(除初始化外)不存在单例对象引用的变更,故线程安全。


饿汉式加载单例对象:


public class SingletonImmediately {
private static final SingletonImmediately INSTANCE = new SingletonImmediately();
public static SingletonImmediately getInstance() { return INSTANCE; }
private SingletonImmediately() { }}
复制代码


当然,也可以用 static 代码块进行实例化。


public class SingletonImmediately {
private static final SingletonImmediately INSTANCE; static { INSTANCE = new SingletonImmediately(); }
public static SingletonImmediately getInstance() { return INSTANCE; }
private SingletonImmediately() { }}
复制代码


关键点:

  • 私有构造:防止外部直接实例化对象

  • 静态常量属性:饿汉模式体现,类加载阶段直接初始化完成

  • 对外访问接口:获取单例对象唯一方式(非常规手段除外,如:反射)

二、懒汉模式


懒汉模式,重在 字,即需要用的时候才去加载单例对象(第一次使用时加载,如对象已存在无需加载),也唤为 “延迟加载”。


随手写出懒加载单例模式代码:


public class SingletonLazy {
private static SingletonLazy INSTANCE;
public static SingletonLazy getInstance() { if (null == INSTANCE) { // 第一次访问,实例化对象 INSTANCE = new SingletonLazy(); }
return INSTANCE; }
private SingletonLazy() { }}
复制代码


很显然,这段代码漏洞百出,比如,如果多个线程同时执行, INSTANCE 则会实例化多次,大致流程如下:


  • 线程 A 进入 getInstance 方法,由于是第一次进来,发现 instance == null,则进行实例化

  • 此时线程 B 进入 getInstance 方法,由于线程 A 还未实例化完成,导致线程 B 判断 instance 也为 null,故继续执行实例化


问题就在这,多个线程并发访问导致数据未同步。那,加上同步吧。


public class SingletonLazy {
private static SingletonLazy INSTANCE; public synchronized static SingletonLazy getInstance() { if (null == INSTANCE) { // 第一次访问,实例化对象 INSTANCE = new SingletonLazy(); }
return INSTANCE; }
private SingletonLazy() { }}
复制代码


注意 getInstance 方法,添加了 synchronized 关键字修饰。


synchronized 关键字修饰 static 方法时,为类锁,即锁住的是整个 SingletonLazy 类。


很显然,这样的实现方式极为低效,每次访问 getInstance 都要拿 "类锁",实际上只需要第一次访问时获取锁,并进行对象的实例化即可。


public class SingletonLazy {
private static SingletonLazy INSTANCE;
public static SingletonLazy getInstance() { if (null == INSTANCE) { synchronized (SingletonLazy.class) { INSTANCE = new SingletonLazy(); } }
return INSTANCE; }
private SingletonLazy() { }}
复制代码


似乎现在看起来,有模有样。细心的读者可能已经发现,还有优化空间。先分析下执行流程:


  • 线程 A 进入方法体,instance 为 null,拿锁,开始实例化

  • 线程 B 进入方法体,instance 为 null(此时线程 A 还没实例化完成),尝试拿锁,没拿到锁,阻塞等待

  • 线程 A 实例化完成,出临界区,释放锁(注意,此处 instance != null)

  • 由于线程 A 释放了锁,线程 B 拿到锁,开始实例化【额,实际上线程 A 已经实例化完成了】


OK,改造一下。拿到锁后,check 下不就好了。


public class SingletonLazy {
private static SingletonLazy INSTANCE;
public static SingletonLazy getInstance() { if (null == INSTANCE) { synchronized (SingletonLazy.class) { // double check,防止在拿锁过程中,其他线程已经实例化完成,重复工作 if (null == INSTANCE) { INSTANCE = new SingletonLazy(); } } }
return INSTANCE; }
private SingletonLazy() { }}
复制代码


似乎,这样就完美了。答案当然是 no。

如果对于 Java 内存模型有过了解的同学,可能会认识到事情远没有这么简单。且听我分析一波。


分析之前,有个知识点得先同步下:对于 new 一个对象,实际上执行路径是


1. 分配一块内存 M

  • 2. 将 M 的地址赋值给 INSTANCE 变量

  • 3. 在内存 M 上初始化 SingletonLazy 对象


  • 那么 getInstance 在多线程下的工作流程即为:


    • 线程 A 进入 getInstance,instance == null,拿锁,开始实例化

    • 线程 A 分配一个内存 M,并把 M 的地址赋值给 instance

    • 此时,线程 B 进入 getInstance,此时 instance != null,直接返回【额,这里返回的 instance 实际上尚未实例化完成】

    • 线程 B 使用 instance 【尴尬了,大概率直接 NPE】

    • 线程 A 在内存 M 上初始化 SingletonLazy 对象


    想必已经了解问题的根因,实际上这是一个并发中比较经典的,可见性 问题。小问题,volatile 解决它。


    public class SingletonLazy {
    private static volatile SingletonLazy INSTANCE;
    public static SingletonLazy getInstance() { if (null == INSTANCE) { synchronized (SingletonLazy.class) { // double check,防止在拿锁过程中,其他线程已经实例化完成,重复工作 if (null == INSTANCE) { INSTANCE = new SingletonLazy(); } } }
    return INSTANCE; }
    private SingletonLazy() { }}
    复制代码


    三、静态内部类实现单例模式


    静态内部类也能实现线程安全的单例模式,实际上和 饿汉模式 区别不大。


    public class SingletonStaticInnerClass {
    public static Singleton getInstance() { return Singleton.INSTANCE; }
    private static class Singleton { private static Singleton INSTANCE = new Singleton(); }
    private SingletonStaticInnerClass() { }}
    复制代码


    同样的,在序列化对象时,这段代码得到的对象也是多实例的。

    解决办法就是在反序列化中使用 readResolve 方法


    public class SingletonStaticInnerClass implements Serializable {
    public static Singleton getInstance() { return Singleton.INSTANCE; }
    private static class Singleton { private static Singleton INSTANCE = new Singleton(); }
    private SingletonStaticInnerClass() { }
    protected Object readResolve() throws ObjectStreamException { return Singleton.INSTANCE; }}
    复制代码


    四、枚举类实现单例模式


    枚举类实现单例对象实际上是比较优雅的,除了其能很简洁的实现外,利用枚举类的特性(常量池),还能保证序列化/反序列化后也是单例。


    public enum SingletonEnumClass {
    // 单例对象 INSTANCE ;
    private Singleton instance;
    public Singleton getInstance() { return instance; }
    SingletonEnumClass() { // 初始化逻辑 this.instance = new Singleton(); }
    // 单例类 public class Singleton {
    }}
    复制代码


    可进一步优化下,职责更加单一:


    public class SingletonEnumClass {
    // 对外访问接口 public static Singleton getInstance() { return SingletonEnum.INSTANCE.instance; }
    // 单例类 public static class Singleton {
    } // 枚举类,用于实例化单例对象 public enum SingletonEnum { // 单例对象 INSTANCE;
    private Singleton instance;
    SingletonEnum() { // 初始化逻辑 this.instance = new Singleton(); } }}
    复制代码


    参考文献


    • 《Java 多线程编程核心技术》

    • 《Java 并发编程之美》


    用户头像

    LamboChen

    关注

    Stay hungry, stay foolish. 2019.02.28 加入

    还未添加个人简介

    评论

    发布
    暂无评论
    重学Java单例对象_单例模式_LamboChen_InfoQ写作社区