首先我们要先了解下单例的四大原则:
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");