深入理解单例模式,单例模式实例

244 阅读4分钟

首先我们要先了解下单例的四大原则:

1.构造私有。
2.以静态方法或者枚举返回实例。
3.确保实例只有一个,尤其是多线程环境。
4.确保反序列换时不会重新构建对象。

双重锁懒汉模式(Double Check Lock)

public class SingleInstance {

    private volatile SingleInstance singleInstance;

    private Object obj;

    private SingleInstance(){
    }
    public SingleInstance getInstance(){
        if (singleInstance == null) {
            synchronized (obj) {
                if (singleInstance == null) {
                    singleInstance = new SingleInstance();
                    return singleInstance;
                }
            }
        }
        return singleInstance;
    }
}

这个步骤,其实在jvm里面的执行分为三步:

1.在堆内存开辟内存空间。 2.在堆内存中实例化SingleTon里面的各个参数。 3.把对象指向堆内存空间。

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。

不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,即在JDK1.6及以后,只要定义为private volatile static SingleTon  INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

静态内部类

public class SingleInstanceNew { private SingleInstanceNew(){ }

static class  SubSingleInstanceNew {
    public static SingleInstanceNew instanceNew = new SingleInstanceNew();
}

public SingleInstanceNew  getNewSingleInstance(){
    return SubSingleInstanceNew.instanceNew;
}

}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。 即当SingleInstanceNew第一次被加载时,并不需要去加载SubSingleInstanceNew,getNewSingleInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getNewSingleInstance()方法会导致虚拟机加载SubSingleInstanceNew类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

枚举实现单例

public class User {
    //私有化构造函数
    private User(){ }
 
    //定义一个静态枚举类
    static enum SingletonEnum{
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private User user;
        //私有化枚举的构造函数
        private SingletonEnum(){
            user=new User();
        }
        public User getInstnce(){
            return user;
        }
    }
 
    //对外暴露一个获取User对象的静态方法
    public static User getInstance(){
        return SingletonEnum.INSTANCE.getInstnce();
    }

    public static void main(String[] args) {
        User user1 = User.getInstance();
        User user2 = User.getInstance();
        if (user1 == user2) {
            System.out.println("it is the same object");
        }
        System.out.println(user1.hashCode());
        System.out.println(user2.hashCode());
    }
}

我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。 所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

为什么反序列化枚举类型也不会创建新的实例?

枚举类型在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。(使用双重校验锁实现的单例其实是存在一定问题的,就是这种单例有可能被序列化锁破坏)

普通类的反序列化是通过反射实现的,枚举类的反序列化不是通过反射实现的。所以,枚举类也就不会发生由于反序列化导致的单例破坏问题。

枚举不可以反射
Constructor类的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;
    }

其中可以看出不可以通过反射来创建枚举对象。

   if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");