一、wait 和 notify
wait notify 是兩個用來協調線程執行順序的關鍵字,用來避免“線程餓死”的情況。
wait? 和 notify 其實都是 Object 這個類的方法,而 Object這個類是所有類的“祖宗類”,也就是說明,任何一個類,都可以調用wait 和notify這兩個方法。
Java標準庫中,涉及到阻塞的方法,都可能拋出InterruptedException
讓我們來觀察一下這個異常的名字。
Illegal:非法的,不正確的,不合理的(而不是違反法律的)、
Monitor:監視器/顯示器(電腦的顯示器,英文不是screen,而是Monitor)此處的Monitor指的是synchronized,synchronized在JVM里面的底層實現,就被稱為“監視器鎖”(JVM源碼,變量名是Monitor相關的詞)
所以這個異常的意思是,當前處于非法的鎖狀態。
眾所周知,鎖一共有兩種狀態,一種是加鎖,一種是解鎖。
wait方法內部做的第一件事情,就是釋放鎖。
而我們必須要先得到鎖,才能去談釋放鎖。因此,wait必須放到synchronized代碼塊內部去進行使用。
此處的阻塞會持續進行,直到其他線程調用notify把該線程進行喚醒。
此處的阻塞會持續進行,直到其他線程調用notify,將該線程進行喚醒。
package Thread;import java.util.Scanner;public class demo28 {// 將 object 變量移到類內部并添加 static 修飾符public static Object object = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (object) { System.out.println("t1 wait之前");try {object.wait(); } catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 wait之后"); }});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("請輸入任意內容 嘗試喚醒t1");scanner.next(); synchronized (object) { object.notify(); System.out.println("t2 notify之后"); }}); t1.start();t2.start();}
}
輸出:
圖上這四處地方的鎖,必須是同一個對象。
假設notify后面又有一堆別的邏輯,此時,這個鎖就會再多占有一會。
【總結】wait要做的事情:
1、使當前執行代碼的線程進行等待(把線程放到等待隊列中去)
2、釋放當前的鎖
3、滿足一定條件的時候被喚醒,并且重新嘗試獲取這把鎖
(這三個步驟是同時進行的)
使用wait的時候,阻塞其實是有兩個階段的:
1、WAITING的阻塞:通過wait 等待其他線程的通知
2、BLOCKED阻塞:當收到通知之后,就會重新嘗試獲取這把鎖。重新嘗試獲取這把鎖,很可能又會遇到鎖競爭
wait進行阻塞之后,需要通過notify喚醒。默認情況下,wait的阻塞也是死等。
這樣子是不合理的,因此,我們在工作中需要設定等待時間上限。(超過時間)
括號里的等待時間是毫秒
package Thread;import java.util.Scanner;public class demo29 {public static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (locker) {System.out.println("t1 wait之前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 wait之后");}});Thread t2 = new Thread(() -> {synchronized (locker) {System.out.println("t2 wait之前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2 wait之后");}});Thread t3 = new Thread(() -> {synchronized (locker) {System.out.println("t3 wait之前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t3 wait之后");}});Thread t4 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("請輸入任意內容 嘗試喚醒t1、t2、t3");scanner.next();synchronized (locker) {System.out.println("t4 notify之前");locker.notify(); // 喚醒一個在 locker 上等待的線程,這里是 t1System.out.println("t4 notify之后");}});t1.start();t2.start();t3.start();t4.start();}}
輸出:
可以看出,當前只是將t1喚醒了
再次嘗試
喚醒的仍然是t1
咱們在多線程中談到的“隨機”其實不是數學上概率均等的隨機,這種隨機的概率是無法預測的。取決于調度器如何去調度。調度器里面,其實不是“概率均等的喚醒”,調度器內部也是有一套規則的。這套規則,對于程序員是“透明的”,程序員做的,就是不能依賴于這里的狀態。
mysql的時候,select查詢一個數據,得到的結果集,是按照怎樣的順序的呢?(是按照id的順序,時間的順序,排列的順序的嗎?)都不是,mysql就沒有這樣的承諾。必須加上orderby
notifyAll可以喚醒全部:
如果沒有任何對象在wait,那么直接調用notify / notifyAll 會發生什么?
不會發生任何事情,直接憑空調用notify是沒有任何副作用的
經典面試題:
請你談一談sleep? 和 wait 的區別
1.wait 的設計就是為了提前喚醒。超時時間,是“后手”(B計劃)
sleep 的設計就是為了到達時間再進行喚醒。雖然也可以通過Interrupt()進行提前喚醒,但是這樣的喚醒是會產生異常的。(此處的異常表示:程序出現不符合預期的情況,才稱為“異常”)
2.wait需要搭配鎖來時進行使用,wait執行時會先釋放鎖
?sleep不需要搭配鎖進行使用,當把sleep放到synchronized內部的時候,不會釋放鎖(抱著鎖睡覺)
綜上所述,在實際開發中,wait比sleep用的更多。
二、單例模式
單例模式是一種設計模式,校招中最常考到的設計模式之一。
為了使得新手的代碼下線也能夠有所保證,大佬們研究出了一些“設計模式”,用來解決一些固定的場景問題,這些問題有著固定的套路。
如果按照設計模式寫,能夠得到一個較為靠譜的代碼,屬于是一種軟性要求。
設計模式有很多很多種類,不僅僅有23種。
單例模式 :單例,也就是單個實例(單個對象)。雖然一個類,在語法角度來說,是可以無限創建實例的,但是在實際的場景當中,可能有時候我們只希望這個類只有一個實例(例如JDBC)
那么,在Java代碼中,如何實現單例模式呢?——有很多種實現方式,其中最主要的有兩種模式:
1、餓漢模式
創建實例的時機是非常緊迫的。
由于此處的Instance是一個static 成員,創建時機,就是在類加載的時候。也就是說,程序一啟動,實例就被創建好了。
package Thread;class Singleton{private static Singleton instance = new Singleton();public static Singleton getInstance(){return instance;} //做了一個“君子協定”,讓其他類不能new這個類,只能通過getInstance()方法獲取這個類的實例。private Singleton(){}
}public class demo33 {public static void main(String[] args) {Singleton instance1 = Singleton.getInstance();Singleton instance2 = Singleton.getInstance();System.out.println(instance1 == instance2);}}
? 做了一個“君子協定”,讓其他類不能new這個類,只能通過getInstance()方法獲取這個類的實例。
2、懶漢模式
第一次使用這個實例的時候,才會創建這個實例,創建的時機更晚
上述兩份代碼,哪一份是線程安全的,哪一份是線程不安全的呢?
而懶漢模式容易因此下述問題:
最終只創建了一個實例!
實際開發中,單例類的構造方法可能是一個非常重量的方法。我們之前,代碼中也有單例模式的使用。當時通過單例類,管理整個服務器程序所以來的所有數據(100G)。這個實例創建的過程,就會從硬盤上把100G的數據加載到內存當中。
那么,如何解決該問題呢?
我們可以通過加鎖,將操作打包成原子的來解決該問題。
但是,這串代碼仍然存在問題:邏輯上來看,我們只是在第一次調用的時候,才會涉及到線程安全問題,只要對象創建完畢,后序都是直接return了,就不涉及修改了。但是,此時這個代碼,鎖是每次調用都會加上的。明明已經線程阿耐庵了,但是還要再進行加鎖,這并不合理。
圖中,一摸一樣的條件連續寫了兩遍。以前都是一個線程,這個代碼執行下來,第一次判定和第二次判定,結論是一定相同的。
而現在是多線程,第一次判定和第二次判定,結論可以不一樣。因為再第一次和第二次判定之間,可能有另外一個線程,修改了instance。多線程,打破了以前的認知。而后面學習的網絡,EE進階里面的框架,也會打破以前的認知。
在多線程當中,指令重排序容易引起線程安全問題。指令重排序是編譯器優化的一種手段,這是編譯器在確保邏輯一致的情況下,為了提高效率,調整代碼的順序,就可以讓效率變高了。然而,指令重排序在遇見多線程就又出現問題了。
此處涉及到的指令是非常多的,為了簡化這個模型,我們將他抽象成三個步驟:
1、申請內存空間
2、在內存空間上進行初始化(構造方法)
3、內存地址,保存到引用變量當中
在多線程中,由于指令重排序,容易引起上述問題。
Instance里面還沒有任何的屬性方法,但是已經被線程2拿去使用了!
那么,如何避免指令重排序的問題呢?
只需要加上volatile這個關鍵字。
volatile的意思是,針對這個變量的讀寫操作,不要觸發優化。