單例模式

介紹

單例設(shè)計模式(Singleton)用于保證一個類在整個程序中只有一個實例,通常我們會把設(shè)計為單例的類的構(gòu)造設(shè)計成私有的,但不代表所有的單例模式的類的構(gòu)造都是私有的;
本文的主要內(nèi)容分為:

  1. 分析常見的單例形式;
  2. 使用懶漢式(DCL)實現(xiàn)一個 AppManager 管理工具類;
  3. 分析 volatile 關(guān)鍵字的作用;

常見的單例的形式

常見的單例設(shè)計模式的形式有:

  1. 餓漢式
    餓漢式的單例如果該類被加載了就會創(chuàng)建該單例對象,能夠避免多線程并發(fā)的問題,但是如果我們的類被加載了(如果有其它的方式,如靜態(tài)方法等),該單例對象就會被創(chuàng)建,因此 餓漢模式 不是我們的最優(yōu)方式;
public class AppManager {
    // 在類被加載的時候就創(chuàng)建好了對象本身
    private static AppManager mInstance = new AppManager();
    // 私有化構(gòu)造器
    private AppManager() {}
    // 提供一個外部訪問單例的方法
    public static AppManager getInstance() {
        return mInstance;
    }
}
  1. 懶漢式(DCL)
    這里就不做懶漢形式的線程不安全到線程安全的鋪墊了,我們直接對線程安全的懶漢形式進(jìn)行分析,懶漢式的單例模式是在調(diào)用 getInstance() 方法的時候去判斷單例的對象是不是為空,此時為了防止多線程并發(fā)的問題,我們需要進(jìn)行同步鎖的二次判空,關(guān)于下面代碼中的 volatile 關(guān)鍵字我們在文章的最后進(jìn)行解析;
    懶漢式既能在多線程環(huán)境中很好的工作,也能夠保證在我們需要用到單例對象的時候創(chuàng)建對象,因此是我們用的最多的單例形式;
public class AppManager {
    // 創(chuàng)建一個靜態(tài)的引用,但是不創(chuàng)建對象
    // 這里的 volatile 關(guān)鍵字在本文最后會進(jìn)行分析
    private volatile static AppManager mInstance = null;
    // 私有化構(gòu)造
    private AppManager() {}
    // 提供一個外部訪問單例的方法
    public static AppManager getInstance() {
        // 第一重判斷對象是不是為空,只要調(diào)用該方法就會判斷
        if (mInstance == null) {
            // 第二重判斷對象是不是為空,這里將當(dāng)前類的 Class 對象
            // 作為同步鎖,保證不同線程的執(zhí)行順序
            synchronized (AppManager.class) {
                if (mInstance == null) {
                    mInstance = new AppManager();
                }
            }
        }
        return mInstance;
    }
}
  1. 靜態(tài)內(nèi)部類形式
    該種方式是利用靜態(tài)類只會加載一次的機(jī)制達(dá)到單例的效果,此種形式加載也能夠達(dá)到懶加載的效果,靜態(tài)內(nèi)部類形式的單例模式是推薦使用的單例模式;
public class AppManager {
    // 創(chuàng)建一個靜態(tài)內(nèi)部類,該靜態(tài)內(nèi)部類持有一個單例類的靜態(tài)成員變量
    private static class Holder {
        private static final AppManager INSTANCE = new AppManager();
    }
    // 私有化構(gòu)造
    private AppManager() {}
    // 提供一個外部訪問單例的方法
    public static AppManager getInstance() {
        return Holder.INSTANCE;
    }
}
  1. 容器式
    容器式的單例模式我們平常自己寫的代碼中并沒有使用的很多,但是我們查看Android的源碼的時候,經(jīng)常能夠看到它的身影,例如我們獲取加載布局資源的 LayoutInflater 對象,實際上我們并不是通過 LayoutInflater 類本身來獲取,而是通過系統(tǒng)的容器來獲取 LayoutInflater 對象,容器式的核心思想是通過一個容器(例如: 'HashMap'),將我們需要的對象緩存起來,如果需要用到該對象的時候,我們只需要通過容器獲取即可,因此此種設(shè)計模式對應(yīng)類的構(gòu)造大概率不是私有的;
  2. 枚舉
    我們平常使用的枚舉形式其實也是單例設(shè)計模式的一種,但是由于枚舉會占用比較多的內(nèi)存開銷,因此不推薦使用枚舉來實現(xiàn)單例模式;

AppManager Activity管理工具類

在項目開發(fā)的過程中,我們需要對打開的 Activity 對象進(jìn)行有效的管理,例如我們收到一個推送需要立即顯示一個 Dialog 彈窗就需要獲取當(dāng)前顯示的 Activity 等,接下來我們用 懶漢模式(DCL) 實現(xiàn)一個 Activity 管理的工具類:

public class AppManager {

    private volatile static AppManager mInstance = null;

    private Stack<Activity> mActivitys;

    private AppManager() {
        mActivitys = new Stack<>();
    }

    public static AppManager getInstance() {
        if (mInstance == null) {
            synchronized (AppManager.class) {
                if (mInstance == null) {
                    mInstance = new AppManager();
                }
            }
        }
        return mInstance;
    }

    /**
     * 新增一個Activity
     *
     * @param activity
     */
    public void attachActivity(Activity activity) {
        if (activity != null) {
            mActivitys.add(activity);
        }
    }

    /**
     * 移除一個Activity對象
     *
     * @param activity
     */
    public void detachActivity(Activity activity) {
        if (activity != null && mActivitys.contains(activity)) {
            mActivitys.remove(activity);
            activity.finish();
            activity = null;
        }
    }

