【多线程经典实例】实现一个线程安全的单例模式
文章目錄
- 1.什么是單例模式
- 2. 單例模式的組成
- 3.餓漢模式實例
- 3.1在餓漢模式中為什么在創建實例的時候使用static修飾?
- 3.2 判斷該實例是否是線程安全的
- 4.懶漢模式實例
- 4.1 判斷該實例是否是線程安全的,如果不是線程安全的,那么怎樣修改可以成為線程安全的實例
- 總結一下:
1.什么是單例模式
單例模式是設計模式中的一種,其實設計模式就好好比是一個棋譜,我們在日常下棋的時候會有一些經典的套路。那么在設計模式中也有這樣的經典套路。這些經典的套路都是有大佬前輩們實現的。我們在寫代碼的時候,有很多經典的場景,在經典場景中有一些經典的應對套路。大佬們把這些常見的應對手段給整理起來,就起來個名字–設計模式。有了設計模式,無論是新手程序員還是資深的老程序員,都會有一個代碼編程規范。以便讓初出茅廬的新手,代碼不至于寫的很糟糕。
2. 單例模式的組成
單例模式分為:餓漢模式和懶漢模式。單例模式之所以被稱為單例模式,是因為我們在創建單例模式類的時候,就把該類的構造方法使用private進行修飾,以便在該類外,不能直接創建出一個實例。
3.餓漢模式實例
餓漢模式:指的是在單例模式中,在對單例進行初始化的時候,直接賦予單例實例,直接new出一個對象。
餓漢模式也可以這樣理解:我們平時在自己家里的時候,都洗過碗吧。就比如說,中午這頓飯使用了4個碗,在吃完飯后,我們立即就把4個碗給刷了。這里之所以被稱為餓漢模式是因為,在餓漢模式中,創建實例的時候比較著急。在初始化的時候,直接創建實例。
餓漢模式代碼案例:
//創建出一個單例模式//單例模式分為餓漢模式和懶漢模式 class Singleton{//在餓漢模式中,在進行初始化的時候,直接創建出實例public static Singleton instance = new Singleton();//使用private 修飾該類的構造方法,在類外無法創建出一個該類的對象private Singleton(){}//在類外調用該類中的實例public static Singleton getInstance(){return instance;} } public class TestDemo2 {public static void main(String[] args) {//Singleton singleton = new Singleton();Singleton single = Singleton.getInstance();} }我們嘗試在main類中,自己創建出一個SingleTon的實例。
3.1在餓漢模式中為什么在創建實例的時候使用static修飾?
因為 static 修飾的成員更準確的說是類成員,類屬性、類方法,不加 static 修飾的成員準確的來說,就是實例方法,實例成員,實例屬性。
在一個java程序中,一個類方法只會存在一份(JVM保證的) 這也就是為什么要使用static對實例進行修飾的原因。進一步的就保證了在類的static 成員也只會存在一份。
在這里我們在深究一個static 關鍵字
其實在我們使用的編程語言java中,static表示的意思和這個單詞的字面意思完全不同,static 的意思大家知道 是靜態的。這其實是一個歷史遺留問題。
在C語言中的static有3個作用:
-
修飾局部變量,把局部變量的生命周期變長。,修飾一個全局變量,把這個全局變量的作用域限制到整個.c文件。
-
修飾一個函數,把這個函數的作用域限制到整個.c文件。
我們在這里也可以看出在c語言中static 關鍵字的英語本意和在c語言中的使用效果,也是對不上號的。
其實在上古時期,那時候的static是表示把變量放到靜態內存區中,于是引入了static關鍵字,但是隨著計算機的發展,這個東西就逐漸的沒落了。但是static 關鍵字有被賦予了新的功能。
在C++中 static關鍵字除了上述C語言的static 功能之外還有新的用法,修飾一個類的成員變量和成員函數,此處static 修飾的成員就表示為類成員。
Java語言就是把C++中static 的功能繼承過來了而已。
既然static 關鍵字的本意和它的對應效果對不上號,那么為什么不使用其他的詞呢?
在一個編程語言中,要想新增一個關鍵字,是一件非常有風險的事情。因為不能還在程序中的單詞重合。
SingleTon.class 類對象,就是.class文件被JVM加載到內存中,表現出來的模樣。類對象就有著.class文件的所有信息。就像類名,屬性等都可以有SingleTon.class中找到。這樣也就實現了反射
3.2 判斷該實例是否是線程安全的
餓漢模式是線程安全的
那么為什么餓漢模式樣式的單例模式是線程安全的呢?我們在程序的哪里判斷該單例模式是線程安全的?
線程安不安全,具體是在多線程環境之下,并發調用的getInstance()方法是否會產生bug?
在博主的上一篇文章中,介紹了產生線程不安全的案例。
造成線程不安全的案例有5種。
我們現在回顧在餓漢模式中的getInstance()方法,在該方法中只有一個return操作,就是對一個變量進行了讀取,符合針對變量操作的原子性。所以是線程安全的
4.懶漢模式實例
懶漢模式創建懶漢模式的單例模式的時候,我們不著急創建出實例。
還是那洗碗舉例,我們中午吃飯的時候,使用了4個碗,吃完飯后,我們不著急洗碗,到了晚上吃飯的時候,需要使用的幾個碗,那么我們現在就洗幾個碗。假如說我們晚上要使用2個碗,那么就洗兩個,剩下的兩個碗不管。
在我們平時生活中,餓漢模式比懶漢模式好,因為你試一試中午吃完飯不洗碗,如果不洗碗肯定會被挨罵。但是在我們的計算機中可未必,懶漢模式要比餓漢模式要好一些。
那么為什么在計算機中懶漢模式要比餓漢模式要好一些呢?
就比如說,現在有一個1G的圖片文件,如果按照餓漢模式,那么計算機就會在內存中一下把這1G大的圖片文件全部加載出來,這不就耗費CUP資源,并且如果計算機用戶在瀏覽圖片文件的時候,就看了一點沒有全部把圖片文件瀏覽完。那么這樣的餓漢模式不就是費力不討好好嗎?
反而看看我們的懶漢模式,之所以懶,是因為你趕它一些它走一下。在懶漢模式中,我們一次不會再內存中把所有的圖片加載完,而是把計算機用戶的一個計算機屏幕中的圖片加載出來。用戶滑動一下滾動條,加載一下。這樣就進行了優化。
其實在計算機中懶漢模式是褒義詞,但是在現實世界中就算了吧😂
class Singleton2{//懶漢模式,在該模式中不著急創建出實例,在類外需要的時候,我們再進行創建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){if(instance == null){instance = new Singleton2();}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }4.1 判斷該實例是否是線程安全的,如果不是線程安全的,那么怎樣修改可以成為線程安全的實例
首先上述的所謂的懶漢模式的單例模式不是線程安全的
那么為什么它不是線程安全的呢?
因為在多線程中,我們調用懶漢模式中的getInstance()方法的時候,針對變量的操作不是原子的,那么有從哪可以看出不是原子的呢?
如圖:
那么針對變量操作不是原子性的,它的解決辦法就是進行加鎖,使用synchronized關鍵字進行加鎖!!!
修改之后的代碼:
//實現一個線程安全的單例模 class Singleton2{//懶漢模式,在該模式中不著急創建出實例,在類外需要的時候,我們再進行創建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){synchronized(Singleton2.class) {if (instance == null) {instance = new Singleton2();}}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }我們知道如果遇到可針對變量操作不是原子的,要使用synchronized關鍵字進行加鎖,但是也不是說,代碼中有了synchroniezd關鍵字就一定不會線程安全,我們要把synchronized關鍵字加對地方。synchronized加的位置正確,不能隨便寫。
//類對象在一個類中只有唯一一份,就能保證調用的getInstance的時候都是針對都一個對象進行加鎖 synchronized(SingleTon2.class){}但是我們加鎖之后,又帶來的新的問題!!!
對于剛才的這個懶漢模式的代碼而言,線程不安全發生在instance沒有被初始化之前,未被初始化的時候,多線程調用getInstance()方法時,會存在線程安全問題,因為涉及到讀和修改。但是在instance初始化之后,instance一定不是null,if條件一定不成立,getInstance()就只剩下兩個讀操作,也就是說instance初始化之后,線程就是安全的了。
并且按照上述的加鎖操作,無論是代碼中的instance初始化之前,還是初始化之后。每次調用getInstance()方法的時候,都會對其進行加鎖。也就意味著即使初始化之后(已經線程安全了),但是仍然存在大量的鎖競爭。
既然這里的instance已經被初始化過了,即使這里的條件在不能被滿足了,但是仍然會調用getInstance()方法,都需要進行加鎖,也就可能會產生鎖競爭,但是我們知道這里的鎖競爭其實是沒有必要的。
我們知道加鎖確實能讓線程安全,但是同時也付出了代價,一旦在一個線程中加了鎖之后,那么就和運行高效無關了。(程序的速度就變慢了)因為加鎖之后,線程之間是串行執行的。代碼的運行效率就變慢了。
博主以前說過開發效率要比運行效率更重要,一切都要從程序員的利益出發,但是運行效率也不是說不重要!如果說運行效率不重要的話,那么我們在前面學習那么多的數據結構干啥,不都是使用一個較好的數據結構,來組織數據,讓代碼變得有效嘛
改進方案:
在instance初始化之前,才進行加鎖,在初始化之后,不進行加鎖。在加鎖這里在加一個條件判斷即可
代碼如下:
//實現一個線程安全的單例模 class Singleton2{//懶漢模式,在該模式中不著急創建出實例,在類外需要的時候,我們再進行創建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){if(instance == null) { //如果instance被初始化過了,那么就不必再進行加鎖,直接返回這個實例即可synchronized (Singleton2.class) {if (instance == null) {instance = new Singleton2();}}}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }我們在上述的代碼中,可以看到在getInstance()方法中,使用了兩個條件判斷語句,都是判斷instance==null ,但是這兩個條件判斷語句的實際含義是千差萬別。這兩個添加判斷長得一樣,純屬是一個美麗的錯誤。
上面的條件判斷是是否需要加鎖。也就是說現在的instance是否已經被初始化過了
下面的條件判斷是是否需要創建實例。
我們如果去掉了里層的條件判斷語句那么就會變成:
//實現一個線程安全的單例模 class Singleton2{//懶漢模式,在該模式中不著急創建出實例,在類外需要的時候,我們再進行創建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){if(instance == null) {synchronized (Singleton2.class) { //其實在這里加鎖,就是加了個寂寞,在這里只針對設置實例加鎖,在加鎖語句的外面,還有istance == null 涉及到 讀和判斷,所以說加鎖和沒加一樣,還是不符合原子性。instance = new Singleton2();}}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }如果直接對getInstance()方法進行加鎖,那么就是一個無腦加鎖
此處博主告訴各位老鐵,在上述的代碼中,還存在一個問題。但是已經線程安全了呀,哪里還有錯呢?我們在單例模式中使用volatile,主要是使用volatile可以進制指令重排序,從而保證程序的正常運行。
public class Singleton {private Singleton() {}// 使用 volatile 禁止指令重排序private static volatile Singleton instance = null;public static Singleton getInstance() {if (instance == null) { // 1synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // 2}}}return instance;} }注意觀察上述代碼,我標記了第 ① 處和第 ② 處的兩行代碼。給私有變量加 volatile 主要是為了防止第 ② 處執行時,也就是“instance = new Singleton()”執行時的指令重排序的,這行代碼看似只是一個創建對象的過程,然而它的實際執行卻分為以下 3 步:
創建內存空間。
在內存空間中初始化對象 Singleton。
將內存地址賦值給 instance 對象(執行了此步驟,instance 就不等于 null 了)。
試想一下,如果不加 volatile,那么線程 1 在執行到上述代碼的第 ② 處時就可能會執行指令重排序,將原本是 1、2、3 的執行順序,重排為 1、3、2。但是特殊情況下,線程 1 在執行完第 3 步之后,如果來了線程 2 執行到上述代碼的第 ① 處,判斷 instance 對象已經不為 null,但此時線程 1 還未將對象實例化完,那么線程 2 將會得到一個被實例化“一半”的對象,從而導致程序執行出錯,這就是為什么要給私有變量添加 volatile 的原因了。
總結一下:
實現一個線程安全的單例模式—針對懶漢模式
總結
以上是生活随笔為你收集整理的【多线程经典实例】实现一个线程安全的单例模式的全部內容,希望文章能夠幫你解決所遇到的問題。