首先來看這樣一個單例,稍微有點經驗的同窗可能都會說,這樣的單例是非線程安全的。要加個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中寫單例的原則就是:
原則上不容許用單例模式傳遞數據,若是必定要這麼作,請考慮數據恢復現場。