人人都会设计模式:02-单例子模式--Singleton

摘要:人人都会设计模式系统宗旨是以简洁明了方式让你明白设计模式,本文介绍了单例设计模式,你真的全方位了解过单例模式吗?我们拭目以待

版权声明:本文为博主原创文章,未经博主允许不得转载

公众号:TigerChain

欢迎关注

教程简介

1、阅读对象 本篇教程适合新手阅读,老手直接略过

2、教程难度 初级,本人水平有限,文章内容难免会出现问题,如果有问题欢迎指出,谢谢

3、Demo 地址https://github.com/githubchen001/DesignPattern请看 SingleTon 部分

正文

一、什么是单例模式

1、 生活中的单例

一个男人只能有一个媳妇「正常情况」,一个人只能有一张嘴,通常一个公司只有一个 CEO ,一个狼群中只有一个狼王等等

2、程序中的单例

一句话,就是保证一个类仅有一个实例即可「new 一次」,其实好多人都不把单例当作成一个设计模式,只是当作是一个工具类而已,因为它的确很简单,并且当你面视的时候面视官问你设计模式的时候估计都会说:可以说说你了解的设计模式吗「单例除外」。虽然很简单,但是我们还是要掌握和了解它,并且要深层次的了解它

单例模式的定义

单例单例就是单一的实例,单例模式就保证一个类仅有一个实例,并且提供一个可以仿问的全局方法可以访问它

单例模式的应用

网站的计数器

应用配置

多线程池一般也采用单例去设计

数据库配置,数据库连接池

其它等等

单例的特点

不能被外部实例化,只能自己内部实例化自己

单例生成的对象是独一无二的「节省资源」

单例模式的结构

角色类别说明

Singleton单例类就是一个普通的类

getInstance()一个静态方法提供类的实例

单例模式的 UML

从上图我们可以了解到编写一个单例的基本步骤「我称之为三步法」

1、成员变量静态化

2、构造方法私有化

3、实例方法静态化

简单的代码结构就是

class SingleTon{privatestaticSingleToninstance;privateSingleTon(){}publicstaticSingleTon getInstance(){        if(null == instance){instance=newSingleTon();        }returninstance ;    }}

在实际开发中,我们按照以上三步法就可以创建出一个单例来「直接用方法套用即可」

二、单例模式举例

单例模式举例

比如在一个狼群当中,只有一个狼王,有若干侦察狼、捕猎狼等等,这样就组成了一个狼群,下面看简单的 java 代码「代码只是用来演示单例模式,参考即可」

先看看狼王单例简单的 UML

根据 UML 编码

1、定义一个狼的接口,比如这里是下达任务

publicinterfaceIWolf{voiddoSomting();}

2、定义一个侦察狼,它是放哨和探路的

