Android中单例模式的一些坑小结
作者:DK_BurNIng 发布时间:2021-11-02 22:33:11
前言
单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
而我对单例的理解是,在可控的范围内充当全局变量的作用,就相当于C语言中一个全局结构体。
首先来看这样一个单例,稍微有点经验的同学可能都会说,这样的单例是非线程安全的。要加个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中写单例的原则就是:
原则上不允许用单例模式传递数据,如果一定要这么做,请考虑数据恢复现场。
来源:https://juejin.im/post/5c623fdef265da2dd16846b8
猜你喜欢
- 本文实例讲述了C#预处理器指令的用法。分享给大家供大家参考。具体用法分析如下:C#预处理器指令是在编译时调用的。预处理器指令(preproc
- 1.需求WPF本身没有直接把点集合绘制成曲线的函数。可以通过贝塞尔曲线函数来绘制。贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩
- public class ReflexTest { public static void main(St
- 如今APP越来越多,我们每天所使用的的软件也越来越多,可是在我们不付费的情况下,App制造商如何实现,实现收入甚至是盈利呢?答案就是在我们打
- 使用正则抓捕网上邮箱这就是我们需要抓捕的网站。实现思路:1、使用java.net.URL对象,绑定网络上某一个网页的地址2、通过java.n
- Model与Session区别什么是Session:Session:在计算机中,尤其是在网络应用中,称为“会话”。它具体是指一个终端用户与交
- 方案实施1、 spring和ehcache集成主要获取ehcache作为操作ehcache的对象。spring.xml中注入ehcacheM
- 前言反射是我们框架的灵魂,反射也是我们框架的一个底层基石,没有反射也就没有框架,如果我们学好了反射,对我们阅读框架底层是有很大班助的——阿俊
- JAVA是面向对象的语言,开发者在操作数据的时候,通常更习惯面对一个特定类型的对象,如一个用户就是一个User类的对象。DAO层需要做的,就
- Android 使用FragmentTabhost代替Tabhost前言:现在Fragment使用越来越广了,虽然Fragment寄生在Ac
- 本文提纲版本约定JDK:8Servlet:4.xtomcat:9.x✍正文什么样的答案终身难忘?学生时代关于记忆经常能听见两种论调:死记硬背
- 背景客户使用我们系统的时候,查询不带任何查询条件,查询就返回全部数据,500多万条数据啊,然后直接导出,数据量庞大,接口超时,这可苦了我们这
- 目前为止我们已经了解了如何通过编程创建 CompletableFuture 对象以及如何获取返回值,虽然看起来这些操作已经比较方便,但还有进
- 1.引言在操作应用的时候,会有很多不同的手势操作,如按下、单击、双击、长按等手势,我们可以在这些手势事件中添加相应的业务逻辑,那么如何检测不
- 最近,Oracle 宣布 Java 14(或 Oracle JDK 14)公开可用。如果你想进行最新的实验或者开发的话,那么你可以试试在 L
- 在学习Spring的过程中,留下一下痕迹。代理模式,其实就是让别人做同样的事情,但是别人却不仅将我的事情做了,还会把他的事情也做了,换言之,
- 1.@RequestMapping的介绍通过@RequestMapping,我们可以把请求地址和方法进行绑定的,可以在类、方法上进行声明。类
- 今天是开篇,得要吹一下算法,算法就好比程序开发中的利剑,所到之处,刀起头落。 针对现实中的排序问题,算法有七把利剑可以助你马道成功
- Java公众号【Java技术迷】一个在互联网领先地位,微信搜索【Java技术迷】第一时间阅读最新文章,通过下面下载链接,即可获得我精心整理的
- Java调用Oracle存储过程详解步骤:1、编写Oracle存储过程2、编写数据库获取连接工具类3、编写简单应用调用存储过程实现:1、Or