    /**
     * 結(jié)束棧頂?shù)腁ctivity
     */
    public void detachLastActivity() {
        if (mActivitys.isEmpty()) {
            return;
        }
        detachActivity(mActivitys.lastElement());
    }

    /**
     * 根據(jù) Class 對象結(jié)束單個 Activity
     *
     * @param activityClass
     */
    public void detachActivity(Class<?> activityClass) {
        for (int i = mActivitys.size() - 1; i >= 0; i--) {
            Activity activity = mActivitys.get(i);
            if (activity.getClass().getCanonicalName()
                    .equals(activityClass.getCanonicalName())) {
                detachActivity(activity);
                break;
            }
        }
    }

    /**
     * 根據(jù) Class 對象結(jié)束一類 Activity
     *
     * @param activityClass
     */
    public void detachActivitys(Class<?> activityClass) {
        for (int i = mActivitys.size() - 1; i >= 0; i--) {
            Activity activity = mActivitys.get(i);
            if (activity.getClass().getCanonicalName()
                    .equals(activityClass.getCanonicalName())) {
                detachActivity(activity);
            }
        }
    }

    /**
     * 移除所有的Activity
     */
    public void detachAllActivity() {
        for (int i = mActivitys.size() - 1; i >= 0; i--) {
            Activity activity = mActivitys.get(i);
            detachActivity(activity);
        }
    }

    /**
     * 獲取棧頂?shù)腁ctivity
     *
     * @return
     */
    public Activity getLastActivity() {
        return mActivitys.lastElement();
    }

    /**
     * 獲取指定類型的Activity
     *
     * @param activityClass
     * @return
     */
    public Activity getActivity(Class<?> activityClass) {
        for (Activity activity : mActivitys) {
            if (activity.getClass().getName().equals(activityClass.getName())) {
                return activity;
            }
        }
        return null;
    }

    /**
     * 獲取Activity棧的大小
     *
     * @return
     */
    public int getSize() {
        return mActivitys.size();
    }

}

完成AppManager 后我們只需要在應(yīng)用的 Application 中調(diào)用registerActivityLifecycleCallbacks() 方法監(jiān)聽Activity 的創(chuàng)建和銷毀即可;

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        AppManager.getInstance().attachActivity(activity);
    }
    // ...
    @Override
    public void onActivityDestroyed(Activity activity) {
        AppManager.getInstance().detachActivity(activity);
    }
});

volatile 關(guān)鍵字

上面我們講到 懶漢式 的時候,我們發(fā)現(xiàn) mInstance 對象有個 volatile 關(guān)鍵字修飾,這個關(guān)鍵字可能很多朋友都聽說過,這里我們簡單的分析下其功能:

  1. 防止指令重排;

JVM為了優(yōu)化執(zhí)行效率,對需要執(zhí)行的代碼(方法中的代碼)進(jìn)行指令重排,其結(jié)果不會影響我們單線程中對方法執(zhí)行的結(jié)果,但會打亂方法中沒有關(guān)聯(lián)語句的順序,例如:

// 情況1,存在第二行代碼在第一行代碼前面執(zhí)行的情況
int a = 0;
int b = 1;
// 情況2,此時 b 依賴于 a,第一行代碼會在第二行代碼之前執(zhí)行
int a = 0;
int b = a;

上述代碼中的第一種情況,在單線程中對代碼的結(jié)果沒有任何影響,但是如果是多線程的情況下就有可能出現(xiàn)問題,我們再來看一段偽代碼:

int value = 0;
volatile boolean flag = false;
// 假設(shè)下面的代碼在線程 a 中執(zhí)行
value = 1;
flag = true;
// 假設(shè)下面的代碼在線程 b 中執(zhí)行
while(!flag){
    sleep();
}
System.out.println("value" = value);

假設(shè) flag 變量沒有被 volatile 修飾,上述代碼出現(xiàn)指令重排的情況,線程 a 中的flag = true;優(yōu)先執(zhí)行了,那么線程 b 中的打印語句就有可能出現(xiàn) value = 0 的情況,使用 volatile 關(guān)鍵字可以避免指令重排,保證線程 a 中 falg = true; 會在 value = 1; 代碼后面執(zhí)行;

  1. 被此關(guān)鍵字定義的變量能夠保證線程的可見性;
    先來看下下面的這段代碼:
public class VolatieTest {
     public static void main(String[] args) {
           ThreadDemo td = new ThreadDemo();
           new Thread(td).start();
           while(true) {
                if(td.getFlag()) {
                     System.out.println("----------------");
                     break;
                }
           }
           System.out.println("while out");
     }
}

class ThreadDemo implements Runnable {

    // 這里 的 flag 如果沒有添加 volatile 關(guān)鍵字則 上述 的 main() 里面的while循環(huán)無法跳出
     private boolean flag = false;
     @Override
     public void run() {
           try {
                Thread.sleep(200);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }      
           flag = true;
           System.out.println("flag = " + flag);           
     }

     public boolean getFlag() {
           return flag;
     }
}

沒有增加 volatile 關(guān)鍵字時:

沒有增加 volatile 關(guān)鍵字

增加了 volatile 關(guān)鍵字時:

增加了 volatile 關(guān)鍵字

第一張圖沒有增加volatile 關(guān)鍵字, ThreadDemo 類里面的 flag 變量在子線程中更新了,在主線程中無法獲取到這個更新,因此主線程一直卡在 while 循環(huán)里面,第二張圖增加了 volatile 關(guān)鍵字,子線程中的 flag 變量一更新,主線程可以立刻知道 flag 這個變量有修改,因此跳出了 while 循環(huán);
關(guān)于 volatile 這里只是簡單的做了分析,想要了解更底層的原理的大家可以網(wǎng)上單獨(dú)找找資料;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容