【设计模式】单例模式
懒汉不安全
class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
多线程下由于指令重排序,可能导致instance分配了空间但还没完成初始化,另一个线程就插入了,此时instance已经不为null了,但实际上还没有完成初始化,此时return的可能一个null。
懒汉安全
class Singleton3 { private static Singleton3 instance; private Singleton3() { } public static synchronized Singleton3 getInstance() { if (instance == null) { instance = new Singleton3(); } return instance; } }
简单粗暴的synchronized,并发性比较低,不建议。
懒汉双重检查(重要)
class Singleton4 { private static volatile Singleton4 instance; private Singleton4() { } public static Singleton4 getInstance() { if (instance == null) {//第一次判断,如果不为null,可以省去synchronized的开销。 synchronized (Singleton4.class) { if (instance == null) {//第二次判断,排除多个线程进入创建多个实例的可能。 instance = new Singleton4(); } } } return instance; } }
如果不给instance变量加volatile关键字,那么第一次判断null时,instance变量可能已经完成了地址分配,但是还没有完成初始化,最终造成空指针异常。
对象创建有三条指令:分配内存空间、初始化对象、设置instance指向分配的内存空间。
这个问题是指令重排序造成的,可能导致还没有发生初始化,指针就完成了指向。因此必须加上volatile,禁止“初始化对象”和“设置instance指向分配的地址”两条指令的重排序。
对于两次instance的是否为空的判断解释:
为何在synchronization外面的判断?
为了提高性能!如果拿掉这次的判断那么在行的时候就会直接的运行synchronization,所以这会使每个getInstance()都会得到一个静态内部锁,这样的话锁的获得以及释放的开销(包括上下文切换,内存同步等)都不可避免,降低了效率。所以在synchronization前面再加一次判断是否为空,则会大大降低synchronization块的执行次数。
为何在synchronization内部还要执行一次呢?
因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
饿汉安全
class Singleton2 { private static Singleton2 instance = new Singleton2();//new对象由jvm的机制保证一定线程安全 private Singleton2() { } public static Singleton2 getInstance() { return instance; } }
由JVM保证天然的线程安全,但是还是不太好,因为我们还是希望懒加载。
静态内部类(重要)
class Singleton5 { private Singleton5() { } private static class SingletonHolder { private static final Singleton5 INSTANCE = new Singleton5(); } public static Singleton5 getInstance() { return SingletonHolder.INSTANCE; } }
推荐用法,由JVM保证天然的线程安全,并且实现了懒加载。
枚举(重要)
参考 https://blog.csdn.net/moakun/article/details/80688851
public enum Singleton { INSTANCE; private Resource instance; Singleton () { instance = new Resource(); } public Resource getInstance() { return instance; } }
Singleton.INSTANCE.getInstance()
即可获得所要实例。
使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?
其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。
定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。
通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。
而且,枚举中的各个枚举项同事通过static来定义的。如:
public enum T { SPRING,SUMMER,AUTUMN,WINTER; }
反编译后代码为:
public final class T extends Enum { //省略部分内容 public static final T SPRING; public static final T SUMMER; public static final T AUTUMN; public static final T WINTER; private static final T ENUM$VALUES[]; static { SPRING = new T("SPRING", 0); SUMMER = new T("SUMMER", 1); AUTUMN = new T("AUTUMN", 2); WINTER = new T("WINTER", 3); ENUM$VALUES = (new T[] { SPRING, SUMMER, AUTUMN, WINTER }); } }
了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。
也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。
枚举可避免反序列化破坏单例前面我们提到过,使用“双重校验锁”实现的单例其实是存在一定问题的,就是这种单例有可能被序列化所破坏,关于这种破坏及解决办法,参看单例与序列化的那些事儿,这里不做更加详细的说明了。
以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。
大概意思就是:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
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 const " + enumType +"." + name); }
从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。
所以,JVM对序列化有保证。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。这部分内容在《深度分析Java的枚举类型—-枚举的线程安全性及序列化问题》中也有更加详细的介绍,还展示了部分代码,感兴趣的朋友可以前往阅读。