介紹
單例設(shè)計模式(Singleton)用于保證一個類在整個程序中只有一個實例,通常我們會把設(shè)計為單例的類的構(gòu)造設(shè)計成私有的,但不代表所有的單例模式的類的構(gòu)造都是私有的;
本文的主要內(nèi)容分為:
- 分析常見的單例形式;
- 使用懶漢式(DCL)實現(xiàn)一個
AppManager管理工具類; - 分析
volatile關(guān)鍵字的作用;
常見的單例的形式
常見的單例設(shè)計模式的形式有:
- 餓漢式
餓漢式的單例如果該類被加載了就會創(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;
}
}
- 懶漢式(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;
}
}
- 靜態(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;
}
}
- 容器式
容器式的單例模式我們平常自己寫的代碼中并沒有使用的很多,但是我們查看Android的源碼的時候,經(jīng)常能夠看到它的身影,例如我們獲取加載布局資源的LayoutInflater對象,實際上我們并不是通過LayoutInflater類本身來獲取,而是通過系統(tǒng)的容器來獲取LayoutInflater對象,容器式的核心思想是通過一個容器(例如: 'HashMap'),將我們需要的對象緩存起來,如果需要用到該對象的時候,我們只需要通過容器獲取即可,因此此種設(shè)計模式對應(yīng)類的構(gòu)造大概率不是私有的; - 枚舉
我們平常使用的枚舉形式其實也是單例設(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)鍵字可能很多朋友都聽說過,這里我們簡單的分析下其功能:
- 防止指令重排;
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í)行;
- 被此關(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)鍵字, ThreadDemo 類里面的 flag 變量在子線程中更新了,在主線程中無法獲取到這個更新,因此主線程一直卡在 while 循環(huán)里面,第二張圖增加了 volatile 關(guān)鍵字,子線程中的 flag 變量一更新,主線程可以立刻知道 flag 這個變量有修改,因此跳出了 while 循環(huán);
關(guān)于 volatile 這里只是簡單的做了分析,想要了解更底層的原理的大家可以網(wǎng)上單獨(dú)找找資料;