文章目錄
- 💡volatile保證內存可見性
- 💡單例模式
- 💡餓漢模式
- 💡懶漢模式
- 💡懶漢模式多線程版
- 💡volatile防止指令重排序
💡volatile保證內存可見性
Volatile 修飾的變量能夠保證“內存可見性”以及防止”指令重排序“
什么是可見性:當某個線程修改了某個共享變量,其他的線程是否可以看見修改后的內容;
因為訪問一個變量時,CPU就會先把變量從內存中讀出來,然后放到CPU寄存器中進行運算;運算完之后,再將新的數據在內存中進行刷新;
對于操作系統來講,讀內存的速度是比較慢的,(注意:這里的慢 是 相對于寄存器而言的,就像,讀內存要比讀硬盤快上千倍或上萬倍,讀寄存器比讀內存快上千倍上萬倍), 這時候就會影響執行的效率。為了提高效率,編譯器就會對代碼進行一個優化,把讀內存的操作優化成讀寄存器,從而減少對內存的讀取,提高整個效率;
舉個例子:
代碼目的:創建兩個線程,通過線程2修改線程1的循環判斷條件來終止線程1的循環執行
public class Demo1 {private static int flag = 0;public static void main(String[] args) {Thread thread1 = new Thread(() -> {while(flag == 0) {//當循環不等于0時,一直循環,直到flag被改變}System.out.println("thread1 執行結束");});Thread thread2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("更改flag:");//通過更改flag終止線程1的執行flag = in.nextInt();System.out.println("輸入成功");});thread1.start();thread2.start();}
}
根據結果可以看到,線程1并沒有終止循環,這就是“內存可見性”所導致的線程不安全👇
在多線程的環境下(在單線程環境下沒問題),如果編譯器作出優化,可能就會導致bug,雖然提高了效率,但是最后結果卻是錯誤的,
此時就需要程序員使用Volatile關鍵字告訴編譯器,不需要進行代碼優化:
直接給flag加上Volatile即可
注意, volatile只能夠保證內存可見性問題,不會保證代碼的原子性,但是Synchronized既可以保證內存可見性,也能保證原子性;
以上就是volatile能夠保證內存可見性的講解
💡單例模式
單例模式是一種經典的設計模式了,它的作用就是保證在有些場景下,需要一個類只能有一個對象,而不能有多個對象,比如像你以后娶媳婦,你娶媳婦肯定是只能娶一個,而不能娶兩個;
但是,問題來了,一個類只需要一個對象,那在new對象的時候只new一次對象不就可以了么,為什么還要弄個這么麻煩的東西呢?
因為啊,只new一次對象確實是只有一個,但是呢,如果你在寫代碼的過程中忘了呢,然后又new了一次,這種概率是很大的,畢竟,人是最不靠譜的動物😅,就像是有一句話說的好:寧可相信世界上有鬼,也不要相信男人的那張嘴😂,所以的,為了防止這種失誤發生,就有了單例模式,在Java中也有許多類似的機制,比如final,就會保證修飾的變量肯定是不能改變的;@override,保證你這方法肯定是一個重寫方法;這些都是在語法方面進行了一些限制,但是,在語法方面,對于單例并沒有特定的語法,所以,這里就通過編程技巧來達到類似的限制效果;
單例模式的兩種實現方式:
💡餓漢模式
1.在類中實例化類的對象,給外界提供一個方法來使用這個對象;
2.將構造方法用private修飾,保證在類外不能再實例化這個類對象
public class SingleTon {//在類的內部實例化對象public static SingleTon instance = new SingleTon();//定義一個方法,用來獲取這個對象//后序如果類外的代碼想要使用對象時,直接調用這個方法即可public static SingleTon getInstance() {return instance;}//設置一個私有的構造方法,保證在這個類外無法實例化這個對象private SingleTon(){}
}
可以看到,這里的對象被static修飾,所以在類被加載的時候創建,創建的時機就比較早,并且被static修飾的對象只會被創建一次,所以這種在類加載時就創建實例的模式稱為餓漢模式
💡懶漢模式
懶漢模式單線程版:
這樣的寫法與上面的相同點就是:同樣在類外不能再第二次實例化對象,不同點是:將創建對象的時機放在getInstance方法中,這樣在類加載的時候就不會創造實例,而是當第一次調用這個方法時才會去創建;
public class SingleTon {public static SingleTon instance = null;//定義一個方法,用來獲取這個對象//后序如果類外的代碼想要使用對象時,直接調用這個方法即可public static SingleTon getInstance() {//懶漢模式if(instance == null) {instance = new SingleTon();}return instance;}//設置一個私有的構造方法,保證在這個類外無法實例化這個對象private SingleTon(){}
}
💡懶漢模式多線程版
在線程安全方面,上面的餓漢模式是在多線程下是安全的,而懶漢模式在多線程下是不安全的;
因為,如果多個線程同時訪問一個變量,那么不會出現不安全問題,如果多個線程同時修改一個變量,就有可能出現不安全問題;
餓漢模式下,只進行了訪問,沒有涉及到修改
懶漢模式下,不僅進行了訪問,還涉及了修改,那么下面就講解以下懶漢模式在多線程下如何會產生不安全
既然出現了不安全問題,那么如何將懶漢模式修改成安全的呢?
💡方法:進行加鎖,使線程安全
但是,如果鎖加在這個地方,仍然是不安全的,因為,這樣還是會進行穿插執行,如果兩個并發的進入的 if 語句中,那么,就會進行鎖競爭,假設,thread1 獲取到了鎖,thread2 在阻塞等待,等到 thread1 創建一次對象,釋放鎖后,thread2 就又會載獲取到鎖,進行創建對象,所以,這個加鎖操作并沒有保證它是一個整體(非原子性)
所以說,并不是加了鎖就安全,只有鎖加對了才會安全,在加鎖的時候要保證以下幾方面:
-
鎖的 {} 的范圍是合理的,能夠把需要作為整體的每個部分都包括進去;
-
鎖的對象能夠起到鎖競爭的效果;
懶漢模式多線程版改進👇:
將if語句和new都放在鎖里面成為一個整體,這樣就避免了會穿插執行;
public static SingleTon getInstance() {synchronized (SingleTon.class) {if(instance == null) {instance = new SingleTon();}}return instance;}
但是上述代碼還有一個問題,每當調用getInstance時,都會嘗試去進行加鎖,而加鎖是一個開銷很大的操作,而懶漢模式之所以會出現線程不安全問題,是因為只是在第一次調用getInstance方法new對象時,可能會出現問題,但是,只要new完對象以后,就不用再進行鎖競爭了,直接訪問就可以了,所以再次進行優化👇:
public static SingleTon getInstance() {//在最外面在進行一次判斷if(instance == null) {synchronized (SingleTon.class) {if(instance == null) {instance = new SingleTon();}}}return instance;}
在第一次實例化對象后,以后再調用個getInstance方法時,就不會再創建對象,而且也不會再去獲取鎖,因為,第一個if判斷語句都不會進去,所以不會執行到加鎖的語句;
上面的單例模式看著好像是完全沒問題了,但是,還是有一個問題,就是可能會觸發指令重排序問題,所以就需要使用volatile解決指令重排序問題:
💡volatile防止指令重排序
指令重排序:編譯器會保證在你代碼邏輯不變的情況下,對代碼進行優化,使代碼的性能得到提高,這樣的操作稱為指令重排序;
舉個例子:
在代碼中,在實例化對象這一步可能會出現指令重排序問題,下面就來講解一下為什么👇
對于上述的指令重排序問題,解決方案就是:使用volatile關鍵字修飾singleTon
**線程安全的單例模式(懶漢模式)**👇
public class SingleTon {//使用volatile關鍵字修飾,防止指令重排序public static volatile SingleTon singleTon = null;public static SingleTon getSingleTon() {if(singleTon == null) {synchronized (SingleTon.class) {if(singleTon == null) {singleTon = new SingleTon();}}}return singleTon;}private SingleTon() {};}
💡💡這里再次提醒,使用單例模式要注意三個要點:
- 加鎖
- 兩層if判斷
- 使用volatile修飾引用,防止指令重排序