你知道几种单例模式?(文末彩蛋)

Posted 码个蛋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你知道几种单例模式?(文末彩蛋)相关的知识,希望对你有一定的参考价值。


作者博客

    http://www.jianshu.com/u/d5b531888b2b

前言

大家好,先简单自我介绍一下啊,我呢现在是在做android这一块,做这一块大概快两年了。工作地是在河南郑州。在开始之前呢,首先感谢一下宇明兄弟啊,因为自己当初写博客的习惯是通过他的启发才开始,现在又做这一块语音文字的方式分享技术,我感觉挺有意义的,通过这种分享不仅能让那些对分享内容感兴趣的人学到知识,更重要的是分享着本人也能对知识有个更深的理解,当然如果有不理解或者疑问的地方,也能和大家讨论解决困惑,我想这就是这个分享活动的真正意义。


目录

  1. 什么是设计模式?

  2. 设计模式的六大原则是什么?

  3. 设计模式的分类?

  4. 什么是单例模式?

  5. 单例模式常见的7种实现方式及优缺点

    1. 懒汉式

    2. 懒汉式(线程安全)

    3. 双重检测机制

    4. 饿汉式

    5. 静态块实现

    6. 静态内部类实现

    7. 枚举

  6. 序列化对单例模式的影响

  7. 单例模式引起内存泄漏的解决方案


1

什么是设计模式?

那现在正式开始今天的分享,设计模式这个词大家应该都不陌生啊,在写代码的时候很多人都会运用到设计模式。那什么是设计模式呢。设计模式(Design pattern)是一套被反复使用、多数人知晓的、是代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式的目标之一就是提高程序的可复用性。考虑的是怎样才能将程序作为“组件”复用。


2

设计模式的六大原则是什么?

  • 开闭原则(Open Close Principle):对扩展开放,对修改关闭

  • 里氏替换原则(Liskov Substitution Principle):子类可以扩展父类的功能,但不能改变父类原有的功能

  • 依赖倒转原则(Dependence Inversion Principle):高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象,核心思想是面向接口编程

  • 接口隔离原则(Interface Segregation Principle)客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

  • 迪米特法则(最少知道原则)(Demeter Principle)一个对象应该对其他对象保持最少的了解

  • 单一职责原则( Single responsibility principle )不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。


3

设计模式的分类

GOF设计模式分类:23种


创建型设计模式:

对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。

  • 工厂方法模式(Factory Method)

  • 抽象工厂模式(Abstract Factory)

  • 创建者模式(Builder)

  • 原型模式(Prototype)

  • 单例模式(Singleton)


结构型模式:

描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。

  • 外观模式/门面模式(Facade门面模式)

  • 适配器模式(Adapter)

  • 代理模式(Proxy)

  • 装饰模式(Decorator)

  • 桥梁模式/桥接模式(Bridge)

  • 组合模式(Composite)

  • 享元模式(Flyweight)


行为型设计模式:

是对在不同的对象之间划分责任和算法的抽象化。

行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用

  • 模板方法模式(Template Method)

  • 观察者模式(Observer)

  • 状态模式(State)

  • 策略模式(Strategy)

  • 职责链模式(Chain of Responsibility)

  • 命令模式(Command)

  • 访问者模式(Visitor)

  • 调停者模式(Mediator)

  • 备忘录模式(Memento)

  • 迭代器模式(Iterator)

  • 解释器模式(Interpreter)


4

什么是单例模式?

程序在运行的时候,通常都会生成多个实例,例如表示字符串的java.lang.String类的实例与字符串是一对一的关系,所以当有一千个字符串的时候,就会生成1000个实例,


许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。(维基百科)。


总结为确保任何情况下都绝对只有一个实例。或者想在程序上表现出“只存在一个实例”这就是单例模式。


5

单例模式常见的7种实现方式及优缺点

懒汉式线程不安全

在单例模式中,有一种称为懒汉式的单例模式。顾名思义,懒汉式可以理解使用时才进行初始化,它包括私有的构造方法,私有的全局静态变量,公有的静态方法,是一种懒加载机制。

/**

 * Created by Code4Android

 *1懒汉式线程不安全

 */

public class Singleton {


    private static Singleton instance;


    private Singleton() {

        System.out.println("初始化");

    }


