首先来看这样一个单例,稍微有点经验的同窗可能都会说,这样的单例是非线程安全的。要加个volatile关键字才能够。java
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;
}
}
复制代码
可是你要是问他,为何是非线程安全的单例就答不出来了。搞清楚这个问题其实 对咱们的多线程理解是颇有好处的。android
咱们首先明确一下对于jvm来讲,完成对一个变量的写操做 究竟是如何进行的。缓存
写操做: (1)先把值写入cpu的高速缓存cache中。(2)而后再把这个cache中的值拷贝到ram(也就是咱们的内存)中。安全
注意啊,对于一个写操做来讲,这个(1)(2) 可不是原子操做,颇有可能(1)执行完毕之后,cpu又去干了其余事情, 并无第一时间把cache的值 写入到ram中。而咱们读操做,都是从ram中去读取一个值的。bash
因此这里咱们能够想一下,若是是多线程场景的话,会有一些坑。多线程
而后再说一个概念,对于 singleton=new Singleton(); 这一条语句来讲,他显然不是一条指令就能够完成的。app
正常状况来讲,咱们要完成这条语句涉及到的指令大约以下:jvm
1.申请一段堆内存空间测试
2.在这个堆内存空间中把咱们须要的对象初始化完毕ui
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来讲,若是你这个里面有单例的话,涉及到安全性要尽量的不被业务方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);
}
}
复制代码
最主要的就是尽可能不要利用单例模式存储传递数据,由于app挂在后台的时候进程会容易被杀掉,若是回到前台再取这个单例里的 数据很容易就取到个null,因此android中写单例的原则就是:
原则上不容许用单例模式传递数据,若是必定要这么作,请考虑数据恢复现场。