为什么80%的码农都做不了架构师?>>>
单例模式作为设计模式中最简单的一种,是一个被说烂了的东西。但是在项目中还是会发现关于单例模式的一些错误实现,可见单例也并不是我们想象的那么简单。最近陆陆续续看了几篇关于单例的博客,很受启发,所以觉得有必要总结一下(只涉及常用的双重检查锁定、静态内部类、枚举三种单例实现方法),本文将从安全和性能两个方面阐述我对单例模式三种最佳实践的理解。不当之处,请 指正。
1、双重校验锁定
//Double Checked locking
public class SingletonByDCL
{private Map<Integer, String> configMap;private volatile static SingletonByDCL instance = null;private SingletonByDCL(Map<Integer, String> configMap){this.configMap = configMap;}public static SingletonByDCL getInstance(){SingletonByDCL inst = instance;if (null == inst){synchronized (SingletonByDCL.class){inst = instance;if (null == inst){inst = new SingletonByDCL(ConfigReader.configMap);instance = inst;}}}return inst;}
}
通过synchronized和volatile实现了线程安全。其中需要注意的是实例变量一定要用volatile修饰。原因可参考Java 单例真的写对了么?。当单例对象需要被序列化时,就应该考虑单例实现的序列化安全,在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以方式单例被破坏,原理可参考单例与序列化的那些事儿。可能会有人使用反射强行调用我们的私有构造器,为了保证访问安全,可以修改构造器,让它在创建第二个实例的时候抛异常。
2、静态内部类
public class SingletonByInnerStaticClass
{private Map<Integer, String> configMap;private SingletonByInnerStaticClass(Map<Integer, String> configMap){this.configMap = configMap;}private static class SingletonHolder{private static SingletonByInnerStaticClass instance = new SingletonByInnerStaticClass(ConfigReader.configMap);}public static SingletonByInnerStaticClass getInstance(){return SingletonHolder.instance;}
}
线程安全,这是 Java 运行环境自动给保证的,在加载的时候,会自动隐形的同步。在访问对象的时候,不需要同步 Java 虚拟机又会自动取消同步。对于序列化安全和访问安全的保证,解决方法同“双重检查锁定”。
3、枚举
public enum SingletonByEnum
{instanse(ConfigReader.configMap);private Map<Integer,String> configMap;private SingletonByEnum(Map<Integer,String> configMap){this.configMap = configMap;}public Map<Integer, String> getConfigMap(){return configMap;}
}
当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型(可用javap查看enum编译后的class文件,从而了解enum包含哪些静态资源)是线程安全的。为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定,从而保证序列化安全。
private static void testSingletonByEnum() throws IOException, FileNotFoundException, ClassNotFoundException{// 序列化ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempfile"));oos.writeObject(SingletonByEnum.instanse);// 反序列化ObjectInputStream ois = new ObjectInputStream(new FileInputStream("tempfile"));SingletonByEnum s = (SingletonByEnum) ois.readObject();// 判断是否为同一对象if (s == SingletonByEnum.instanse){System.out.println("创建的是同一个实例");} else{System.out.println("创建的不是同一个实例");}}
创建的是同一个实例
同时java.lang.reflect.Constructor的newInstance()方法中有如下代码,禁止了通过反射构造枚举对象,所以枚举可以保证访问安全。关于枚举如何保证线程安全和序列化安全,可参考深度分析 Java 的枚举类型:枚举的线程安全性及序列化问题
if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
4、三种方法的性能比较
三种方法都实现了延迟加载,在8线程同时调用,每个线程调用100000000次的情况下时间对比如下:
SingletonByDCL—》845
SingletonByEnum—》90
SingletonByInnerStaticClass—》89
具体测试代码参见附件工程中的com.zjg.perf.PerformanceTest2
综上所述,Effective Java中推荐使用的枚举实现单例无论从安全还是性能都是有道理的。当然代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不同的平台、不同的开发环境(尤其是jdk版本)下,也就会有不同的最优解。比如枚举在Android平台上却是不被推荐的。在这篇Android Training中明确指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再比如双重检查锁法,不能在jdk1.5之前使用,而在Android平台上使用就比较放心了(一般Android都是jdk1.6以上了,不仅修正了volatile的语义问题,还加入了不少锁优化,使得多线程同步的开销降低不少)。
参考文章:
http://blog.jobbole.com/94074/ 深度分析 Java 的枚举类型:枚举的线程安全性及序列化问题
http://www.importnew.com/18872.html 你真的会写单例模式吗——Java实现
http://www.hollischuang.com/archives/1144 单例与序列化的那些事儿
http://www.race604.com/java-double-checked-singleton/?utm_source=tuicool&utm_medium=referral Java 单例真的写对了么?
demo工程:https://git.oschina.net/zjg23/SingletonDemo