单例是用来创建对象的,但是这个类智能产生同一个对象。因此,并不能用我们平时所说的new来进行产生新对象。而是在这个类中设置getInstance方法,new的过程交给这个类。外部不允许使用new创建对象,因此单例模式的一个核心就是构造函数要是private的,想要获取新的对象,必须通过getInstance方法。
单例模式的第二个核心就是线程安全,也就是如何只产生一个对象。仅仅在getInstance方法中判断对象是不是null并不能解决问题,因为可能有很多个线程同时进行了判断,然后都为空,然后获得的对象不是一个对象。因此线程安全也是单例模式的重中之重。单例有5中实现的方法,各有什么优缺点会在代码中的注释中写出,主要从3个方面进行考虑:线程安全、懒加载(延迟加载)、调用效率。除此之外,也会稍微提一下如何防止反射和反序列化来进行预防单例模式的漏洞。单例的5中实现方法:懒汉式、饿汉式、双重检查锁、内部类、枚举。现在直接看代码吧:
1 /** 2 * 双重检查锁,避免同步问题。延时加载,调用效率高,但是由于JVM的原因,使用可能存在问题。 3 * 但是这个方法由于jvm优化,可能存在一些问题 4 * @author WXY 5 * 6 */ 7 public class Singleton { 8 //volatile保证所有更改内存 9 private volatile static Singleton uniqueInstance; 10 11 private Singleton() { 12 } 13 //要设置成静态,因为这个是单例,不能通过new获得对象 14 public static Singleton getInstance() { 15 if(uniqueInstance == null){//第一次检查 16 synchronized(Singleton.class){//同步方法,保证只有一个进程进入 17 if(uniqueInstance == null){//第二次检查 18 uniqueInstance = new Singleton();//初始化 19 } 20 } 21 } 22 return uniqueInstance; 23 } 24 } 25 26 /** 27 * 饿汉模式:(延时加载,因为Synchronized修饰,调用效率低) 28 * 最简单的多线程单例模式,一个同步块解决问题 29 * @author WXY 30 * 31 */ 32 class Singleton1 implements Serializable{//继承了Serializable才能序列化,java中腰序列化必须实现这个接口 33 private static Singleton1 uniqueSingleton; 34 35 private Singleton1(){//防止反射造成的单例破坏(若是构造函数为空,则不能防止反射破坏) 36 if(uniqueSingleton != null){ 37 throw new RuntimeException(); 38 } 39 } 40 41 //反序列化时候,如果定义了readResolve方法则之间调用这个方法,而不是创建新对象 42 private Object readResolve() throws ObjectStreamException{ 43 return uniqueSingleton; 44 } 45 46 public static synchronized Singleton1 getInstance(){ 47 if(uniqueSingleton == null){ 48 uniqueSingleton = new Singleton1(); 49 } 50 return uniqueSingleton; 51 } 52 } 53 54 /** 55 * 懒汉模式:类初始化的时候立即加载这个对象,没有延时加载的优势,方法不同步,调用效率高 56 * (无延时加载优势,调用效率高) 57 * 类加载的过程中就new一个对象,直接获取 58 * @author WXY 59 * 60 */ 61 class Singleton2{ 62 private static Singleton2 uniqueSingleton = new Singleton2(); 63 private Singleton2(){} 64 65 public static Singleton2 getInstance(){ 66 return uniqueSingleton; 67 } 68 } 69 70 /** 71 * 采用内部类的方式,较好 72 * 满足:1、多线程同步问题,虚拟机加载内部类保证,静态的只初始化一次 73 * 2、性能比较高(不需要同步) 74 * 3、延时加载 75 * @author WXY 76 * 77 */ 78 class Singleton3{ 79 private static class SingletonInside{ 80 private static Singleton3 uniqueSingleton = new Singleton3(); 81 } 82 private Singleton3(){} 83 public static Singleton3 getInstance(){ 84 return SingletonInside.uniqueSingleton; 85 } 86 } 87 88 /** 89 * 利用枚举创建单例,简单,而且方式反射创建新对象,上面的方法不能防止反射创建对象 90 * 防止了反射和序列化的漏洞,但是没有懒加载(延时加载) 91 * @author WXY 92 * 93 */ 94 enum Singleton4{ 95 INSTANCE; 96 public void method(){ 97 98 } 99 }
懒汉式是类初始化的时候就new出了新对象,不需要使用Synchronized关键字,因此调用效率极高,线程安全,但是没有懒加载(延迟加载,也就是需要的时候才加载,类初始化的时候才加载会浪费资源)。
饿汉式是需要的时候才加载,实现了延迟加载,但是Synchronized关键字修饰(为了安全性)使得调用效率低,很多线程阻塞在这里。
双重锁检查需要了解一下volatile关键字的含义(1、保证线程修改的内容强制写入到主存,并设置高速缓存中的数据失效。2、强制不进行代码指令的顺序优化,与JVM优化相关。详细的可以去查阅相关资料,这里不多说了,这里主要用到作用1)。既实现了懒加载有实现了不适用Synchronized同步,貌似很好,但是这个方法在JVM优化的时候可能存在异常,因此不建议适用。
内部类利用static的变量只创建一次的天然特性,实现了延迟加载和调用高调用性能。
枚举是一个简单好用的办法,但是没有实现懒加载。
以上就是简单的分析,具体请各位小伙伴自己动手实验,可以开很多线程同时调用获取对象的getInstance方法,观察一下时间。这里不上代码了。
五种方法中,除了枚举,其他几种方式都有反射和反序列化的漏洞。虽然构造方法时private的,但是,反射可以获取private方法的使用,因此不安全,所以在构造方法中加入判断可以解决反射漏洞。反序列化时,会直接调用readResolve方法,不调用构造方法,也解决了漏洞。对于这两个问题的考虑,如果再自己的项目中完全没有必要考虑,如果是写API等,必须需要考虑这么细。
好了,就这些。写下文章作为回忆复习使用,如果能帮到其他小伙伴当然更好。