单例模式
单例模式应该是我及大多数Java程序员最早接触的设计模式之一,也是最常用的设计模式之一。
是什么?
单例模式是确保一个类只有一个实例,并提供一个全局访问点的一个模式。
作用及应用场景
在Java应用中,单例模式能保证在一个JVM中,该单例对象只有一个实例存在。
单例模式的uml图:
应用场景如下:
- 在开发中,有一些对象可能我们只需要一个,例如:线程池、缓存、日志对象、一些操作系统设备的类。
- 相比于用new关键字创建多个对象,能降低系统开销,减轻jvm压力。
但是,为什么不能用其他东西替代单例(比如,用程序员间的约定替代单例,又比如,Java.lang.Math类是采用静态方法类而非单例的方式实现的?),为什么要用单例?
第一,单就这个事例看,单例模式比约定好,单例模式付出的代价仅仅是代码上的,而且单例却能避免约定中会产生的很多问题。 第二,单例模式与静态类的对比,网上说法众说纷纭,而且有不少是有问题的。我的看法如下。
单例模式和静态类对比
单例模式 | 静态类 | |
---|---|---|
典型例子 | java.lang.Runtime | java.lang.Math |
初始化时间点 | 用到时创建 | 加载时创建 |
OOP(主要) | 可以实现接口;单例对象能作为其他类的属性或方法参数 | (空) |
注:
- 初始化时间点中仅针对主流用法,单例的时间点也要取决于具体哪种单例,而静态类的加载其实与具体jvm的实现有关,某些jvm是在用到时才创建的。
单例的各种写法
饿汉式加载
public class Singleton1 {
public static final Singleton1 instance = new Singleton1();
private Singleton1() {
}
}
急切的加载写法,对于一些必定会使用到且启动早期就会使用的对象(如日志对象),使用该方法并不是不行。
双重校验锁
/**
* 双重校验锁
*/
public class Singleton2 {
private volatile static Singleton2 instance;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
能起到延迟加载的效果。注意,由于volitale关键字,java1.5之前的版本不支持该写法。
枚举
public enum Singleton3 {
INSTANCE;
}
这种书写方式是《Effective Java》中最推荐的单例写法,一目了然,推荐。
静态的内部类
public class Singleton4 {
private Singleton4(){}
public static Singleton4 getInstance(){
return SingletonInstance.instance;
}
private static class SingletonInstance{
static Singleton4 instance = new Singleton4();
}
}
考虑反射的单例
其实这种情况比较少出现。但是,如果存在享有特权的客户端使用反射,调用私有构造器,就需要防范一下。
枚举天生能免疫这用情况。
双重校验锁能通过在默认构造器中加上如下代码防御反射:
synchronized (Singleton2.class) {
if (instance != null) {
//throw 一个异常
}
}
比较各种单例写法占用的字节数
最近使用java.lang.instrument.Instrumentation
,通过javaagent测试了单例的各实现的总大小(Class + Object)。
事实上,测完之后才惊觉这是件“然并卵”的操作,不过测都测了,就在这放出来以供参考吧。(下图忽略英文,4个数字[16,16,24,16]依次是上述4个单例模式占用的字节数)