前言
創(chuàng)新互聯(lián)建站公司2013年成立,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站制作、成都網(wǎng)站建設(shè)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元云浮做網(wǎng)站,已為上家服務(wù),為云浮各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:028-86922220
單例模式最初的定義出現(xiàn)于《設(shè)計(jì)模式》(艾迪生維斯理, 1994):“保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。”
而我對(duì)單例的理解是,在可控的范圍內(nèi)充當(dāng)全局變量的作用,就相當(dāng)于C語(yǔ)言中一個(gè)全局結(jié)構(gòu)體。
首先來(lái)看這樣一個(gè)單例,稍微有點(diǎn)經(jīng)驗(yàn)的同學(xué)可能都會(huì)說(shuō),這樣的單例是非線程安全的。要加個(gè)volatile關(guān)鍵字才可以。
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; } }
但是你要是問(wèn)他,為什么是非線程安全的單例就答不出來(lái)了。搞清楚這個(gè)問(wèn)題其實(shí) 對(duì)我們的多線程理解是很有好處的。
我們首先明確一下對(duì)于jvm來(lái)說(shuō),完成對(duì)一個(gè)變量的寫操作 到底是如何進(jìn)行的。
寫操作:
(1)先把值寫入cpu的高速緩存cache中。(2)然后再把這個(gè)cache中的值拷貝到ram(也就是我們的內(nèi)存)中。
注意啊,對(duì)于一個(gè)寫操作來(lái)說(shuō),這個(gè)(1)(2) 可不是原子操作,很有可能(1)執(zhí)行完畢以后,cpu又去干了其他事情,
并沒(méi)有第一時(shí)間把cache的值 寫入到ram中。而我們讀操作,都是從ram中去讀取一個(gè)值的。
所以這里我們可以想一下,如果是多線程場(chǎng)景的話,會(huì)有一些坑。
然后再說(shuō)一個(gè)概念,對(duì)于 singleton=new Singleton(); 這一條語(yǔ)句來(lái)說(shuō),他顯然不是一條指令就可以完成的。
正常情況來(lái)說(shuō),我們要完成這條語(yǔ)句涉及到的指令大約如下:
1.申請(qǐng)一段堆內(nèi)存空間
2.在這個(gè)堆內(nèi)存空間中把我們需要的對(duì)象初始化完畢
3.把singleton這個(gè)引用指向我們的堆內(nèi)存空間地址。
但是坑爹就坑爹在,虛擬機(jī)會(huì)有一個(gè)指令重排序的概念。當(dāng)虛擬機(jī)發(fā)現(xiàn)單線程下 指令的順序變更不會(huì)導(dǎo)致結(jié)果異常的時(shí)候
就會(huì)觸發(fā)指令重排序的機(jī)制, 他會(huì)導(dǎo)致上述的 123順序發(fā)生變更,比如我們把順序改成132 你就會(huì)發(fā)現(xiàn) 結(jié)果還是一樣的。
(指令重排序的觸發(fā)機(jī)制準(zhǔn)確的來(lái)說(shuō)是happens before原則 有興趣的同學(xué)可以深挖)
如果發(fā)生132的執(zhí)行順序 會(huì)發(fā)生什么?
假設(shè)線程a 進(jìn)入到了同步代碼塊中,這個(gè)時(shí)候觸發(fā)了指令重排序,順序變成132,假設(shè)cpu這個(gè)時(shí)候執(zhí)行了13。然后轉(zhuǎn)頭
去執(zhí)行線程b,線程b 進(jìn)入getInstance方法的時(shí)候,他發(fā)現(xiàn)singleton 不是null了,于是歡天喜地的return了,
但是要知道這個(gè)時(shí)候線程a的 2還沒(méi)執(zhí)行,也就是說(shuō)singleton雖然不是空,但是他指向的地址空間里面啥都沒(méi)有,對(duì)象還沒(méi)有初始化。所以這是一個(gè)非常大的隱患,雖然他發(fā)生的概率極低,低到我現(xiàn)在都沒(méi)有復(fù)現(xiàn)過(guò)這種現(xiàn)象,但是依舊有概率。
那么正確的寫法:
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; } }
有很多人就會(huì)說(shuō) volatile 這個(gè)關(guān)鍵字以后,singleton=new Singleton(); 就不會(huì)發(fā)生指令重排了,所以這么做是正確的。
現(xiàn)在明確的告訴你,上面這個(gè)觀點(diǎn)是錯(cuò)誤的
singleton=new Singleton();
這條語(yǔ)句背后的指令依舊有概率發(fā)生指令重排,只不過(guò) volatile修飾過(guò)以后,在 這條語(yǔ)句背后的指令完全執(zhí)行完畢以前,對(duì)singleton這個(gè)引用的讀操作全部被屏蔽了。
也就是說(shuō) 132的執(zhí)行順序依舊會(huì)發(fā)生,只不過(guò) 當(dāng)執(zhí)行完13 而2沒(méi)有執(zhí)行的時(shí)候,volatile修飾過(guò)的這個(gè)變量,所有對(duì)他的讀操作
都會(huì)暫時(shí)屏蔽,等待2操作執(zhí)行完以后,才會(huì)進(jìn)行讀操作。
這才是volatile關(guān)鍵字加上去以后的作用。
android很多代碼比如eventbus的單例就是用的上述寫法。
當(dāng)然了,上述寫法是典型的懶漢寫法,所謂懶漢你就理解成用的時(shí)候才實(shí)例化,不用的話不實(shí)例化。
但是如果你的需求是這個(gè)單例無(wú)論在什么情況下都會(huì)存在,你當(dāng)然可以寫成餓漢,餓漢的寫法更簡(jiǎn)單。
缺點(diǎn)就是他會(huì)一直占用內(nèi)存。餓漢寫法很多,我寫個(gè)最簡(jiǎn)單的:
class Singleton { //最簡(jiǎn)單的寫法就是這個(gè)了,直接public就行 public static final Singleton instance = new Singleton(); private Singleton() { } }
單例序列化會(huì)破壞對(duì)象唯一性嗎?
答案是會(huì)的:
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 { //最簡(jiǎn)單的寫法就是這個(gè)了,直接public就行 public static final Singleton instance = new Singleton(); private Singleton() { } // //這個(gè)方法就可以保證序列化和反序列化得到的對(duì)象是同一個(gè)了 // private Object readResolve() { // return instance; // } } }
代碼比較簡(jiǎn)單,大家可以測(cè)試一下,s1和s3就是2個(gè)不同的對(duì)象,但是如果把注釋掉的readResolve方法放開(kāi)的話,你就會(huì)發(fā)現(xiàn)
這個(gè)問(wèn)題解決了,序列化和反序列化是同一個(gè)對(duì)象了。
對(duì)外部公開(kāi)提供的sdk的單例要注意些什么?
尤其是對(duì)于很多金融安全類的sdk來(lái)說(shuō),如果你這個(gè)里面有單例的話,涉及到安全性要盡可能的不被業(yè)務(wù)方hook,
其中尤其要注意的就是 有人可能會(huì)利用反射來(lái)new一個(gè)對(duì)象,破壞單例
解決這個(gè)問(wèn)題也不難,
private Singleton() { //防止有人利用反射惡意修改 if (null != instance) { throw new RuntimeException("dont construct more!"); } }
項(xiàng)目中的單例太多,如何有效管理?
其實(shí)就拿map管理就可以了,android里面的 wms,ams 等等系統(tǒng)單例服務(wù)都是這樣的。你傳一個(gè)key進(jìn)去 返回一個(gè)單例給你。
這個(gè)真的很有用哦,特別是大型工程,可以有效管理單例,文檔輸出就簡(jiǎn)單許多。
static class SingletonManager { private static MapobjectMap = 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中使用單例還要注意些什么?
最主要的就是盡量不要利用單例模式存儲(chǔ)傳遞數(shù)據(jù),因?yàn)閍pp掛在后臺(tái)的時(shí)候進(jìn)程會(huì)容易被殺掉,如果回到前臺(tái)再取這個(gè)單例里的數(shù)據(jù)很容易就取到個(gè)null,所以android中寫單例的原則就是:
原則上不允許用單例模式傳遞數(shù)據(jù),如果一定要這么做,請(qǐng)考慮數(shù)據(jù)恢復(fù)現(xiàn)場(chǎng)。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)創(chuàng)新互聯(lián)的支持。