    public static Singleton getInstance() {

        if (instance == null) {

            instance = new Singleton();

        }

        return instance;

    }

}


上面是最常见的懒汉式单例模式的写法,但是如果在多线程的情况下,上述方法就会出现问题,它达不到只有一个单例对象的效果,例如当某个线程1调getInstance()方法并判断instance == null,此时(就在判断为空后new Singleton()之前)另一个线程2也调用getInstance()方法,由于此时线程1还没有new出对象,则线程2执行getInstance()中instance 也为空,那么此时就会出现多个实例的情况,而达不到只有一个实例的目的。


懒汉式(线程安全)

在上述实现中我们提到的懒汉式单例模式是一种非线程安全的,非线程安全即多线程访问时会生成多个实例。那么怎么样实现线程安全呢,也许你应该已经想到使用同步关键字synchronized。

/**

 * Created by Code4Android

 *2懒汉式线程安全:

 */

public class Singleton {


    private static Singleton instance;


    private Singleton() {

        System.out.println("初始化");

    }


    public static synchronized Singleton getInstance() {

        if (instance == null) {

            instance = new Singleton();

        }

        return instance;

    }

}


使用同步关键字后,也就实现了线程安全访问,因为在任何时候它只能有一个线程调用 getInstance() 方法。那么你可能会发出疑问,这样加入同步,在高并发情况下,效率是很低的,因为真正需要同步的是我们第一次初始化的时候,是的,所以我们要进行进一步的优化。


双重检测机制

双重检测顾名思义就是两次检测,一次是检测instance 实例是否为空,进行第一次过滤,在同步快中进行第二次检测,因为当多个线程执行第一次检测通过后会同时进入同步快,那么此时就有必要进行第二次检测来避免生成多个实例。

/**

 * Created by Code4Android

 *3DCL

 */

public class DCLSingleton {


    private static DCLSingleton instance;


    private DCLSingleton() {

        System.out.println("初始化");

    }

    public static synchronized  DCLSingleton getInstance() {

        if(instance==null){

            synchronized (DCLSingleton.class) {

                if (instance == null) {

                    instance = new DCLSingleton();

                }

            }

        }

        return instance;

    }

}


对于上面的代码时近乎完美的,既然说近乎完美,那肯定还是有瑕疵的,瑕疵出现的原因就是instance = new Singleton();这一句代码,你可能会问,这会有什么问题,其实我也不知道,哈哈。

在计算机语言中,初始化包含了三个步骤


  1. 分配内存

  2. 执行构造方法初始化

  3. 将对象指向分配的内存空间


由于java编译器为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU就会出现指令重排序(happen-before),从而导致上面的三个步骤执行顺序发生改变。正常情况下是123,但是如果指令重排后执行为1,3,2那么久会导致instance 为空,进而导致程序出现问题。


既然已经知道了上述双重检测机制会出现问题,那么我们该怎么避免出现,该如何解决呢?

在java语言中有一个关键字volatile,我们都知道它提供了内存可见性这一特性,其实它还有一个作用就是防止指令重排序,那么我们把变量singleton用volatile修饰下就可以了。


饿汉式

饿汉式与懒汉式区别是它在类加载的时候就进行初始化操作,而懒汉式是调用getInstance()方法时才进行初始化,有延迟加载的作用。

/**

 * Created by Code4Android

 *4饿汉式

 */

public class HungrySingleton {

    private static  HungrySingleton instance = new HungrySingleton();

 

    private HungrySingleton() {

        System.out.println("初始化");

    }


    public static HungrySingleton getInstance(){

        return instance;

    }


}


与懒汉式相比,它是线程安全的(无需用同步关键字修饰),由于没有加锁,执行效率也相对较高,但是也有一些缺点,在类加载时就初始化,会浪费内存。


静态块实现方式

/**

 * Created by Code4Android

 *5静态块

 */

public class HungrySingleton{

    private   HungrySingleton instance = null;

    //饿汉式变种5

    static {

        instance = new HungrySingleton();

    }

    private HungrySingleton() {

        System.out.println("初始化");

    }


    public static HungrySingleton getInstance(){

        return instance;

    }

}


静态内部类实现方式

/**

 * Created by Code4Android on 2017/3/10.

 *6静态内部类

 */

