单例模式

介绍

单例设计模式(Singleton)用于保证一个类在整个程序中只有一个实例,通常我们会把设计为单例的类的构造设计成私有的,但不代表所有的单例模式的类的构造都是私有的;
本文的主要内容分为:

  1. 分析常见的单例形式;
  2. 使用懒汉式(DCL)实现一个 AppManager 管理工具类;
  3. 分析 volatile 关键字的作用;

常见的单例的形式

常见的单例设计模式的形式有:

  1. 饿汉式
    饿汉式的单例如果该类被加载了就会创建该单例对象,能够避免多线程并发的问题,但是如果我们的类被加载了(如果有其它的方式,如静态方法等),该单例对象就会被创建,因此 饿汉模式 不是我们的最优方式;
public class AppManager {
    // 在类被加载的时候就创建好了对象本身
    private static AppManager mInstance = new AppManager();
    // 私有化构造器
    private AppManager() {}
    // 提供一个外部访问单例的方法
    public static AppManager getInstance() {
        return mInstance;
    }
}
  1. 懒汉式(DCL)
    这里就不做懒汉形式的线程不安全到线程安全的铺垫了,我们直接对线程安全的懒汉形式进行分析,懒汉式的单例模式是在调用 getInstance() 方法的时候去判断单例的对象是不是为空,此时为了防止多线程并发的问题,我们需要进行同步锁的二次判空,关于下面代码中的 volatile 关键字我们在文章的最后进行解析;
    懒汉式既能在多线程环境中很好的工作,也能够保证在我们需要用到单例对象的时候创建对象,因此是我们用的最多的单例形式;
public class AppManager {
    // 创建一个静态的引用,但是不创建对象
    // 这里的 volatile 关键字在本文最后会进行分析
    private volatile static AppManager mInstance = null;
    // 私有化构造
    private AppManager() {}
    // 提供一个外部访问单例的方法
    public static AppManager getInstance() {
        // 第一重判断对象是不是为空,只要调用该方法就会判断
        if (mInstance == null) {
            // 第二重判断对象是不是为空,这里将当前类的 Class 对象
            // 作为同步锁,保证不同线程的执行顺序
            synchronized (AppManager.class) {
                if (mInstance == null) {
                    mInstance = new AppManager();
                }
            }
        }
        return mInstance;
    }
}
  1. 静态内部类形式
    该种方式是利用静态类只会加载一次的机制达到单例的效果,此种形式加载也能够达到懒加载的效果,静态内部类形式的单例模式是推荐使用的单例模式;
public class AppManager {
    // 创建一个静态内部类,该静态内部类持有一个单例类的静态成员变量
    private static class Holder {
        private static final AppManager INSTANCE = new AppManager();
    }
    // 私有化构造
    private AppManager() {}
    // 提供一个外部访问单例的方法
    public static AppManager getInstance() {
        return Holder.INSTANCE;
    }
}
  1. 容器式
    容器式的单例模式我们平常自己写的代码中并没有使用的很多,但是我们查看Android的源码的时候,经常能够看到它的身影,例如我们获取加载布局资源的 LayoutInflater 对象,实际上我们并不是通过 LayoutInflater 类本身来获取,而是通过系统的容器来获取 LayoutInflater 对象,容器式的核心思想是通过一个容器(例如: 'HashMap'),将我们需要的对象缓存起来,如果需要用到该对象的时候,我们只需要通过容器获取即可,因此此种设计模式对应类的构造大概率不是私有的;
  2. 枚举
    我们平常使用的枚举形式其实也是单例设计模式的一种,但是由于枚举会占用比较多的内存开销,因此不推荐使用枚举来实现单例模式;

AppManager Activity管理工具类

在项目开发的过程中,我们需要对打开的 Activity 对象进行有效的管理,例如我们收到一个推送需要立即显示一个 Dialog 弹窗就需要获取当前显示的 Activity 等,接下来我们用 懒汉模式(DCL) 实现一个 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;
        }
    }

    /**
     * 结束栈顶的Activity
     */
    public void detachLastActivity() {
        if (mActivitys.isEmpty()) {
            return;
        }
        detachActivity(mActivitys.lastElement());
    }

    /**
     * 根据 Class 对象结束单个 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;
            }
        }
    }

    /**
     * 根据 Class 对象结束一类 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);
        }
    }

    /**
     * 获取栈顶的Activity
     *
     * @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 后我们只需要在应用的 Application 中调用registerActivityLifecycleCallbacks() 方法监听Activity 的创建和销毁即可;

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 关键字

上面我们讲到 懒汉式 的时候,我们发现 mInstance 对象有个 volatile 关键字修饰,这个关键字可能很多朋友都听说过,这里我们简单的分析下其功能:

  1. 防止指令重排;

JVM为了优化执行效率,对需要执行的代码(方法中的代码)进行指令重排,其结果不会影响我们单线程中对方法执行的结果,但会打乱方法中没有关联语句的顺序,例如:

// 情况1,存在第二行代码在第一行代码前面执行的情况
int a = 0;
int b = 1;
// 情况2,此时 b 依赖于 a,第一行代码会在第二行代码之前执行
int a = 0;
int b = a;

上述代码中的第一种情况,在单线程中对代码的结果没有任何影响,但是如果是多线程的情况下就有可能出现问题,我们再来看一段伪代码:

int value = 0;
volatile boolean flag = false;
// 假设下面的代码在线程 a 中执行
value = 1;
flag = true;
// 假设下面的代码在线程 b 中执行
while(!flag){
    sleep();
}
System.out.println("value" = value);

假设 flag 变量没有被 volatile 修饰,上述代码出现指令重排的情况,线程 a 中的flag = true;优先执行了,那么线程 b 中的打印语句就有可能出现 value = 0 的情况,使用 volatile 关键字可以避免指令重排,保证线程 a 中 falg = true; 会在 value = 1; 代码后面执行;

  1. 被此关键字定义的变量能够保证线程的可见性;
    先来看下下面的这段代码:
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 关键字则 上述 的 main() 里面的while循环无法跳出
     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 关键字时:

没有增加 volatile 关键字

增加了 volatile 关键字时:

增加了 volatile 关键字

第一张图没有增加volatile 关键字, ThreadDemo 类里面的 flag 变量在子线程中更新了,在主线程中无法获取到这个更新,因此主线程一直卡在 while 循环里面,第二张图增加了 volatile 关键字,子线程中的 flag 变量一更新,主线程可以立刻知道 flag 这个变量有修改,因此跳出了 while 循环;
关于 volatile 这里只是简单的做了分析,想要了解更底层的原理的大家可以网上单独找找资料;

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

推荐阅读更多精彩内容