设计模式 - 由浅入深分析单例模式

由浅入深分析单例模式

单例模式(Singleton Patten):确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例(Ensure a class has only one instance, and provide a global point of access to it)。程序员

Singleton类称为单例类,经过使用private的构造函数确保了在一个应用中只产生一个实例,而且是自行实例化的(在Singleton中本身使用new Singleton())。安全

1.单例入门

饿汉式:在类加载时就建立对象实例,而无论实际是否须要建立。多线程

public class Singleton {

    private static final Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

懒汉式:只有调用getInstance的时候,才实例化对象。ide

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

问题:函数

  • 饿汉式没能作到延迟加载(lazy loading)。所谓延迟加载就是当真正须要数据的时候,才执行数据加载操做,为了不一些无谓的性能开销。但饿汉式的好处是线程安全。
  • 上文中的懒汉式单例在多线程环境下可能会有多个进程同时经过(singleton == null)的条件检查。这样就建立出了多个实例,而且极可能形成内存泄露。

2.多线程进阶

方案1:源码分析

在getInstance()方法上加synchronized关键字。性能

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

问题:只有在单例初始化的时候咱们才须要保证线程安全,其余时候方法上的synchronized关键字只会下降性能。测试

方案2:flex

只在单例初始化的时候加synchronized关键字。优化

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

问题:仍是有可能会有多个进程同时经过(singleton == null)的条件检查,进而建立多个实例。

方案3:

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

问题:和方案1相似,原本只是想让new这个操做并行,如今只要是进入getInstance()的线程都得同步,影响性能。

方案4:

双重检查加锁(Double-Check Lock)。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                //若是被同步的线程中,有一个线程建立了对象,那么别的线程就不用再建立了
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

问题:DCL失效。

DCL失效主要在于singleton = new Singleton()这句,这并不是是一个原子操做,事实上在JVM中这句话大概作了下面 3 件事情:

  • 给 singleton 分配内存。
  • 调用 Singleton 的构造函数来初始化成员变量,造成实例。
  • 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)。

可是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序多是 1-2-3 也多是 1-3-2。若是是后者,则在3执行完毕、2未执行以前,若是另外一个线程抢占了锁,这时 instance 已是非 null 了(但却没有初始化),因此该线程会直接返回 instance,而后使用,而后瓜熟蒂落地报错。

方案5(最终版):

加volatile。

public class Singleton {

    private volatile static Singleton singleton = null;

    private Singleton() {
    }

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

使用 volatile 有两个做用:

  • 这个变量不会在多个线程中保存复本,而是直接从内存读取。

  • 这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操做后面会有一个内存屏障(生成的汇编代码上),读操做不会被重排序到内存屏障以前。

3.还能更好吗

老版《Effective Java》中推荐的方式:

public class Singleton {

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
    }

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

上面这种方式,仍然使用JVM自己的机制保证了线程安全问题:

  • 因为 SingletonHolder 是私有的,除了 getInstance() 以外没有办法访问它,所以它只有在getInstance()被调用时才会真正建立。
  • 同时读取实例的时候不会进行同步,没有性能缺陷。
  • 不依赖 JDK 版本。

枚举实现:

public enum Singleton {

    INSTANCE;

    public void doSomething() {
    }
}

默认枚举实例的建立是线程安全的,因此不须要担忧线程安全的问题。可是在枚举中的其余方法的线程安全由程序员本身负责。还有防止上面的经过反射机制调用私用构造器。

这个版本基本上消除了绝大多数的问题,代码也很是简单,是新版的《Effective Java》中推荐的模式。

4.其余问题

序列化攻击:

单例实现采用方案5,序列化攻击代码以下:

public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //序列化方式破坏单例   测试
        serializeDestroyMethod();
    }

    private static void serializeDestroyMethod() throws IOException, ClassNotFoundException {
        Singleton singleton;
        Singleton singletonNew;

        singleton = Singleton.getInstance();

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(singleton);

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        singletonNew = (Singleton) ois.readObject();

        System.out.println(singleton == singletonNew);
    }
}

打印结果为:false。在单例类中添加一个方法 readResolve():

public class Singleton implements Serializable {
    private volatile static Singleton singleton = null;

    private Singleton() {
    }

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

    private Object readResolve() {
        return singleton;
    }
}

再执行攻击代码,打印结果为:true。

反序列化攻击源码分析:

    //默认状况下 该方法经过反射建立一个新对象并返回
    private Object readOrdinaryObject(boolean unshared)
            throws IOException {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        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);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        //通过上面的代码,新对象已经被new出来了
        //下面hasReadResolveMethod()这个方法很关键。若是该类存在readResolve()方法,就调用该方法返回的实例替换掉新建立的对象。若是不存在就直接把new出来的对象返回出去。
        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) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

...
/** * 返回该类是否有readResolve方法 */ boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }

因此在单例类中添加方法 readResolve(),就能够防范反序列化攻击。

反射攻击:

单例实现依然采用方案5,反射攻击代码以下:

public class Main {

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        reflexDestroyMethod();
    }

    private static void reflexDestroyMethod() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = Singleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        Singleton singleton = Singleton.getInstance();
        Singleton singletonNew = (Singleton) constructor.newInstance();

        System.out.println(singleton == singletonNew);
    }
}

打印结果为:false。

使用枚举实现单例后,执行反射攻击报错以下:

缘由是Singleton.class.getDeclaredConstructors()获取全部构造器,会发现并无咱们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器。看下Enum源码就明白,这两个参数是name和ordinal两个属性:

public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
        private final String name;
        public final String name() {
            return name;
        }
        private final int ordinal;
        public final int ordinal() {
            return ordinal;
        }
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
        //余下省略

枚举Enum是个抽象类,一旦一个类声明为枚举,实际上就是继承了Enum,因此就会有(String.class,int.class)的构造器。既然无参构造方法找不到,那咱们就使用父类Enum的构造器,看看是什么状况:

public class Main {

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        //反射方式破坏单例 测试
        reflexDestroyMethod();
    }

    private static void reflexDestroyMethod() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = SingletonE.class;
        Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);

        SingletonE singleton = SingletonE.INSTANCE;
        SingletonE singletonNew = (SingletonE) constructor.newInstance("test", 1);

        System.out.println(singleton == singletonNew);
    }
}

执行结果以下:

说是不能反射建立枚举对象,newInstance()方法源码以下:

    @CallerSensitive
    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;
    }

由上文源码可知,反射在经过newInstance建立对象时,会检查该类是否ENUM修饰,若是是则抛出异常,反射失败。

总结:

单元素的枚举类型已经成为实现Singleton的最佳方法 —— 《Effective Java》

相关文章
相关标签/搜索