雄安新區(qū)做網(wǎng)站公司東莞整站優(yōu)化推廣公司找火速
單例模式的概念
單例模式是指的是整個系統(tǒng)生命周期內(nèi),保證一個類只能產(chǎn)生一個實例對象
保證類的唯一性 。
通過一些編碼上的技巧,使編譯器可以自動發(fā)現(xiàn)咱們的代碼中是否有多個實例,并且在嘗試創(chuàng)建多個實例的時候,直接編譯出錯。
應(yīng)用場景:數(shù)據(jù)庫連接池、多線程線程池 windows回收站
單例模式特點
單一實例:單例模式確保一個類只有一個實例。
全局訪問點:單例模式提供了一個全局的訪問點來獲取這個唯一的實例。
延遲初始化:單例模式通常實現(xiàn)延遲初始化,即在實際需要實例之前不會創(chuàng)建實例,這樣可以節(jié)省系統(tǒng)資源。
線程安全:在多線程環(huán)境中,單例模式需要確保即時在高并發(fā)的情況下也能保持實例的唯一性。
單例模式實現(xiàn)方式
單例模式有很多種實現(xiàn)方式,包括餓漢式和懶漢式 。
懶漢式會涉及到線程安全問題 可以使用加鎖的方式可以解決線程安全。
而餓漢式就不會有線程安全問題
下面我們用代碼來分別實現(xiàn)一下餓漢 和懶漢的單例模式:
package thread;
//單例模式 懶漢模式的實現(xiàn)class SingletonLazy {private static SingletonLazy instance = null;private static Object locker = new Object();public static SingletonLazy getInstance() {// if (instance == null) {//synchronized (locker) {if (instance == null) {instance = new SingletonLazy();}// }// }return instance;}public SingletonLazy() {}
}
public class Demo28 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}
}
package thread;
//單例模式 懶漢模式的實現(xiàn)class SingletonLazy {private static SingletonLazy instance = null;private static Object locker = new Object();public static SingletonLazy getInstance() {// if (instance == null) {//synchronized (locker) {if (instance == null) {instance = new SingletonLazy();}// }// }return instance;}public SingletonLazy() {}
}
public class Demo28 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}
}
單例模式多線程環(huán)境下的安全問題
在多線程中 餓漢式單例模式是線程安全的 而 懶漢式單例模式是線程不安全的
為什么呢 因為餓漢式寫法 創(chuàng)建實例的時機是在Java線程啟動(比main調(diào)用還早的時機)再后續(xù)線程執(zhí)行獲取對象的時候 意味著實例早就已經(jīng)存在了 每個線程的獲取操作就做了一件事 讀取代碼中靜態(tài)變量的值
多個線程讀取同一個變量的值 線程是安全的
懶漢式 則涉及到讀和修改操作 就是要先判斷instance里面的引用地址是否為空 為空才修改 多線程環(huán)境下可能就會產(chǎn)生bug
像上面圖片這種執(zhí)行順序就會出現(xiàn)線程安全問題 就不止一個實例了 就不符合我們單例模式的初衷了。
那怎么辦呢 我們最容易想到處理的方式就是加鎖了 那要怎么加鎖呢
比如這樣加鎖:
這樣很明顯還是線程不安全:
因為這個鎖就相當(dāng)于沒加 兩個線程還是會new兩個對象 那怎么辦呢 我們可不可以把if判斷操作和new操作打包成一個原子 答案是當(dāng)然可以。
像上面這樣加鎖 t1執(zhí)行加鎖之后 ,t2就會阻塞等待 直到t1釋放鎖(new完了)t2才拿到鎖,才能進(jìn)行條件判讀 t2判斷的時候instance早就非空了 ,也就不會再new了。
但是現(xiàn)在還存在一個問題 就是我們上面那種加鎖雖然解決了線程安全問題 但是這樣設(shè)計鎖 每次調(diào)用那個getinstance方法,就需要先加鎖,再執(zhí)行后續(xù)操作。 但是懶漢模式只是一開始調(diào)用的時候存在線程安全問題 ,一旦實例創(chuàng)建好了,后續(xù)再調(diào)用就只是讀取操作了 ,就不存在線程安全問題
但是我們這樣加鎖就會出現(xiàn)后面都沒有線程安全問題了 但是我們還在加鎖,這就有點畫蛇添足了。因為鎖本身也是有開銷的可能會使線程阻塞。
那怎么辦呢 我們可以引入雙重if判定
在上面這種加鎖方式下 首先我們要先判斷一下是否需要加鎖 實例化之后線程安全了就不用加鎖了 實例化之前就應(yīng)該加鎖 在兩個if判斷之間,synchronized會使線程阻塞等待 阻塞過程其他線程會修改instance的值
下面我們來畫個時間軸來解釋:
當(dāng)t2在進(jìn)去第一個if條件之后就會阻塞等待 等到t1釋放鎖 現(xiàn)在instace已經(jīng)不為null了 t2的第二個if條件也是進(jìn)不去的 后面不為空了 鎖就不用加了。
這樣就解決了沒有線程安全也加鎖的情況了。
但是現(xiàn)在還有一個問題 就是內(nèi)存可見性引起的線程安全問題 就相當(dāng)于
t1線程修改了instance引用,t2有可能讀不到(不過這種概率應(yīng)該很小)為了避免這種情況的發(fā)生 我們還要加上volatile關(guān)鍵字
這個關(guān)鍵字還可以解決指令重排序問題
指令重排序
指令重排序也是編譯器的一種優(yōu)化策列 按照正常來說你寫一段代碼 cpu應(yīng)該使按照順序一條一條執(zhí)行的 ,但是編譯器就比較智能,會根據(jù)實際情況生成二進(jìn)制指令的執(zhí)行順序,和你最初寫的代碼的順序可能會存在差別
調(diào)整順序最主要的目的就是為了提高效率,但是在保證邏輯是等價的。
上面執(zhí)行這個代碼會有三條指令
1、申請內(nèi)存空間
2、調(diào)用構(gòu)造方法(對內(nèi)存空間進(jìn)行初始化)
3、把此時內(nèi)存空間的地址,賦值給instance引用
在多線程環(huán)境下 如果執(zhí)行順序是1 3 2 就會 出現(xiàn)線程安全問題
如果3指令比2指令先執(zhí)行就會出現(xiàn)返回未初始化完畢的對象
就相當(dāng)于t1線程執(zhí)行完 instance就不是null了 但其實他是一個為初始化的對象 到時候t2線程執(zhí)行的時候instance引用已經(jīng)不是空的了 就進(jìn)不去 就直接返回instance 了 返回了一個沒有初始化完畢的對象 。這樣就會導(dǎo)致很嚴(yán)重的線程安全問題 所以我們要加上volatile關(guān)鍵字 這樣就很好的解決了指令重排序引起的線程安全問題。
總結(jié)
上面就是單例模式的相關(guān)實現(xiàn)和線程安全問題 當(dāng)然單例模式還有很多延伸問題 怎么解決反射下能夠保證是單例的模式 即使使用反射也不能破壞單例模式的唯一性呢 那可能就要用到枚舉的實現(xiàn) 。但是我們上面講的單例模式的內(nèi)容 是經(jīng)常用到的 在面試中也是會經(jīng)常問到的 一般HR會讓你現(xiàn)場寫一個單例模式 你應(yīng)該一步一步寫 先不考慮線程安全問題 ,等著HR問你 你再慢慢一步一步加上解決的實現(xiàn)方法 。
謝謝大家的瀏覽 !!!