单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并 提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。单例模式结构图如下
单例模式通常有5种常见写法:
1. 饿汉模式
在类加载阶段就已经生成实例,在有线程之前就生成实例,绝对的线程安全。这种写法有缺点,导入该类的时候就生成实例,这样会浪费很多资源。
public class HungrySingleton {
private final static HungrySingleton single = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return single;
}
}
2. 懒汉模式
只有当外部类调用的时候才会加载,先看一种简单的懒汉模式,这种模式可以保证线程的安全,但是当有很多线程使用的话容易造成性能下降。
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){
if(singleton == null)
singleton = new LazySingleton();
return singleton;
}
}
3.双重检查锁
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton singleton = null;
private DoubleCheckSingleton(){}
public static DoubleCheckSingleton getInstance(){
if(singleton == null){
synchronized (DoubleCheckSingleton.class){
if(singleton == null){
DoubleCheckSingleton singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
为什么需要volatile修饰?
new 实例背后的指令
这个被忽略的问题在于 Cache cache=new Cache()
这行代码并不是一个原子指令。使用 javap -c
指令,可以快速查看字节码。
// 创建 Cache 对象实例,分配内存
0: new #5 // class com/query/Cache
// 复制栈顶地址,并再将其压入栈顶
3: dup
// 调用构造器方法,初始化 Cache 对象
4: invokespecial #6 // Method "<init>":()V
// 存入局部方法变量表
7: astore_1
从字节码可以看到创建一个对象实例,可以分为三步:
- 分配对象内存
- 调用构造器方法,执行初始化
- 将对象引用赋值给变量。
虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics。intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
上述的单例模式都存在一个问题,通过反射机制可以破坏单例模式,生成多个实例,如下代码。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Logger logger = Logger.getLogger("test");
Class clazz = SingleDoubleCheckLazy.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingleDoubleCheckLazy singleDoubleCheckLazy1 = (SingleDoubleCheckLazy)constructor.newInstance();
SingleDoubleCheckLazy singleDoubleCheckLazy2 = SingleDoubleCheckLazy.getInstance();
logger.info("singleDoubleCheckLazy1 == singleDoubleCheckLazy2 " + String.valueOf(singleDoubleCheckLazy1 == singleDoubleCheckLazy2));
}
//信息: singleDoubleCheckLazy1 == singleDoubleCheckLazy2 false
4. 静态内部类单例模式
不会被反射破坏掉的单例模式。静态内部类只有当用到该实例的时候才会加载,因此不会出现饿汉模式资源浪费的情况。
public class LazyInnerClassSingleton {
//默认使用 LazyInnerClassGeneral 的时候,会先初始化内部类 //如果没使用的话,内部类是不加载的
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
//每一个关键字都不是多余的
//static 是为了使单例的空间共享
//保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton getInstance() {
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
序列化会破坏单例模式,如下所示
public class SingleHungrySerializable implements Serializable {
private final static SingleHungrySerializable singleHungrySerializable = new SingleHungrySerializable();
private SingleHungrySerializable(){}
public static SingleHungrySerializable getInstance(){
return singleHungrySerializable;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
SingleHungrySerializable s1 = SingleHungrySerializable.getInstance();
SingleHungrySerializable s2 = null;
FileOutputStream fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s2 = (SingleHungrySerializable) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
}
运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加 readResolve()方法即可。来看优化代码
5. 枚举类单例(最好的单例模式)
枚举类的单例模式可以防止,反射,序列化的攻击,是最安全的单例模式
public class EnumSingleton implements Serializable {
static class Person implements Serializable{
@Override
public String toString() {
return "Person{" +
"a='" + a + '\'' +
", date=" + date +
", id=" + id +
'}';
}
private String a = null;
private Date date = null;
private int id = 0;
public Person(String a, Date date, int id){
this.a = a;
this.date = date;
this.id = id;
System.out.println("create the person");
}
}
public enum Singleton{
INSTANCE;
private Person instance = null;
private Singleton(){
instance = new Person("single person", new Date(), 1);
}
public Person getInstance(){
return instance;
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person s1 = Singleton.INSTANCE.getInstance();
Person s2 = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(s1);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
s2 = (Person)ois.readObject();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
}
//create the person
// Person{a='single person', date=Fri Nov 27 13:30:39 CST 2020, id=1}
//Person{a='single person', date=Fri Nov 27 13:30:39 CST 2020, id=1}
//false