版權聲明:本文爲博主原創文章,未經博主容許不得轉載html
PS:轉載請註明出處
做者: TigerChain
地址: www.jianshu.com/p/62b2e8962…
本文出自 TigerChain 簡書 Android 設計模式系列java
教程簡介android
正文git
一個男人只能有一個媳婦「正常狀況」,一我的只能有一張嘴,一般一個公司只有一個 CEO ,一個狼羣中只有一個狼王等等github
一句話,就是保證一個類僅有一個實例便可「new 一次」,其實好多人都不把單例看成成一個設計模式,只是看成是一個工具類而已,由於它的確很簡單,而且當你面視的時候面視官問你設計模式的時候估計都會說:能夠說說你的你瞭解的設計模式嗎「單例除外」。雖然很簡單,可是咱們仍是要掌握和了解它數據庫
單例模式的定義編程
單例單例就是單一的實例,單例模式就保證一個類僅有一個實例,而且提供一個能夠仿問的全局方法能夠訪問它設計模式
單例模式的應用安全
單例的特色bash
單例模式的結構
角色 | 類別 | 說明 |
---|---|---|
Singleton | 單例類 | 就是一個普通的類 |
getInstance() | 一個靜態方法 | 提供類的實例 |
單例模式的 UML
從上圖咱們能夠了解到編寫一個單例的基本步驟「我稱之爲三步法」
簡單的代碼結構就是
class SingleTon{
private static SingleTon instance ;
private SingleTon(){}
public static SingleTon getInstance(){
if(null == instance){
instance = new SingleTon();
}
return instance ;
}
}複製代碼
在實際開發中,咱們按照以上三步法就能夠建立出一個單例來「直接用方法套用便可」
單例模式舉例
好比在一個狼羣當中,只有一個狼王,有若干偵察狼、捕獵狼等等,這樣就組成了一個狼羣,下面看簡單的 java 代碼「代碼只是用來演示單例模式,參考便可」
先看看狼王單例簡單的 UML
根據 UML 編碼
public interface IWolf {
void doSomting() ;
}複製代碼
/** * 偵察狼 */
public class ZhenChaLang implements IWolf {
@Override
public void doSomting() {
// 執行狼王交行的任務
System.out.println(" 去探路");
}
public void fangShao(){
System.out.println(" 去放哨");
}
}複製代碼
/** * 捕獵狼 */
public class BuLieLang implements IWolf {
@Override
public void doSomting() {
System.out.println(" 去獵羊");
}
}複製代碼
/** * 狼王 */
public class LangWang implements IWolf {
private static LangWang langWang ;
private LangWang(){
System.out.println("狼王產生了--構造方法被調用");
}
public static LangWang getLangWang(){
if(null == langWang){
langWang = new LangWang() ;
}
System.out.println("狼王對應的地址:"+langWang.toString());
return langWang ;
}
public static void main(String args[]){
LangWang.getLangWang().doSomting();
LangWang.getLangWang().buLie();
}
@Override
public void doSomting() {
// 安排一些工做給下屬狼 好比偵查狼
ZhenChaLang zhenChaLang1 = new ZhenChaLang() ;
System.out.print("偵察狼 "+zhenChaLang1.toString());
zhenChaLang1.doSomting();
ZhenChaLang zhenChaLang2 = new ZhenChaLang();
System.out.print("偵察狼 "+zhenChaLang2.toString());
zhenChaLang2.fangShao();
}
public void buLie(){
BuLieLang buLieLang1 = new BuLieLang() ;
System.out.print("捕獵狼 "+buLieLang1.toString());
buLieLang1.doSomting();
BuLieLang buLieLang2 = new BuLieLang() ;
System.out.print("捕獵狼 "+buLieLang2.toString());
buLieLang1.doSomting();
}
}複製代碼
咱們能夠看到狼王是一個單例的「一個狼羣確實只有一個狼王」,下面咱們來驗證一下結果
咱們能夠看到,雖然咱們調用了兩次狼王實例方法確實都是同一個狼王,而不偵查狼和捕獵狼分別是不一樣的狼,這就是一個單例的使用,各自體會一下。
上面狼王的例子中咱們使用的是非線程安全的懶漢式單例模式,單例模式有好幾種實現方式,下面咱們來講說這幾種實現方式
單例模式的幾種實現方式
餓漢式單例模式如其名,是一個餓貨,類的實例在類加載的時候就初始化出來「把這一過程看成一個漢堡,也就是說必需要把漢堡提早準備好,餓貨就知道吃」
優勢:
沒有加鎖,執行效率很是高「實際上是以空間來換時間」缺點:
在類加載的時候就會初始化,浪費內存「你知道我要不要使用這個實例嗎,你就給我初始化,太任性了」public class SingleTon{
// 一、成員變量靜態化 餓漢式直接在類加載的時候就初始化實例
private static SingleTon instance = new SingleTon();
// 二、構造方法私有化
private SingleTon(){}
// 三、實例公有方法靜態化
public static SingleTon getInstance(){
return instance ;
}
}複製代碼
懶漢式單例模式,是在我須要的時候纔去初始化實例,也就是說在類加載的時候,靜態成員變量是 null 的,只有須要它的時候纔去初始化實例,因此懶漢式能夠延時加載
優勢:
延時初始化類,省資源,不想用的時候就不會浪費內存缺點:
線程不安全,多線程操做就會有問題public class SingleTon{
// 一、類變量靜態化 類加載的時候是空的,因此不開闢內存
private static SingleTon instance = null ;
// 二、構造方法私有化,這沒什麼好說的
private SingleTon(){}
// 三、實例方法公有而且靜態化
public static SingleTon getInstance(){
if(null == instance){
instance = new SingleTon() ;
}
}
return instance ;
}複製代碼
懶漢式線程安全比懶漢式線程不全多了一個線程安全
優勢:
延時初始化類,省資源,不想用的時候就不會浪費內存,而且線程安全缺點:
雖然線程安全,可是加了鎖對性能影響很是大「至關於排隊獲取資源,沒有拿到鎖子就乾等」public class SingleTon{
private static SingleTon instance ;
private SingleTon(){}
// 在這裏加一個同步鎖,這樣就保證線程安全了
public static synchronized SingleTon getInstance(){
if(null == instalce){
instance = new SingleTon() ;
}
return instance ;
}
}複製代碼
如其名,雙檢鎖,這種方式單例模式在多線程的狀況下能提升性能
優勢:
延時初始化類,省資源,不想用的時候就不會浪費內存,而且線程安全,雙重加鎖,多線程仿問性能達到提高「後面詳細說 WHY」缺點:
雖然線程安全,可是於在多線程中因爲指令重排會有問題「後面會說」public class DCLSingleTon {
/**一、成員變量靜態化**/
private static DCLSingleTon instance ;
/**二、構造方法私有化*/
private DCLSingleTon(){}
/**三、實例方法靜態化**/
public static DCLSingleTon getInstance(){
if(null == instance){ //第一次檢查
synchronized (DCLSingleTon.class){ //加鎖
if(null == instance){ // 第二次檢查
instance = new DCLSingleTon() ;
}
}
}
return instance ;
}
}複製代碼
雙檢鎖性能提升
那麼這種方式,如何保證線程而且有很好的性能呢,首先安全安全不說了看到 synchronized 關鍵字咱們就知道了,這裏說一下爲何說性能比 3 中的提升了呢
咱們知道線程安全性能主要是出在 synchronized 鎖上,咱們只要能保證鎖最小化調用便可
從上面代碼能夠看出,只有第一次當 instance 爲空的時候,纔會去調用 synchronized 中的方法,之後就直接返回 synchronized 實例了,也就說 synchronized 只調用一次,因此在多線程上性能會大大的提高
指令重排引發 DCL 問題
這樣作看起來很不錯,解決了多線程問題並延時加載,而且同步一次性能有了不錯的提高,可是這樣作仍然會有問題,這和 Java 的內存模型有關「這種內存模型可讓處理器大大的提升執行效率」
若是再深刻的說,就要說 JAVA 的內存模型了「這不在本節範圍以內」,你們只要記住,Java 的指令重排會致使多線程問題「單線程不會受影響」,指令排序通俗的說就是代碼執行順序改變了,好比:如下一個簡單的例子「下面代碼只是爲了說明問題,並非真實狀況下的代碼」
class A{
private static int a,b = 0 ;
public static void main(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 2 3 執行順序那麼也就存在什麼問題,但是實際狀況是 2 3 執行順序是不肯定的「指令重排序」,這時結果就會成 1 3 2 ,那麼問題來了,假如按後者來講,3 剛執行完畢,2 尚未開始以前,忽然被另一個線程2搶佔了,此時 instance 已經非空的「可是卻沒有初始化」,那麼線程2會直接返回 instance 去使用,結果就是掛了
好了,既然找到了問題,那麼解決辦法有如下兩種
使用 volatile 關鍵字「Java 5 以後 volatile 就能夠禁止對指令從新排序 」,就能夠指令不發生重排,修改代碼
public class DCLSingleTon {
/**一、成員變量靜態化**/
private volatile static DCLSingleTon instance ;
/**二、構造方法私有化*/
private DCLSingleTon(){}
/**三、實例方法靜態化**/
public static DCLSingleTon getInstance(){
if(null == instance){ //第一次檢查
synchronized (DCLSingleTon.class){ //加鎖
if(null == instance){ // 第二次檢查
instance = new DCLSingleTon() ;
}
}
}
return instance ;
}
}複製代碼
固然了,Java 5 以後才能完美的使用 volatile ,那麼以前如何解決 DCL 安全問題呢?可使用 Thread Local ,臨時變量等具體能夠看關於 DCL 的講解以及改善 雙重鎖定被破壞聲明 說的很是的好
利用 classloder 的機制來保證初始化 instance 時只有一個線程。JVM 在類初始化階段會獲取一個鎖,這個鎖能夠同步多個線程對同一個類的初始化
修改代碼
public class DCLSingleTon {
private DCLSingleTon(){}
static class SingleTonHolder{
private static final DCLSingleTon instance = new DCLSingleTon() ;
}
public static DCLSingleTon getInstance(){
return SingleTonHolder.instance ;
}
}複製代碼
靜態內部類能夠容許指令重排,可是對別的線程是不可見的,那麼就想當於單線程指令重排對結果是沒有影響的「這是內存模型的特色」,咱們來一下單線程的執行行時序圖,咱們來看 SingleTon instence = new SingleTon()
這一過程
因此靜態內存類單例,你就能夠理解成一個線程把上述過程作完了,因此別的線程看不見,因此不會出現時間排序的問題
只要保證 2 在 4 的前面,那麼 2 3 是否重排,對結果都是沒有影響的「在單線程的狀況下」
優勢:
延時初始化類,省資源,不想用的時候就不會浪費內存,而且線程安全,還能夠執行其它的靜態方法缺點:
--public class SingleTon {
private SingleTon(){}
static class SingleTonHolder{
private static final DCLSingleTon instance = new DCLSingleTon() ;
}
public static SingleTon getInstance(){
return SingleTonHolder.instance ;
}
}複製代碼
枚舉類單例模式是 《Effective Java》 做者極力推薦的單例的方法
特色也就是檢舉類的特色,咱們先看看枚舉類的特色吧,多說無用,咱們結合 java 代碼來分析
// 一週的枚舉,這裏爲了說明問題,只列舉到週三
public enum EnumDemo {
MONDAY,
TUESDAY,
WEDNESDAY ;
public void donSomthing(){}
}複製代碼
以上就是一個簡單的枚舉 Java 類,咱們反編譯來看一下它的實現機制是雜樣的,在這裏我使用 jad 來反編譯「固然你也可使用 javap 來反編譯還能看到二制」,以上 java 代碼反編譯出來的結果以下:
從以上反編譯出來的代碼圖咱們能夠看出如下幾點信息:
以上就是枚舉類的特色,很符合單例模式,而且集成上以上幾種單例模式的優勢
優勢:
除以上特色優勢以外,枚舉類還有兩個優勢:寫法簡單
、支持序列化和反序列化操做「以上的單例序列化和反序列化會破壞單例模式」
、而且反射也不能調用構造方法
缺點:
--public enum EnumSingleTon {
INSTACE; // 定義一個枚舉原素,表明 EnumSingleTon 一個實例
/** * 枚舉中的構造方法只能寫成 private 或是不寫「不寫默認就是 private」,因此枚舉防止外部來實例化對象 */
EnumSingleTon(){}
/** * 一些額外的方法 */
public void doSometing(){
Log.e("枚舉類單例","這是枚舉單例中的方法") ;
}
}複製代碼
通常狀況下,不建議使用第 2 種和第 3 種懶漢式單例,建議使用第 1 種餓漢式單例,若是項目中明確要使用延時加載那麼使用第 5 種靜態內存類的單例,若是有序列化反序列化操做可使用第 6 種單例模式,若是是其它需求可使用第 4 種 DCL 單例
一、 InputMethodManager 類
InputMethodManager 就一個服務類「輸入法類」源碼目錄 Androidsdk\sources\android-26\android\view\inputmethod
,部分代碼以下:
@SystemService(Context.INPUT_METHOD_SERVICE)
public final class InputMethodManager {
// 省略若干行代碼
...
static InputMethodManager sInstance;
// 省略若干行代碼
...
// 如下是構造方法,沒有聲明權限就是私有的
InputMethodManager(Looper looper) throws ServiceNotFoundException {
this(IInputMethodManager.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)), looper);
}
// 如下是構造方法,沒有聲明權限就是私有的
InputMethodManager(IInputMethodManager service, Looper looper) {
mService = service;
mMainLooper = looper;
mH = new H(looper);
mIInputContext = new ControlledInputConnectionWrapper(looper,
mDummyInputConnection, this);
}
public static InputMethodManager getInstance() {
synchronized (InputMethodManager.class) {
if (sInstance == null) {
try {
sInstance = new InputMethodManager(Looper.getMainLooper());
} catch (ServiceNotFoundException e) {
throw new IllegalStateException(e);
}
}
return sInstance;
}
}
// 省略若干行代碼
...
}複製代碼
從上面代碼能夠看出,InputMethodManager 是一個典型的-- 線程安全的懶漢式單例
二、Editable 類
文件目錄:frameworks/base/core/java/android/text/Editable.java 部分代碼以下:
private static Editable.Factory sInstance = new Editable.Factory();
/** * Returns the standard Editable Factory. */
public static Editable.Factory getInstance() {
return sInstance;
}複製代碼
能夠看到很是典型的一個餓漢式單例模式
Android 源碼中有很是多的單例模式的例子,這裏就一一列舉了,相信你看完上面的介紹絕對能夠寫出一個適合本身項目的單例了
到此爲止,咱們就把單例械說完了,動手試試吧,點贊是一種鼓勵,是一種美德