单例模式
单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
优点:
1、省略创建对象所花费的时间减少系统开销,尤其是重量级对象。
2、减少对象的创建,减轻GC压力。
3、设置全局访问入口,优化资源访问。
一、最简单的实现
方案1:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class SingletonClass { 2 private static final SingletonClass instance = new SingletonClass(); 3 4 private SingletonClass(){} 5 6 public static SingletonClass getInstance(){ 7 return instance; 8 } 9 10 }
将SingletonClass对象声明为final类型结合Java的类加载机制,保证了对象的唯一性。
二、延时加载
方案1乍看上去简单可行,但会出现两个问题:
问题1:可以通过反射再创建实例。
问题2:可以通过反序列化创建实例。
以上两个问题有点偏,先掩耳盗铃不考虑。
问题3:对象在类加载时就创建了不管使用与否,创建对象消耗小还好,消耗大的话就容易造成浪费。
对于问题2的解决方案是延时加载。
方案2:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class SingletonClass { 2 private static SingletonClass instance = null; 3 4 private SingletonClass(){} 5 6 public static SingletonClass getInstance(){ 7 if(null == instance){ 8 instance = new SingletonClass(); 9 } 10 11 return instance; 12 } 13 14 }
不在类加载时创建,等到需要使用的时候在进行创建。
三、线程安全
方案2 解决了问题3,但这样做又会引出新的问题4:同步。假如A、B 两个线程同时调用getInstance(),A 进行null == instance判断通过,然后instance对象进行创建且没有创建完或者 A 让出CPU时间片。在此时 B 执行到nulll == instance这里,也能通过判断。结果就是 A、B 拿到的是不同的对象。针对这种情况,加锁就好。
方案3:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class SingletonClass { 2 private static SingletonClass instance = null; 3 4 private SingletonClass(){} 5 6 public static SingletonClass getInstance(){ 7 synchronized (SingletonClass.class) { 8 if(null == instance){ 9 instance = new SingletonClass(); 10 } 11 } 12 13 return instance; 14 } 15 16 }
在每次 null==instance判断时加锁,保证同步。
四、性能优化
方案3解决了同步问题,但是每次调用getInstance()都加一次锁,性能有点低哦。那能不能在同步代码块外面再加一个非空判断呢?
方案4:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class SingletonClass { 2 private static SingletonClass instance = null; 3 4 private SingletonClass(){} 5 6 public static SingletonClass getInstance(){ 7 if(null == instance){ 8 synchronized (SingletonClass.class) { 9 System.out.println("进入锁"); 10 if(null == instance){ 11 instance = new SingletonClass(); 12 } 13 } 14 } 15 return instance; 16 } 17 18 public static void main(String[] args) { 19 for (int i = 0; i < 10000; i++) { 20 SingletonClass.getInstance(); 21 } 22 System.out.println("处理完成"); 23 } 24 25 }
运行得到的结果:
![](https://images2015.cnblogs.com/blog/775480/201705/775480-20170504173209632-1213426056.png)
编译后的代码:
![](https://images2015.cnblogs.com/blog/775480/201705/775480-20170504173210257-1319626352.png)
编译后的代码与代码逻辑一致,做两层非空判断,线程只在第一次进入了锁,后面都是直接返回instance对象。
方案4可行,但是总感觉怪怪的,还有没有其他写法呢?
答案是有,使用内部类,在调用时加载它。
方案5:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class SingletonClass { 2 3 private SingletonClass(){} 4 5 private static class innerSingletonClass{ 6 private static final SingletonClass instance = new SingletonClass(); 7 } 8 9 public static SingletonClass getInstance(){ 10 return innerSingletonClass.instance; 11 } 12 13 }
五、反序列化
上面方案4和方案5 已经能应付绝大多数的单例需求了,但是它们都是在没有考虑问题1、问题2 这两种比较极端的情况。先来看反序列化(序列化的相关知识自行搜索)。
方案6:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class SingletonClass implements Serializable{ 2 private static final long serialVersionUID = 1L; 3 4 private static SingletonClass instance = null; 5 6 private SingletonClass(){} 7 8 public static SingletonClass getInstance(){ 9 if(null == instance){ 10 synchronized (SingletonClass.class) { 11 if(null == instance){ 12 instance = new SingletonClass(); 13 } 14 } 15 } 16 return instance; 17 } 18 19 private Object readResolve(){//阻止生成新的实例,总是返回当前对象 20 return instance; 21 } 22 23 }
readResolve():如果类中定义了这个特殊序列化方法,在这个对象被序列化之后就会调用它。它必须返回一个对象,而该对象之后会成为 readObject 的返回值。
六、反射
通过反射调用构造方法来创建对象,要在代码层面防范这种行为,现阶段我还不知道,但是可以通过枚举来防止反射破坏单例。
方案7:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public enum SeasonEnum { 2 /** 3 * 在加载枚举类时初始化。 4 * 线程安全,与java的类加载机制相关。 5 * 全局唯一,反射不能更改。 6 * 反序列化后还是同一个对象 7 * */ 8 Spring(1); 9 10 private final int value;//可以像普通类一样定义Field,但是要定义在 枚举 后面,不然编译器会报错。 11 12 private SeasonEnum(int v) {//编译器只允许器定义成私有的。 13 value = v; 14 } 15 // private SeasonEnum() {//报错,不能重写无参构造方法 16 // 17 // } 18 19 public int getValue() { 20 return value; 21 } 22 23 public static void main(String[] args) throws Exception { 24 Class<SeasonEnum> cla = SeasonEnum.class; 25 Constructor<?>[] colls = cla.getDeclaredConstructors();//获得所有的构造器,拿到的是无参的创建枚举的构造方法。 26 System.out.println("获得的构造方法数量:" + colls.length);//获得的构造方法数量:1 27 for (Constructor<?> constructor : colls) { 28 System.out.println("构造方法名称:" + constructor.getName());//构造方法名称:testEnum.SeasonEnum 29 /** 30 * 运行报错 31 * Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects 32 * at java.lang.reflect.Constructor.newInstance(Constructor.java:402) 33 * */ 34 constructor.newInstance(0); 35 System.out.println(SeasonEnum.Spring); 36 } 37 38 } 39 }
枚举算是 现阶段单例的终极解决方案,简单线程安全,防反射,防反序列化。
推荐在代码中使用枚举来实现单例!
参考:
《Effective Java》【Joshua Bloch 】
《Java 程序性能优化》【葛一鸣等】