详解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对象。

这么一来,既能保证INSTANCE的单例,也可以实现懒加载。代码也很简单清晰。是完美的解决方式之一。

8、枚举单例

先看代码

public enum Mgr08 {
    INSTANCE;
    
    public void m{System.out.println("do method");}
}

 

超级精简的实现。这是SUN发布的《Effective JAVA》中推荐的一种写法。利用枚举类型的特性。是完美的解决方式之一。

这样写还可以解决反序列化重新生成新对象的问题。(并不常见)

posted @ 2020-08-30 16:20  Rooooy7  阅读(207)  评论(0编辑  收藏  举报