本系列,记录了我深入学习设计模式的过程。也算是JAVA进阶学习的一个重要知识点吧。java
与设计相关的代码会贴出,可是基础功能的代码会快速带过。有任何错误的地方,都欢迎读者评论指正,感谢。冲冲冲!设计模式
只须要一个实例存在的场景安全
好比各类Manager 类学习
好比各类Factory 类优化
总得来讲,一共有8种单例的实现方式。其中只有2种是完美解决单例问题的。this
其余6种都有他的缺点。下面会经过先讲述6个实现并解决他们的缺点的方式,一个个来说实现。spa
先来看看它的实现.net
1 public class Mrg01 { 2 // static类型保证在classLoader装载这个类时,就实例化了这个对象 3 private static final Mgr01 INSTANCE = new Mgr01(); 4 /** 5 //这是第二种写法 6 //这样的写法和上面单句的写法是一个意思,只是初始化写在静态语句块里 7 private static final Mgr01 INSTANCE = null; 8 static { 9 this.INSTANCE = new Mgr01(); 10 } 11 **/ 12 13 // 私有构造方法避免其余类new出该对象 14 private Mgr01() {} 15 // 想要拿到这个类的实例,就得用该类静态公共方法获取 16 public static Mgr01 getInstance() {return INSTANCE;} 17 // 这个类的任意方法 18 public void m() {System.out.println("do method");} 19 }
优势:线程
很简单清晰的实现,在平常的开发中也推荐这种方式来实现单例模式,由于这种方式简单实用。设计
这种实现是线程安全的,由于JVM只会初始化一次INSTANCE对象。
缺点:
惟一的缺点就是无论这个类是否被用到,它都会被完成实例化。
懒汉式相比上面的饿汉式,它主要是弥补饿汉式的缺点,因而就有下面代码
1 public class Mgr03 { 2 private static Mgr03 INSTANCE; 3 private Mgr03() {} 4 // 当调用该静态方法时,根据对象是否为空,决定是否new一个对象出来,最后返回该对象 5 public static Mgr03 getInstance() { 6 if (INSTANCE == null) { 7 INSTANCE = new Mgr03(); 8 } 9 return INSTANCE; 10 } 11 12 public void m() {System.out.println("do method");} 13 }
看似这样的实现解决了饿汉式的提早初始化问题,可是却带来了线程不安全问题。
缘由很简单,当首个线程Thread1进入静态方法时,此时INSTANCE==null,程序会执行new Mgr03(),可是此时线程Thread2也进入静态方法,但Thread1还未完成INSTANCE初始化,那么Thread2也会去new Mgr03()。这样就在首次使用时没法保证该类的单例存在。
为了解决懒汉式的线程不安全问题,咱们尝试用synchronized解决它
1 public class Mgr04 { 2 private static Mgr04 INSTANCE; 3 private Mgr04() {} 4 // 此方法加上synchronized修饰之后,该代码块变成同步代码块(加锁) 5 // 如此一来就能够保证线程顺序执行该方法,也就解决了线程不安全问题 6 public static synchronized Mgr04 getInstance() { 7 if (INSTANCE == null) { 8 INSTANCE = new Mgr04(); 9 } 10 return INSTANCE; 11 } 12 public void m() {System.out.println("do method");} 13 }
看似又解决了问题,可是因为同步锁的存在,使得这个静态方法的效率急剧降低。由于每一个线程必须在获得锁之后才能获取到对象,得不到锁就只能阻塞线程,能不慢吗(只是相比下很慢)。
1 public class Mgr05 { 2 private static Mgr05 INSTANCE; 3 private Mgr05() {} 4 // 这一次,我把同步代码块放在if (INSTANCE==null) 后面 5 // 代码行数标出来方便下面理解 6 public static Mgr05 getInstance() { //4 7 if (INSTANCE == null) { //5 8 synchronized (Mgr05.class) { //6 9 INSTANCE = new Mgr05(); //7 10 } //8 11 } //9 12 return INSTANCE; //10 13 } //11 14 15 public void m() {System.out.println("do method");} 16 }
这样的写法,由于先检查是否已经实例化INSTANCE,再加锁在初始化代码块上,又又看似解决了上面的效率问题,其实又带来了新的问题。
首先首次Thread1,Thread2 几乎同时进入到getInstance方法中,两个线程执行到第5行,INSTANCE为空,Thread1先拿到第6行代码的锁,Thread2等待锁释放,Thread1执行第7行,而后释放锁,以后Thread2得到锁,也执行第7行。这样INSTANCE就被初始化了2次,仍然线程不安全。
若是检查一次不行,那就再检查一次如何
1 public class Mgr06 { 2 private static volatile Mgr06 INSTANCE; 3 // private static Mgr06 INSTANCE; 4 // volatile 关键字能够保证多个线程初始化时,CPU指令能顺序执行 5 private Mgr06() {} 6 public static Mgr06 getInstance() { 7 // 双重检查 8 if (INSTANCE == null) { 9 synchronized (Mgr06.class) { 10 if (INSTANCE == null) { 11 INSTANCE = new Mgr06(); 12 } 13 } 14 } 15 return INSTANCE; 16 } 17 18 public void m() {System.out.println("do method");} 19 }
双重检查下,这样的单例模式就没有上面的各类问题了。
可是!关于DCL单例需不须要加volatile关键字的问题,这里还须要额外说明一下。
因为JVM是无法保证CPU指令顺序执行的(容许乱序执行),不加上volatile关键字就会可能出现意外。
而volatile 关键字能够保证多个线程初始化时,CPU指令能顺序执行
须要知道的是instance = new Mgr05();这句代码并非一个原子操做,他的操做大致上能够被拆分为三步
1.分配内存空间
2.实例化对象instance
3.把instance引用指向已分配的内存空间,此时instance有了内存地址,再也不为null了
java是容许对指令进行重排序, 那么以上的三步的执行顺序就有多是1-3-2. 在这种状况下, 若是线程A执行完1-3以后被阻塞了, 而刚好此时线程B进来了 此时的instance已经不为空了因此线程B判断完INSTANCE == null 结果为 false 之后就直接返回了这个尚未实例化好的instance, 因此在调用其后续的实例方法时就会得不到预期的结果
具体能够参考该文章:https://blog.csdn.net/ACreazyCoder/aicle/details/80982578
虽然这算是个完美的解决方法。可是代码上仍是不够简洁。
先来看代码
public class Mgr07() { private Mgr07() {} // 加载外部类时不会加载内部类,这样能够实现懒加载 // 同时JVM也保证了静态对象的单例性质 private static class Mgr07Holder { private final static Mgr07 INSTANCE = new Mgr07(); } public static getInstance() { return Mgr07Holder.INSTANCE; } public void m() {System.out.println("do method");} }
定义了一个静态内部类,当JVM加载Mgr07这个类时,是不会加载其内部类的,也就是说,只有调用getInstance方法时,才会加载Mgr07Holder,同时初始化INSTANCE对象。
先看代码
public enum Mgr08 { INSTANCE; public void m{System.out.println("do method");} }
超级精简的实现。这是SUN发布的《Effective JAVA》中推荐的一种写法。利用枚举类型的特性。是完美的解决方式之一。
这样写还能够解决反序列化从新生成新对象的问题。