单例模式的那些事

文章目录

  • 单例模式
  • 单例应用场景:
  • 单例的特点
  • 单例模式的7种写法
    • 第一种(懒汉,线程不安全):
    • 第二种(懒汉,线程安全):
    • 第三种(饿汉,在类刚开始被加载的时候就创建出一个对象):
    • 第四种(饿汉,变种):
    • 第五种(静态内部类):
    • 第六种(枚举):
    • 第七种(双重校验锁):
  • 单例模式真的能够实现实例的唯一性吗?答案是否定的!
  • 如何破坏单例
    • 反射
    • 序列化和反序列化
  • 单元素枚举类型
  • 总结
  • 实现方法选择:

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

但是,他却有很多的东西需要注意,性能、线程安全等。

单例应用场景:

windows的任务管理器,回收站,web应用的配置对象,Spring中的bean默认单例

单例的特点

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例

单例模式的7种写法

第一种(懒汉,线程不安全):

类初始化的时候并不创建,什么时候想用再创建

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  

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

这种写法lazy loading(只在需要的时候才去加载必要的数据,这样可以避免即时加载所带来的不必要的系统开销)很明显,但是致命的是在多线程不能正常工作。

第二种(懒汉,线程安全):

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

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading(只在需要的时候才去加载必要的数据,这样可以避免即时加载所带来的不必要的系统开销),但是,遗憾的是,效率很低,99%情况下不需要同步。

第三种(饿汉,在类刚开始被加载的时候就创建出一个对象):

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

这种方式基于classloder机制,在深度分析Java的ClassLoader机制(源码级别)和Java类的加载、链接和初始化两个文章中有关于CLassload而机制的线程安全问题的介绍,避免了多线程的同步(classLoader在加载类的时候是同步的)问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。

第四种(饿汉,变种):

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

表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance

第五种(静态内部类):

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}  

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理

第六种(枚举):

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中有详细介绍枚举的线程安全问题和序列化问题,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。

第七种(双重校验锁):

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
	    if (singleton == null) {  
	        synchronized (Singleton.class) {  
		        if (singleton == null) {  
		            singleton = new Singleton();  
		        }  
	       	}  
	    }  
    	return singleton;  
    }  
}  

以上就是单例模式的七种实现方法,有两个问题需要注意:

  1. 如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
  2. 如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。

对第一个问题修复的办法是:

private static Class getClass(String classname)  
throws ClassNotFoundException {  
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    if(classLoader == null)     
          classLoader = Singleton.class.getClassLoader();     
          return (classLoader.loadClass(classname));     
       }     
    }  

对第二个问题修复的办法是:

public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     
   protected Singleton() {     
   }     
   private Object readResolve() {     
   		return INSTANCE;     
   }    
}   

对我来说,我比较喜欢第三种和第五种方式,简单易懂,而且在JVM层实现了线程安全(如果不是多个类加载器环境),一般的情况下,我会使用第三种方式,只有在要明确实现lazy loading效果时才会使用第五种方式,另外,如果涉及到反序列化创建对象时我会试着使用枚举的方式来实现单例,不过,我一直会保证我的程序是线程安全的,而且我永远不会使用第一种和第二种方式,如果有其他特殊的需求,我可能会使用第七种方式,毕竟,JDK1.5已经没有双重检查锁定的问题了。

不过一般来说,第一种不算单例,第四种和第三种就是一种,如果算的话,第五种也可以分开写了。所以说,一般单例都是五种写法。懒汉,饿汉,双重校验锁,枚举和静态内部类。

单例模式真的能够实现实例的唯一性吗?答案是否定的!

如何破坏单例

反射

有两种常见的方式来实现单例。他们的做法都是将构造方法设为私有,并导出一个公有的静态成员来提供对唯一实例的访问。在第1种方式中,成员是个final字段:

// Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}

只调用私有构造函数一次,以初始化公共静态final字段elvi.instance。不提供公有的或者受保护的构造函数保证了全局唯一性:当Elvis类初始化的时候,仅仅只会有一个Elvis实例存在——不多也不少 。无论客户端怎么做都无法改变这一点,只不过我还是要警告一下 :授权的客户端可以通过反射来调用私有构造方法,借助于AccessibleObject.setAccessible方法即可做到 。如果需要防范这种攻击,请修改构造函数,使其在被要求创建第二个实例时抛出异常。

测试代码:

public class TestSingleton {
 
