详解JAVA面向对象的设计模式 (一)、单例模式

前言

本系列,记录了我深入学习设计模式的过程。也算是JAVA进阶学习的一个重要知识点吧。java

与设计相关的代码会贴出,可是基础功能的代码会快速带过。有任何错误的地方,都欢迎读者评论指正,感谢。冲冲冲!设计模式

 

单例模式 Singleton

应用场景

只须要一个实例存在的场景安全

  • 好比各类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次,仍然线程不安全。

六、双重检查 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/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对象。

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

八、枚举单例

先看代码

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

 

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

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

相关文章
相关标签/搜索