/**

* 侦察狼

*/publicclassZhenChaLangimplementsIWolf{    @OverridepublicvoiddoSomting(){// 执行狼王交行的任务System.out.println(" 去探路");    }publicvoidfangShao(){        System.out.println(" 去放哨");    }}

3、定义一个捕猎狼,猎羊

/**

* 捕猎狼

*/publicclassBuLieLangimplementsIWolf{@OverridepublicvoiddoSomting(){        System.out.println(" 去猎羊");    }}

4、主角狼王上场,统一安排规划

/**

* 狼王

*/publicclassLangWangimplementsIWolf{privatestaticLangWang langWang ;privateLangWang(){        System.out.println("狼王产生了--构造方法被调用");    }publicstaticLangWanggetLangWang(){if(null== langWang){            langWang =newLangWang() ;        }        System.out.println("狼王对应的地址:"+langWang.toString());returnlangWang ;    }publicstaticvoidmain(String args[]){        LangWang.getLangWang().doSomting();        LangWang.getLangWang().buLie();    }    @OverridepublicvoiddoSomting(){// 安排一些工作给下属狼 比如侦查狼ZhenChaLang zhenChaLang1 =newZhenChaLang() ;        System.out.print("侦察狼 "+zhenChaLang1.toString());        zhenChaLang1.doSomting();        ZhenChaLang zhenChaLang2 =newZhenChaLang();        System.out.print("侦察狼 "+zhenChaLang2.toString());        zhenChaLang2.fangShao();    }publicvoidbuLie(){        BuLieLang buLieLang1 =newBuLieLang() ;        System.out.print("捕猎狼 "+buLieLang1.toString());        buLieLang1.doSomting();        BuLieLang buLieLang2 =newBuLieLang() ;        System.out.print("捕猎狼 "+buLieLang2.toString());        buLieLang1.doSomting();    }}

我们可以看到狼王是一个单例的「一个狼群确实只有一个狼王」,下面我们来验证一下结果

我们可以看到,虽然我们调用了两次狼王实例方法确实都是同一个狼王「地址是一样的」,而侦查狼和捕猎狼分别是不同的狼,这就是一个单例的使用,各自体会一下。

上面狼王的例子中我们使用的是非线程安全的懒汉式单例模式,单例模式有好几种实现方式,下面我们来说说这几种实现方式

单例模式的几种实现方式

1、饿汉式

饿汉式单例模式如其名,是一个饿货,类的实例在类加载的时候就初始化出来「把这一过程当作一个汉堡,也就是说必须要把汉堡提前准备好,饿货就知道吃」

特点

1、是线程安全的

2、类不是延时加载「直接是类加载的时候就初始化」

优缺点

优点:没有加锁,执行效率非常高「其实是以空间来换时间」

缺点:在类加载的时候就会初始化,浪费内存「你知道我要不要使用这个实例吗,你就给我初始化,太任性了」

演示代码

public class SingleTon{    // 1、成员变量静态化  饿汉式直接在类加载的时候就初始化实例privatestaticSingleToninstance=newSingleTon();    // 2、构造方法私有化privateSingleTon(){}    // 3、实例公有方法静态化publicstaticSingleTon getInstance(){returninstance ;    }}

2、懒汉式线程不安全

懒汉式单例模式,是在我需要的时候才去初始化实例,也就是说在类加载的时候,静态成员变量是 null 的,只有需要它的时候才去初始化实例,所以懒汉式可以延时加载

特点

1、线程不安全

2、延时初始化类,在我需要的时候「也就调用 getInstance」的时候才去初始化

优缺点

1、优点:延时初始化类,省资源,不想用的时候就不会浪费内存

2、缺点:线程不安全,多线程操作就会有问题

演示代码

public class SingleTon{    // 1、类变量静态化 类加载的时候是空的,所以不开辟内存privatestaticSingleToninstance= null ;    // 2、构造方法私有化,这没什么好说的privateSingleTon(){}    // 3、实例方法公有并且静态化publicstaticSingleTon getInstance(){        if(null == instance){instance=newSingleTon() ;        }    }returninstance ;}

3、懒汉式线程安全

懒汉式线程安全比懒汉式线程不全多了一个线程安全

特点

1、线程安全

2、延时初始化类,在我需要的时候「也就调用 getInstance」的时候才去初始化化

优缺点

1、优点:延时初始化类,省资源,不想用的时候就不会浪费内存,并且线程安全

2、缺点:虽然线程安全,但是加了锁对性能影响非常大「相当于排队获取资源,没有拿到锁子就干等」

演示代码

public class SingleTon{privatestaticSingleToninstance;privateSingleTon(){}    // 在这里加一个同步锁,这样就保证线程安全了publicstaticsynchronized SingleTon getInstance(){        if(null == instalce){instance=newSingleTon() ;        }returninstance ;    }}

4、DCL「双重检查锁:double-checked locking」 单例

如其名,双检锁,这种方式单例模式在多线程的情况下能提高性能

特点

1、线程安全

2、延时初始化类,在我需要的时候「也就调用 getInstance」的时候才去初始化化

优缺点

1、优点:延时初始化类,省资源,不想用的时候就不会浪费内存,并且线程安全,双重加锁,多线程仿问性能达到提升「后面详细说 WHY」

2、缺点:虽然线程安全,但是双检锁会遇到指令重排的问题,导致多线程下失效「后面会说」

演示代码

publicclass DCLSingleTon {    /1、成员变量静态化/privatestaticDCLSingleTon instance ;/*2、构造方法私有化/privateDCLSingleTon(){}    /3、实例方法静态化/publicstaticDCLSingleTon getInstance(){        if(null == instance){ //第一次检查            synchronized (DCLSingleTon.class){ //加锁                if(null == instance){ // 第二次检查instance=newDCLSingleTon() ;                }            }        }returninstance ;    }}

双检锁性能提高

那么这种方式,如何保证线程并且有很好的性能呢,首先安全安全不说了看到 synchronized 关键字我们就知道了,这里说一下为什么说性能比 3 中的提高了呢

我们知道线程安全性能主要是出在 synchronized 锁上,我们只要能保证锁最小化调用即可

从上面代码可以看出,只有第一次当 instance 为空的时候,才会去调用 synchronized 中的方法,以后就直接返回 synchronized 实例了,也就说 synchronized 只调用一次,所以在多线程上性能会大大的提升

指令重排引起 DCL 问题

这样做看起来很不错,解决了多线程问题并延时加载,并且同步一次性能有了不错的提升,但是这样做仍然会有问题,这和 Java 的内存模型有关「这种内存模型可以让处理器大大的提高执行效率」

如果再深入的说,就要说 JAVA 的内存模型了「这不在本节范围之内」,大家只要记住,Java 的指令重排会导致多线程问题「单线程不会受影响」,指令排序通俗的说就是代码执行顺序改变了,比如:以下一个简单的例子「下面代码只是为了说明问题,并不是真实情况下的代码」

classA{privatestaticinta,b =0;publicstaticvoidmain(String args[]){        a =1;        b =2;        System.out.print("a = "+a+"b = "+b)    }}

如果按照正常情况下肯定结果是 a=1,b=2。但是如果指令排序多线程情况下就有可能会出现 a=0,b=2 ,也就是 a = 1 和 b =2 调用顺序反过来了「便于理解,实际比这个复杂多了」,这样就大概解释了指令重排,详细可以看看美团点评技术团队的Java内存访问重排序的研究讲的还是非常好的

DCL 遇到指令重排出现问题分析

上面的问题要从instance = new SingleTon()这句初始化开始「由于这是很多条指令,JVM 可能会指令重排,也叫乱序执行」,这个过程分成三个步骤

1、给 instance 分配内存

2、然后调用 SingleTon 的构造方法初始化成员变量

3、把 instance 对象指向分配的内存空间(到这一步,那么 instance 肯定就是非空的)

问题:

如果按照 1 2 3 执行顺序那么也就存在什么问题,可是实际情况是 2 3 执行顺序是不确定的「指令重排序」,这时结果就会成 1 3 2 ,那么问题来了,假如按后者来说,3 刚执行完毕,2 还没有开始之前,突然被另外一个线程2抢占了,此时 instance 已经非空的「但是却没有初始化」,那么线程2会直接返回 instance 去使用,结果就是挂了

好了,既然找到了问题,那么解决办法有以下两种

1、不让 2 3 步骤发生指令排序

2、让保证初始化 intance 时只有一个线程来操作「就是单线程操作,单线程不会存在排序问题」

解决方案一:不发生指令排序

使用 volatile 关键字「Java 5 之后 volatile 就可以禁止对指令重新排序 」,就可以指令不发生重排,修改代码

public class DCLSingleTon {    /1、成员变量静态化/privatevolatilestaticDCLSingleTon instance ;/*2、构造方法私有化/privateDCLSingleTon(){}    /3、实例方法静态化/publicstaticDCLSingleTon getInstance(){        if(null == instance){ //第一次检查            synchronized (DCLSingleTon.class){ //加锁                if(null == instance){ // 第二次检查instance=newDCLSingleTon() ;                }            }        }returninstance ;    }}

当然了,Java 5 之后才能完美的使用 volatile ,那么之前如何解决 DCL 安全问题呢?可以使用 Thread Local ,临时变量等具体可以看关于 DCL 的讲解以及改善双重锁定被破坏声明说的非常的好

解决方案二:静态内存部类 其实就是我们要说的第 5 种单例模式

利用 classloder 的机制来保证初始化 instance 时只有一个线程。JVM 在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化

修改代码

public class DCLSingleTon {privateDCLSingleTon(){}staticclass SingleTonHolder{privatestaticfinalDCLSingleToninstance=newDCLSingleTon() ;    }publicstaticDCLSingleTon getInstance(){returnSingleTonHolder.instance ;    }}

5、静态内部类单例模式

静态内部类可以允许指令重排,但是对别的线程是不可见的,那么就想当于单线程指令重排对结果是没有影响的「这是内存模型的特点」,我们来看一下单线程的执行行时序图,我们来看SingleTon instence = new SingleTon()这一过程

所以静态内存类单例,你就可以理解成一个线程把上述过程做完了,所以别的线程看不见,所以不会出现时间排序的问题

只要保证 2 在 4 的前面,那么 2 3 是否重排,对结果都是没有影响的「在单线程的情况下」

特点

1、线程安全

2、延时初始化类,在我需要的时候「也就调用 getInstance」的时候才去初始化化

优缺点

1、优点:延时初始化类,省资源,不想用的时候就不会浪费内存,并且线程安全,还可以执行其它的静态方法

2、缺点:--

演示代码

public class SingleTon {privateSingleTon(){}staticclass SingleTonHolder{privatestaticfinalDCLSingleToninstance=newDCLSingleTon() ;    }publicstaticSingleTon getInstance(){returnSingleTonHolder.instance ;    }}

6、枚举类单例

枚举类单例模式是 《Effective Java》 作者极力推荐的单例的方法

特点

特点也就是检举类的特点,我们先看看枚举类的特点吧,多说无用,我们结合 java 代码来分析

// 一周的枚举,这里为了说明问题,只列举到周三publicenumEnumDemo {  MONDAY,  TUESDAY,  WEDNESDAY ;publicvoiddonSomthing(){}}

以上就是一个简单的枚举 Java 类,我们反编译来看一下它的实现机制是杂样的,在这里我使用jad来反编译「当然你也可以使用 javap 来反编译还能看到二进制」,以上 java 代码反编译出来的结果如下:

从以上反编译出来的代码图我们可以看出以下几点信息:

1、枚举类类型是 final 的「不可以被继承」

2、构造方法是私有的「也只能私有,不允许被外部实例化,符合单例」

3、类变量是静态的

4、没有延时初始化,随着类的初始化就初始化了「从上面静态代码块中可以看出」

5、由 4 可以知道枚举也是线程安全的

以上就是枚举类的特点,很符合单例模式,并且集成上以上几种单例模式的优点

优缺点

1、优点:除以上特点优点之外,枚举类还有两个优点:写法简单、支持序列化和反序列化操作「以上的单例序列化和反序列化会破坏单例模式」、并且反射也不能调用构造方法

2、缺点:--

演示代码

publicenumEnumSingleTon{INSTACE;//定义一个枚举原素,代表 EnumSingleTon 一个实例    /**    * 枚举中的构造方法只能写成private或是不写「不写默认就是private」,所以枚举防止外部来实例化对象    */

EnumSingleTon(){}

/**    * 一些额外的方法    */

public void doSometing(){

Log.e("枚举类单例","这是枚举单例中的方法") ;

}

}

总结

一般情况下,不建议使用第 2 种和第 3 种懒汉式单例,建议使用第 1 种饿汉式单例,如果项目中明确要使用延时加载那么使用第 5 种静态内存类的单例,如果有序列化反序列化操作可以使用第 6 种单例模式,如果是其它需求可以使用第 4 种 DCL 单例

三、Android 中的单例模式

1、 InputMethodManager 类

InputMethodManager 就一个服务类「输入法类」源码目录Androidsdk\sources\android-26\android\view\inputmethod,部分代码如下:

@SystemService(Context.INPUT_METHOD_SERVICE)publicfinalclassInputMethodManager{// 省略若干行代码...staticInputMethodManager sInstance;// 省略若干行代码...// 以下是构造方法,没有声明权限就是私有的InputMethodManager(Looper looper)throwsServiceNotFoundException {this(IInputMethodManager.Stub.asInterface(                ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)), looper);    }// 以下是构造方法,没有声明权限就是私有的InputMethodManager(IInputMethodManager service, Looper looper) {        mService = service;        mMainLooper = looper;        mH =newH(looper);        mIInputContext =newControlledInputConnectionWrapper(looper,                mDummyInputConnection,this);    }publicstaticInputMethodManager getInstance() {synchronized(InputMethodManager.class) {if(sInstance ==null) {try{                    sInstance =newInputMethodManager(Looper.getMainLooper());                }catch(ServiceNotFoundException e) {thrownewIllegalStateException(e);                }            }returnsInstance;        }    }// 省略若干行代码...}

从上面代码可以看出,InputMethodManager 是一个典型的--线程安全的懒汉式单例

2、Editable 类

文件目录:frameworks/base/core/java/android/text/Editable.java 部分代码如下:

privatestaticEditable.Factory sInstance =newEditable.Factory();/**

* Returns the standard Editable Factory.

*/publicstaticEditable.FactorygetInstance(){returnsInstance;  }

可以看到非常典型的一个饿汉式单例模式

Android 源码中有非常多的单例模式的例子,这里就一一列举了,相信你看完上面的介绍绝对可以写出一个适合自己项目的单例了

到此为止,我们就把单例械说完了,动手试试吧,点赞是一种鼓励,是一种美德

参考资料:

1、美团点评技术团队:Java内存访问重排序的研究

2、双重锁定被破坏声明:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

3、方腾飞 《Java 并发编程的艺术》 第三章 Java 内存模型

版权声明:本文内容由互联网用户自发贡献,本社区不拥有所有权,也不承担相关法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:yqgroup@service.aliyun.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

用云栖社区APP,舒服~

原文链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容