详解JAVA面向对象的设计模式 (一)、单例模式
前言
本系列,记录了我深刻学习设计模式的过程。也算是JAVA进阶学习的一个重要知识点吧。
与设计相关的代码会贴出,但是基础功能的代码会快速带过。有任何错误的地方,都欢迎读者评论指正,感谢。冲冲冲!
单例模式 Singleton
应用场景
只需要一个实例存在的场景
-
比如各种Manager 类
-
比如各种Factory 类
实现方式
总得来说,一共有8种单例的实现方式。其中只有2种是完美解决单例问题的。
其他6种都有他的缺点。下面会通过先讲述6个实现并解决他们的缺点的方式,一个个来讲实现。
1、2、饿汉式
先来看看它的实现
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对象。
-
缺点:
唯一的缺点就是不管这个类是否被用到,它都会被完成实例化。
3、懒汉式
懒汉式相比上面的饿汉式,它主要是弥补饿汉式的缺点,于是就有下面代码
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()。这样就在首次使用时无法保证该类的单例存在。
4、懒汉式 -- 优化
为了解决懒汉式的线程不安全问题,我们尝试用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 }
看似又解决了问题,但是由于同步锁的存在,使得这个静态方法的效率急剧下降。因为每个线程必须在得到锁以后才能获取到对象,得不到锁就只能阻塞线程,能不慢吗(只是相比下很慢)。
5、懒汉式 -- 再优化
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次,仍然线程不安全。
6、双重检查 double-check locking (DCL单例)
如果检查一次不行,那就再检查一次如何
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/article/details/80982578
虽然这算是个完美的解决方法。但是代码上还是不够简洁。
7、静态内部类
先来看代码
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对象。
8、枚举单例
先看代码
public enum Mgr08 { INSTANCE; public void m{System.out.println("do method");} }
超级精简的实现。这是SUN发布的《Effective JAVA》中推荐的一种写法。利用枚举类型的特性。是完美的解决方式之一。
这样写还可以解决反序列化重新生成新对象的问题。