    /**
     * 通过反射破坏单例
     */
    @Test
    public void testReflection() throws Exception {
        /**
         * 验证单例有效性
         */
        Elvis elvis1 = Elvis.INSTANCE;
        Elvis elvis2 = Elvis.INSTANCE;
 
        System.out.println("elvis1 == elvis2 ? ===>" + (elvis1 == elvis2));
        System.err.println("-----------------");
 
        /**
         * 反射调用构造方法
         */
        Class clazz = Elvis.class;
        Constructor cons = clazz.getDeclaredConstructor(null); 
        cons.setAccessible(true);
 
        Elvis elvis3 = (Elvis) cons.newInstance(null);
 
        System.out.println("elvis1 == elvis3 ? ===> "
            + (elvis1 == elvis3));
    }
}

运行结果:

Elvis Constructor is invoked!
elvis1 == elvis2 ? ===> true
elvis1 == elvis3 ? ===> false
-----------------
Elvis Constructor is invoked!

结论:

反射是可以破坏单例属性的。因为我们通过反射把它的构造函数设成可访问的,然后去生成一个新的对象。

改进版的单例写法:

第一种实现单例模式的方法:

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
 
    private Elvis() { 
        System.err.println("Elvis Constructor is invoked!");
        if (INSTANCE != null) {
            System.err.println("实例已存在,无法初始化!");
            throw new UnsupportedOperationException("实例已存在,无法初始化!");
        }
    }
 
}

结果:

Elvis Constructor is invoked!
elvis1 == elvis2 ? ===> true
-----------------
Elvis Constructor is invoked!
实例已存在,无法初始化!

第二种实现单例模式的方法是,提供一个公有的静态工厂方法:

// Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }
    public void leaveTheBuilding() { ... }
}

所有调用Elvis类的getInstance方法,返回相同的对象引用,并且不会有其它的Elvis对象被创建。但同样有上面第1个方法提到的反射破坏单例属性的问题存在

序列化和反序列化

如果对上述2种方式实现的单例类进行序列化,反序列化得到的对象是否是同一个对象呢?答案是否定的。
看下面的测试代码:
单例类:

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { 
        System.err.println("Elvis Constructor is invoked!");
    }
}

测试代码:

/**
 * 序列化对单例属性的影响
 * @throws Exception 
 */
@Test
public void testSerialization() throws Exception {
    Elvis elvis1 = Elvis.INSTANCE;
    FileOutputStream fos = new FileOutputStream("a.txt");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(elvis1);
    oos.flush();
    oos.close();
 
    Elvis elvis2 = null;
    FileInputStream fis = new FileInputStream("a.txt");
    ObjectInputStream ois = new ObjectInputStream(fis);
    elvis2 = (Elvis) ois.readObject();
 
    System.out.println("elvis1 == elvis2 ? ===>" + (elvis1 == elvis2));
}

结果是:

Elvis Constructor is invoked! 
elvis1 == elvis2 ? ===>false

说明:

通过对序列化后的Elvis 进行反序列化得到的对象是一个新的对象,这就破坏了Elvis 的单例性。

《Effective Java》已经告诉我们,在单例类中提供一个readResolve方法就可以完成单例特性。这里大家可以自己去测试。

首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源
深入理解单例模式(下)
2018/08/01 | 分类: 基础技术 | 1 条评论 | 标签: 单例, 设计模式

分享到:
原文出处: 拿笔小星_
《Effective Java》已经告诉我们,在单例类中提供一个readResolve方法就可以完成单例特性。这里大家可以自己去测试。

接下来,我们去看看Java提供的反序列化是如何创建对象的!

ObjectInputStream
对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream的readObject 方法执行情况到底是怎样的。

为了节省篇幅,这里给出ObjectInputStream的readObject的调用栈:

单例模式的那些事_第1张图片
大家顺着此图的关系,去看readObject方法的实现。
首先进入readObject0方法里,关键代码如下:

switch (tc) {
    //省略部分代码
 
    case TC_STRING:
    case TC_LONGSTRING:
        return checkResolve(readString(unshared));
 
    case TC_ARRAY:
        return checkResolve(readArray(unshared));
 
    case TC_ENUM:
        return checkResolve(readEnum(unshared));
 
    case TC_OBJECT:
        return checkResolve(readOrdinaryObject(unshared));
 
    case TC_EXCEPTION:
        IOException ex = readFatalException();
        throw new WriteAbortedException("writing aborted", ex);
 
    case TC_BLOCKDATA:
    case TC_BLOCKDATALONG:
        if (oldMode) {
            bin.setBlockDataMode(true);
            bin.peek();             // force header read
            throw new OptionalDataException(
                bin.currentBlockRemaining());
        } else {
            throw new StreamCorruptedException(
                "unexpected block data");
        }
 
    //省略部分代码

这里就是判断目标对象的类型,不同类型执行不同的动作。我们的是个普通的Object对象,自然就是进入case TC_OBJECT的代码块中。然后进入readOrdinaryObject方法中。
readOrdinaryObject方法的代码片段:

private Object readOrdinaryObject(boolean unshared)
        throws IOException {
    //此处省略部分代码
 
    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }
 
    //此处省略部分代码
 
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }
 
    return obj;
}

