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

标签:android,单例模式,坑
0
投稿

猜你喜欢

  • Android优化提升应用启动速度及Splash页面的设计

    2022-02-18 06:02:27
  • adb通过wifi连接android设备流程解析

    2021-12-27 08:39:37
  • Android自定义ProgressBar实现漂亮的进度提示框

    2023-04-04 22:05:30
  • Android运用BroadcastReceiver实现强制下线

    2021-07-20 19:49:29
  • Spring源码完美导入IDEA的过程

    2023-05-13 14:11:50
  • springboot整合JSR303参数校验与全局异常处理的方法

    2023-10-06 01:31:40
  • 简单了解JavaCAS的相关知识原理

    2023-03-25 12:09:31
  • 基于Spring概念模型:PathMatcher 路径匹配器

    2022-08-20 12:52:38
  • Java设计模式之代理模式(Proxy模式)介绍

    2023-04-27 16:01:34
  • Android播放assets文件里视频文件相关问题分析

    2021-08-10 20:04:41
  • Android Loader的使用以及手机通讯录的获取方法

    2023-12-22 02:10:05
  • Unity shader实现高斯模糊效果

    2023-11-17 20:23:45
  • 基于Unity制作一个简易的计算器

    2023-02-18 10:02:39
  • 基于java构造方法Vector修改元素源码分析

    2023-11-25 10:54:56
  • 详解C语言结构体中的函数指针

    2023-11-21 13:04:33
  • Android通过wifi连接手机(不需要root)

    2023-10-12 00:33:23
  • C# 实现简易的串口监视上位机功能附源码下载

    2023-10-12 11:12:19
  • java如何消除太多的if else判断示例代码

    2023-01-17 21:57:40
  • SpringBoot2.0集成MQTT消息推送功能实现

    2022-02-28 01:55:04
  • Java8 Optional原理及用法解析

    2022-04-22 16:37:58
  • asp之家 软件编程 m.aspxhome.com