设计模式专家

设计模式(三)—— 让我们一起来扒光“单例模式”

2018-11-21  本文已影响76人  X_Meteor

前言

时隔一月之久,设计模式第三篇终于来了。(好吧,主要还是我自己拖沓了,下次注意)

通过前两篇文章,相信大家已经对设计模式有了大概的了解。那么今天我们就正式进入模式的学习。首先肯定要先拿软柿子捏,单例模式——最简单的设计模式

身为程序员,你可能没有系统的学习过设计模式,但是你一定知道单例模式,因为它相对简单,而且最常被大家所用到。既然大家都用到过,也都知道为什么我还要单独列出一篇文章来写呢?

因为绝大部分开发者平时对单例模式的认识,可能仅仅停留在“会用”的阶段。为什么会有这个模式?为什么要用这个模式?在哪里用单例模式最合适?乱用了会有什么负面影响?

这些可能大多数人都一知半解。今天就让我们大家一起来扒光单例模式的外衣,有深度的认识一下单例模式。

扒光

通过这篇文章你能学到什么

(建议你可以带着问题去学习)

  1. 单例模式的​定义
  2. 单例模式在Android源码中的应用
  3. 单例模式的九种写法以及优劣对比
  4. 单例模式的使用场景
  5. 单例模式存在的缺点​​​​​​

接下来我们就一起进入今天的学习了

单例模式的定义

在学单例模式之前,我想大家都会自己问自己:“单例模式存在的意义是什么?我们为什么要用单例模式?”

众所周知,在古代封建社会,一个国家都只有一个国王或者叫皇帝。我们在这个国家的任何一个地方,只要提起国王,大家都知道他是谁。因为国王是唯一的。其实这个就是单例模式的核心思想:保证对象的唯一性。

​单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。 单例模式是一种对象创建型模式。

​​从其定义我们可以看出来单例模式存在三个要点:

1、实例唯一性
2、自行创建
3、全局访问

​​​​如何设计一个优秀的单例模式​其实也是围绕着这三点来的。

说了这么多了,还不知道单例模式到底啥样呢?接下来我们一起来着手设计这个“国王”的单例类。我们先看一下单例模式的类图:

单例模式类图

单例模式的类图看起来很简单,一个私有的当前类型的成员变量,一个私有的构造方法,一个 getInstance 方法,创建对象不再通过new 而通过 getInstance 让该类自行创建。相信我们大多数人使用的单例模式都是这种,因为太简单了。但是单例模式的写法可不止这一种。接下来我们一起来看一下单例模式的九种写法。

单例模式的九种写法

一、饿汉式(静态常量)
 /**
 * 饿汉式(静态常量)
 */
class King {
    private static final King kingInstance = new King();

    static King getInstance() {
        return kingInstance;
    }

    private King() {
    }
} 
二、饿汉式(静态代码块)
/**
 * 饿汉式(静态代码块)
 */
class King {
    private static King kingInstance;

    static {
        kingInstance = new King();
    }

    private King() {
    }

    public static King getKingInstance() {
        return kingInstance;
    }
} 
三、懒汉式(线程不安全)
/**
 * 懒汉式(线程不安全)
 */
public class King {
    private static King kingInstance;

    private King() {
    }

    public static King getKingInstance() {
        if (kingInstance == null) {
            kingInstance = new King();
        }
        return kingInstance;
    }
}
四、懒汉式(线程安全)
/**
 * 懒汉式(线程安全,同步方法)
 */
public class King {
    private static King kingInstance;

    private King() {
    }

    public static synchronized King getKingInstance() {
        if (kingInstance == null) {
            kingInstance = new King();
        }
        return kingInstance;
    }
}
五、懒汉式(线程安全,同步代码块)
/**
 * 懒汉式(线程安全,同步代码块)
 */
public class King {
    private static King kingInstance;

    private King() {
    }

    public static King getKingInstance() {
        if (kingInstance == null) {
            synchronized (King.class) {
                kingInstance = new King();
            }
        }
        return kingInstance;
    }
}
六、双重检查(DCL)
/**
 * 双重检查(DCL)
 */
public class King {

    private static volatile King kingInstance;

    private King() {
    }

    public static King getKingInstance() {
        if (kingInstance == null) {
            synchronized (King.class) {
                if (kingInstance == null){
                    kingInstance = new King();
                }
            }
        }
        return kingInstance;
    }
}
就是为了装逼
七、静态内部类
/**
 * 静态内部类
 */
public class King {

    private King() {
    }

    private static class KingInstance{
        private static final King KINGINSTANCE = new King();
    }

    public static King getInstance(){
        return KingInstance.KINGINSTANCE;
    }
}
八、枚举
/**
 * 枚举
 */
public enum  King {
    KINGINSTANCE;
}
九、容器类管理
/**
 * 使用容器实现单例模式(可以用于管理单例,有兴趣的可以尝试一下)
 * */