public class StaticInnerClassSingleton {

    private static class SingletonHolder {

        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();

    }


    private StaticInnerClassSingleton() {

        System.out.println("初始化");

    }


    public static final StaticInnerClassSingleton getInstance() {

        return SingletonHolder.INSTANCE;

    }

}


静态内部类相对实现较为简单,并且它是一种懒加载机制, 当Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。


枚举

/**

 * Created by Code4Android

 * 7默认枚举实例的创建是线程安全的

 */

public enum EnumSinglton {

    INSTANCE;


    private EnumSinglton() {

        System.out.println("构造方法");

    }

    public void  doSomething() {

        System.out.println("调用单例方法");

    }


}


枚举方式实现的单例模式是一种线程安全的单例模式。


6

序列化对单例模式的影响

通过上面我们对单例模式的学习,对单例模式有了进一步的学习,当我们有序列化的需求之后,那么会产生怎样的效果呢?先通过下面的代码来看下结果

public class Main {

    public static void main(String[] args) throws Exception {


        //测试序列化对单例模式的影响

        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        new ObjectOutputStream(bos).writeObject(LazySingleton.getInstance());

        ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());

        LazySingleton singleton = (LazySingleton) new ObjectInputStream(bin).readObject();



        ByteArrayOutputStream bos1 = new ByteArrayOutputStream();

        new ObjectOutputStream(bos1).writeObject(EnumSinglton.INSTANCE);

        ByteArrayInputStream bin1 = new ByteArrayInputStream(bos1.toByteArray());

        EnumSinglton singleton1 = (EnumSinglton) new ObjectInputStream(bin1).readObject();

        System.out.println(singleton == LazySingleton.getInstance());//false

        System.out.println(singleton1 == EnumSinglton.INSTANCE);//true

    }

}


通过我们的测试瞬时就懵逼了,除了通过枚举方式实现的单例模式,其它几种实现都生成了多个实例。那么该如何解决呢?难道反序列话只能生成多个实例。当然不是。我们可以看下面修改后的代码


public class HungrySingleton implements Serializable {

    private static  HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {

        System.out.println("初始化");

    }


    public static HungrySingleton getInstance(){

        return instance;

    }


    /**

     * 如果序列化,需要加入此方法,否则单例模式无效

     * @see java.io.ObjectStreamClass

     * @return

     */


    private Object readResolve() {

        return instance;

    }


}


单例模式引起内存泄漏的解决方案

public class LazySingleton {

    private static volatile LazySingleton instance;

    private Context context;


    private LazySingleton(Context context) {

        this.context=context;

        System.out.println("初始化");

    }


    public static   LazySingleton getInstance(Context context) {

        if(instance==null){

            synchronized (LazySingleton.class) {

                if (instance == null) {

                    instance = new LazySingleton(context);

                }

            }

        }

        return instance;

    }

}


上午在群里听到有人反映说使用单例模式造成了内存泄漏,那么咱们就分析一下造成内存泄漏的原因,如上面代码,该单例模式中context是强引用,并且被static的变量instance持有,因为在java中静态变量在类被装载的时候分配内存,在被卸载的时候销毁,也就是说它的生命周期就是应用的整个生命周期。那么当我在一个Activity或者服务广播中通过LazySingleton.getInstance(BaseActivity.this);获取单例并使用,

由于LazySingleton的创建引用了Activity,所以导致系统GC的时候试图去回收Activity时,发现它却被单例LazySingleton所引用,所以GC回收失败,进而导致内存泄漏。


既然我们明白了为什么单例模式会导致内存泄漏,那么解决也就简单了,我们不需要传入contex对象,可以使用Application的getApplicationContext即可。


当然我们可以使用弱引用WeakReference,它不同于强引用,它可以被系统回收,从而能解决避免内存泄漏。


Tips:在《如何高效学习》这本书中提到过将知识理解并内在化最好的方式是通过不同形式来学习,如:看过的文章(视觉)以及以下的音频(听觉)。


音频




以上是关于你知道几种单例模式?(文末彩蛋)的主要内容,如果未能解决你的问题,请参考以下文章

几种单例模式实现方式及其优缺点分析

这 9 种单例模式你都会吗?

常见的几种单例模式

常见的几种单例模式

常见的几种单例模式

常见的几种单例模式