Android中单例模式的几个坑

栏目: IOS · Android · 发布时间: 7年前

内容简介:首先来看这样一个单例,稍微有点经验的同学可能都会说,这样的单例是非线程安全的。要加个volatile关键字才可以。但是你要是问他,为什么是非线程安全的单例就答不出来了。搞清楚这个问题其实 对我们的多线程理解是很有好处的。我们首先明确一下对于jvm来说,完成对一个变量的写操作 到底是如何进行的。

首先来看这样一个单例,稍微有点经验的同学可能都会说,这样的单例是非线程安全的。要加个volatile关键字才可以。

class Singleton{
        private static  Singleton singleton;
        private Singleton(){};
        public static Singleton getInstance()
        {
            if (singleton==null)
            {
                synchronized (Singleton.class)
                {
                    if (singleton==null)
                    {
                        singleton=new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
复制代码

但是你要是问他,为什么是非线程安全的单例就答不出来了。搞清楚这个问题其实 对我们的多线程理解是很有好处的。

我们首先明确一下对于jvm来说,完成对一个变量的写操作 到底是如何进行的。

写操作: (1)先把值写入cpu的高速缓存cache中。(2)然后再把这个cache中的值拷贝到ram(也就是我们的内存)中。

注意啊,对于一个写操作来说,这个(1)(2) 可不是原子操作,很有可能(1)执行完毕以后,cpu又去干了其他事情, 并没有第一时间把cache的值 写入到ram中。而我们读操作,都是从ram中去读取一个值的。

所以这里我们可以想一下,如果是多线程场景的话,会有一些坑。

然后再说一个概念,对于 singleton=new Singleton(); 这一条语句来说,他显然不是一条指令就可以完成的。

正常情况来说,我们要完成这条语句涉及到的指令大约如下:

1.申请一段堆内存空间

2.在这个堆内存空间中把我们需要的对象初始化完毕

3.把singleton这个引用指向我们的堆内存空间地址。

但是坑爹就坑爹在,虚拟机会有一个 指令重排序 的概念。当虚拟机发现单线程下 指令的顺序变更不会导致结果异常的时候 就会触发指令重 排序 的机制, 他会导致上述的 123顺序发生变更,比如我们把顺序改成132 你就会发现 结果还是一样的。 (指令重排序的触发机制准确的来说是happens before原则 有兴趣的同学可以深挖)

如果发生132的执行顺序 会发生什么?

假设线程a 进入到了同步代码块中,这个时候触发了指令重排序,顺序变成132,假设cpu这个时候执行了13。然后转头 去执行线程b,线程b 进入getInstance方法的时候,他发现singleton 不是null了,于是欢天喜地的return了, 但是要知道这个时候线程a的 2还没执行,也就是说singleton虽然不是空,但是他指向的地址空间里面啥都没有,对象还没有初始化。所以这是一个非常大的隐患,虽然他发生的概率极低,低到我现在都没有复现过这种现象,但是依旧有概率。

那么正确的写法:

class Singleton{
        private static volatile Singleton singleton;
        private Singleton(){};
        public static Singleton getInstance()
        {
            if (singleton==null)
            {
                synchronized (Singleton.class)
                {
                    if (singleton==null)
                    {
                        singleton=new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

复制代码

有很多人就会说 volatile 这个关键字以后,singleton=new Singleton(); 就不会发生指令重排了,所以这么做是正确的。

现在明确的告诉你,上面这个观点是错误的

singleton=new Singleton(); 这条语句背后的指令依旧有概率发生指令重排,只不过 volatile修饰过以后,在 这条语句背后的 指令完全执行完毕以前,对singleton这个引用的读操作全部被屏蔽了。

也就是说 132的执行顺序依旧会发生,只不过 当执行完13 而2没有执行的时候,volatile修饰过的这个变量,所有对他的读操作 都会暂时屏蔽,等待2操作执行完以后,才会进行读操作。

这才是volatile关键字加上去以后的作用。

android很多代码比如eventbus的单例就是用的上述写法。

当然了,上述写法是典型的懒汉写法,所谓懒汉你就理解成用的时候才实例化,不用的话不实例化。

但是如果你的需求是这个单例无论在什么情况下都会存在,你当然可以写成饿汉,饿汉的写法更简单。

缺点就是他会一直占用内存。饿汉写法很多,我写个最简单的:

class Singleton {
        //最简单的写法就是这个了,直接public就行
        public static final Singleton instance = new Singleton();

        private Singleton() {
        }

    }
复制代码

单例序列化会破坏对象唯一性吗?

答案是会的:

package com.wuyue.test;

import java.io.*;

/**
 * Created by 16040657 on 2019/2/12.
 */
public class Test2 {


    public static void main(String args[]) {

        Singleton s1 = Singleton.instance;

        File f = new File("../test.txt");
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
            oos.writeObject(s1);
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
            Singleton s3 = (Singleton) ois.readObject();

            System.out.println("s1==s3:" + (s1 == s3));

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }


    }

    static class Singleton implements Serializable {
        //最简单的写法就是这个了,直接public就行
        public static final Singleton instance = new Singleton();

        private Singleton() {
        }

//        //这个方法就可以保证序列化和反序列化得到的对象是同一个了
//        private Object readResolve() {
//            return instance;
//        }

    }
}

复制代码

代码比较简单,大家可以测试一下,s1和s3就是2个不同的对象,但是如果把注释掉的readResolve方法放开的话,你就会发现 这个问题解决了,序列化和反序列化是同一个对象了。

对外部公开提供的sdk的单例要注意些什么?

尤其是对于很多金融安全类的sdk来说,如果你这个里面有单例的话,涉及到安全性要尽可能的不被业务方hook, 其中尤其要注意的就是 有人可能会利用反射来new一个对象,破坏单例

解决这个问题也不难,

private Singleton() {
            //防止有人利用反射恶意修改
            if (null != instance) {
                throw new RuntimeException("dont construct more!");
            }

        }
复制代码

项目中的单例太多,如何有效管理?

其实就拿map管理就可以了,android里面的 wms,ams 等等系统单例服务都是这样的。你传一个key进去 返回一个单例给你。

这个真的很有用哦,特别是大型工程,可以有效管理单例,文档输出就简单许多。

static class SingletonManager {
        private static Map<String, Object> objectMap = new HashMap<>();

        private SingletonManager() {
        }

        public static void registerService(String key, Object ins) {
            if (!objectMap.containsKey(key)) {
                objectMap.put(key, ins);
            }
        }

        public static Object getService(String key) {
            return objectMap.get(key);
        }

    }

复制代码

android中使用单例还要注意些什么?

最主要的就是尽量不要利用单例模式存储传递数据,因为app挂在后台的时候进程会容易被杀掉,如果回到前台再取这个单例里的 数据很容易就取到个null,所以android中写单例的原则就是:

原则上不允许用单例模式传递数据,如果一定要这么做,请考虑数据恢复现场。


以上所述就是小编给大家介绍的《Android中单例模式的几个坑》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

Speed Up Your Site

Speed Up Your Site

Andrew B. King / New Riders Press / 2003-01-14 / USD 39.99

There's a time bomb on the web: user patience. It starts ticking each time someone opens one of your pages. You only have a few seconds to get compelling content onto the screen. Fail, and you can kis......一起来看看 《Speed Up Your Site》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具