单例的几种实现

作为GoF 23种设计模式之一的单例模式,在程序之中有大量的使用。所谓单例模式,简单说就是确保类在程序中只会被创建一个实例。看起来似乎很简单,那么下面这个样例符合基本需求吗?

public class Singleton {
  private static Singleton instance = new Singleton();
  public static Singleton getInstance() {
    return instance;
  }
}

是不是缺了点什么?原来,Java会自动为没有明确声明构造函数的类,定义一个public的无参构造函数。所以上面的例子并不能保证额外的对象实例被创建出来,别人完全可以直接“new Singleton()”。解决的方法十分简单,可以定义一个private的构造函数。

public class Singleton {
  private static Singleton instance = new Singleton();
  private Singleton() {}
  public static Singleton getInstance() {
    return instance;
  }
}

也有人建议声明为枚举,这是有争议的,我个人不建议选择相对复杂的枚举。毕竟日常开发不是学术研究。这样做除了有“炫技”的嫌疑外,看不出有什么明显的好处。

在这个专栏之前介绍HashMap的文章中,提到JDK标准类库中很多地方使用懒加载(lazy-load),改善初始内存开销,在单例的问题上也适用:

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

这个实现在单线程环境下不存在问题,但如果处于并发场景下,就需要考虑线程安全。最简单就是将getInstance()标记为synchronized。

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

事实上一旦对象实例被创建后,instance就不会被修改,如果每次调用getInstance()都要进入同步块,将导致性能低下。改进的方法是使用“双检锁”:

public class Singleton {
  private static volatile Singleton instance = null;
  private Singleton() {}
  public static Singleton getInstance() {
    if (instance == null) { // 尽量避免重复进入同步块
      synchronized (Singleton.class) {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

这段实现的要点在于:

  • 这里的volatile能够提供可见性,以及保证getInstance返回的是初始化完全的对象。也就是说,当Singleton类本身有多个成员变量时,需要保证初始化过程完成后,才能被get到。
  • 在同步之前进行null检查,目的是尽量避免进入相对昂贵的同步块。
  • 直接在class级别进行同步,保证线程安全的类方法调用。

在现在Java中,内存排序模型(JMM)已经非常完善,通过volatile的write或者read,能保证所谓的happen-before,也就是避免常被提到的指令重排。换句话说,构造对象的store指令能够被保证一定在volatile read之前。

当然,也有一些人推荐利用内部类持有静态对象的方式实现。其理论依据是对象初始化过程中隐含的初始化锁,这种和前面的双检锁实现都能够保证线程安全。不过语法稍显晦涩,未必有特别的优势。

public class Singleton {
  private Singleton() {}
  public static Singleton getInstance() {
    return Holder.instance;
  }
  private static class Holder {
    private static Singleton instance = new Singleton();
  }
}

可以看出,即使是看似简单的单例模式,在增加各种高标准需求之后,同样需要非常多的实现考量。

上面是比较学究的实现,其实实践中未必需要如此复杂。比如来自Java核心类库自己的单例实现java.lang.Runtime:

private static final Runtime currentRuntime = new Runtime();
private static Version version;
// ...
public static Runtime getRuntime() {
  return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
  • 它并没有使用复杂的双检锁之类。
  • 静态实例被声明为final,这是被通常实践忽略的,一定程度保证了实例不被篡改(反射之类可以绕过私有访问限制),也有有限的保证执行顺序的语义。

【完】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值