class InstanceManager {
    private static Map<String, Object> objectMap = new HashMap<>();
    private InstanceManager(){}
    public static void registerService(String key,Object instance){
        if (!objectMap.containsKey(key)){
            objectMap.put(key,instance);
        }
    }
    public static Object getService(String key){
        return objectMap.get(key);
    }
}
/**
* 使用方式
* Dog类就不贴出来了 
* 自己随便写个就行
* 可以运行一下看看 打印的地址是否一致
*/
class Test {
    public static void main(String[] args) {

        InstanceManager .registerService("dog", new Dog());

        Dog dog = (Dog) InstanceManager .getService("dog");
        Dog dog2 = (Dog) InstanceManager .getService("dog");
        Dog dog3 = (Dog) InstanceManager .getService("dog");
        Dog dog4 = (Dog) InstanceManager .getService("dog");

        System.out.println(dog);
        System.out.println(dog2);
        System.out.println(dog3);
        System.out.println(dog4);
    }
}

九种写法的优劣对比

名称 优点 缺点 是否推荐
饿汉式(静态常量) 写法简单,类装载时完成实例化。 避免了线程同步 急切实例化, 容易内存泄漏 可用
饿汉式(静态代码块) 同上 同上 可用
懒汉式(线程不安全) 懒加载 只能在单线程下使用 多线程不可用
懒汉式(线程安全,同步方法) 懒加载,方法同步锁 效率低 不推荐用
懒汉式(线程安全,同步代码块) 同上,同时改变效率低问题 不能完全保证单例 不推荐用
双重检查(DCL)① 线程安全;延迟加载;效率较高 JDK < 1.5 的时候不可用 JDK >1.5 推荐用
静态内部类② 线程安全,延迟加载,效率高。 暂无发现 墙裂推荐
枚举 写法简单,防止反序列化 JDK 1.5之后才能使用 JDK >1.5 推荐用
容器实现 可用管理多个单例对象 不常用,多创建了一个Map 可用

①:不可用原因:由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,而JDK 1.5 以及之后的版本都修复了这个问题。(面试装逼用,谨记!!!)

②:这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用, 而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

单例模式在Android源码中的应用

在我们每天接触的Android源码中其实也有很多地方用到了单例模式:

1、EventBus中获取实例:

private static volatile EventBus defaultInstance;
public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}

可以看到,EventBus采用的是双重检查(DCL)的方式实现的单例模式。

2、InputMethodManager获取实例

static InputMethodManager sInstance;
public static InputMethodManager getInstance() {
    synchronized (InputMethodManager.class) {
        if (sInstance == null) {
            IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
            IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
            sInstance = new InputMethodManager(service, Looper.getMainLooper());
        }
        return sInstance;
    }
}

我们看到,其实这里是懒汉式(同步代码块)方式的改写,去掉了外部判断为空,放到了里面。然后通过ServiceManger.getService()方法,通过容器的方式获取了单例。

在Android很多系统服务都是通过容器获取的单例。

单例模式在日常开发中的应用场景

日常开发中我们也有些场景是需要用到单例模式的,例如:

1、图片加载
2、网络请求
3、工具类封装

案例有很多,相信大家也都有用到,我就不列举了。这里我们如何合理的在项目中使用单例模式。

合理的辨析一个设计是否应该为单例模式前,大家先问问自己几个问题,也是检验标准:

Quote from 《 Use your singletons wisely 》

Will every application use this class exactly the same way? (keyword: exactly)
Will every application ever need only one instance of this class? (keyword: ever & one)
Should the clients of this class be unaware of the application they are part of?

每一个应用(组件/模块)是否以完全一致的方式来使用这个类?
每一个应用(组件/模块)是否真的只需要这个类的一个实例呢?
对于这个类的客户端类来说,对他们自己是应用中的一部分这件事是否应该保持毫无察觉的状态呢?

以上3条就是检验一个类是否应该被设计为单例模式的判断准则,

如果我们对于以上这3条均给出了“是的”的答案,那么这个类就是可以被设计为单例模式了。反之还是不要用的好。

单例模式的优点

单例模式的优点其实已经在定义中提现了:可以减少系统内存开支,减少系统性能开销,避免对资源的多重占用、同时操作。

单例模式的缺点

任何事物都不是完美的,单例模式也是如此,它也存在以下几个缺点:

1、违反了单一责任链原则,测试困难

单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

2、扩展困难

由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。修改功能必须修改源码。

3、共享资源有可能不一致。

现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

总结

今天我们通过文章学习了第一个设计模式,了解了他的设计理念,学会了他的九种写法,也认识了他的优缺点。相信大家已经对单例模式有了一个全新的认识。(反正我写完文章才认识到自己原来根本不了解单例模式)

最后还是要给大家说一句话:模式是死的,代码是活的。不要硬套模式。代码会告诉你怎么做,你听就是了。(也是借鉴前辈们的经验)

设计模式目录

设计模式(一)—— 认识设计模式
设计模式(二)—— 技术直男正确“面向对象”的六大原则
设计模式(三)—— 单例模式

参考资料

《设计模式——可复用面向对象软件的基础》
《Head First设计模式》
《大话设计模式》
《设计模式之禅》
《Android 源码设计模式解析与实战》

单例模式,这一篇差不多了 -- Singleton is an angel but an evil!
单例模式的八种写法比较
潜谈单例模式
刘伟

上一篇下一篇

猜你喜欢

热点阅读