重点看代码块

Object obj;
try {
     obj = desc.isInstantiable() ? desc.newInstance() : null;
 } catch (Exception ex) {
     throw (IOException) new InvalidClassException(
         desc.forClass().getName(),
         "unable to create instance").initCause(ex);
 }

这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。

isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。
desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。

所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?即序列化会通过反射调用无参数的构造方法创建一个新的对象。

接下来再看,为什么在单例类中定义readResolve就可以解决该问题呢?还是在readOrdinaryObjec方法里继续往下看。

if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
     if (unshared && rep.getClass().isArray()) {
         rep = cloneArray(rep);
     }
     if (rep != obj) {
         handles.setObject(passHandle, obj = rep);
     }
}

这段代码也很清楚地给出答案了!
如果目标类有readResolve(desc.hasReadResolveMethod())方法,那就通过反射的方式调用要被反序列化的类的readResolve方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)。那readResolve 方法里是什么?就是直接返回我们的单例对象。

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();
 
    private Elvis() { 
        System.err.println("Elvis Constructor is invoked!");
    }
 
    private Object readResolve() {
       return INSTANCE;
    }
}

所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

单元素枚举类型

第三种实现单例的方式是,声明一个单元素的枚举类:

// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

这个方法跟提供公有的字段方法很类似,但它更简洁,提供天然的可序列化机制和能够强有力地保证不会出现多次实例化的情况 ,甚至面对复杂的序列化和反射的攻击下。这种方法可能看起来不太自然,但是拥有单元素的枚举类型可能是实现单例模式的最佳实践。注意,如果单例必须要继承一个父类而非枚举的情况下是无法使用该方式的(不过可以声明一个实现了接口的枚举)。
我们分析一下,枚举类型是如何阻止反射来创建实例的?直接源码:
看Constructor类的newInstance方法。

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

这行代码(clazz.getModifiers() & Modifier.ENUM) != 0 就是用来判断目标类是不是枚举类型,如果是抛出异常IllegalArgumentException(“Cannot reflectively create enum objects”),无法通过反射创建枚举对象!很显然,反射无效了。

接下来,再看一下反序列化是如何预防的。依然按照上面说的顺序去找到枚举类型对应的readEnum方法,如下:

private Enum<?> readEnum(boolean unshared) throws IOException {
    if (bin.readByte() != TC_ENUM) {
        throw new InternalError();
    }
 
    ObjectStreamClass desc = readClassDesc(false);
    if (!desc.isEnum()) {
        throw new InvalidClassException("non-enum class: " + desc);
    }
 
    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(enumHandle, resolveEx);
    }
 
    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }
 
    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

readString(false):首先获取到枚举对象的名称name。
Enum en = Enum.valueOf((Class)cl, name):再指定名称的指定枚举类型获得枚举常量,由于枚举中的name是唯一,切对应一个枚举常量。所以我们获取到了唯一的常量对象。这样就没有创建新的对象,维护了单例属性。

看看Enum.valueOf 的JavaDoc文档:

返回具有指定名称的指定枚举类型的枚举常量。
该名称必须与用于声明此类型中的枚举常量的标识符完全匹配。 (不允许使用无关的空白字符。)

具体实现:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

enumConstantDirectory():返回一个Map,维护着名称到枚举常量的映射。我们就是从这个Map里获取已经声明的枚举常量,通过这个缓存池一样的组件,让我们可以重用这个枚举常量!

总结

  1. 常见的单例写法有他的弊端,存在安全性问题,如:反射,序列化的影响。
  2. 《Effective Java》作者Josh Bloch 提倡使用单元素枚举类型的方式来实现单例,首先创建一个枚举很简单,其次枚举常量是线程安全的,最后有天然的可序列化机制和防反射的机制。

实现方法选择:

一般情况下直接使用饿汉式就好了,要求延迟加载时倾向于用静态内部类,涉及到反序列化创建对象或反射问题最好选择枚举

你可能感兴趣的:(设计模式,学习笔记,深入Java)