一.wait 和 notify(等待 和 通知)
引入 wait notify 就是為了能夠從應用層面,干預到多個不同線程代碼的執行順序,可以讓后執行的線程主動放棄被調度的機會,等先執行的線程完成后通知放棄調度的線程重新執行。
自助取款機
當取款機沒有錢的時候,要想去取錢只能等別人進去存錢或者等銀行的人過來放錢,否則他永遠拿不到錢,他在出去之后又會進去取款,剛剛釋放了鎖,就又會參與到鎖競爭,并且大概率他會一直拿到鎖,這叫“線程餓死“,這種情況只是概率性事件,但還是會極大影響到其他線程運行,這個時候他為了不影響到其他人就會等待(wait),然后讓后面如果存錢的人存了錢了,通知(notify)自己一聲,自己就可以重新排隊取錢,在沒通知的這段時間,他就會一直在旁邊等,不會去排隊了。?
join 和 wait
join 是等待另一個線程執行完,才繼續執行
wait 則是等待另一個線程通過 notify 進行通知(不要求另一個線程必須執行完)
wait進入阻塞,只能說明自己釋放鎖了,至于是否有其他線程拿到鎖,這是不能確定的
public class ThreadDemo25 {public static void main(String[] args) {//需要有一個統一的對象進行加鎖,wait,nitifyObject locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker){System.out.println("t1 wait 之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(" t1 wait 之后");}});Thread t2 = new Thread(() -> {try {Thread.sleep(5000);synchronized (locker){System.out.println(" t2 notify 之前");locker.notify();System.out.println("t2 notify 之后");}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();}
}
我們發現阻塞等待的原因是19行的wait
wait方法放到synchronized是因為要釋放鎖,前提是先加上鎖
java特別約定要把notify放到synchronized中?
由于線程的隨機調度我們并不知道要先調用誰,如果先調用t2就沒有線程去給t1喚醒了,所以要保證t1先執行,我們給t2加上了sleep?
需要注意的是t2在notify之后并沒有釋放鎖,而t1喚醒后會嘗試加鎖,所以會產生小小的阻塞。
notifyAll? ? ?喚醒這個對象所有等待的線程
假設有很多個線程,都使用同一個對象wait,針對這個對象進行notifyAll,此時就會全都喚醒
需要注意的是,這些線程在wait返回的時候,需要重新獲取鎖,就會因為鎖競爭,是這些線程串行執行
wait 和 sleep?
wait 提供了一個帶有超時時間的版本
sleep 也是能指定時間
都是時間到了,就繼續執行,解除阻塞了
wait 和 sleep 都可以被提前喚醒(雖然時間還沒到),wait通過notify喚醒,sleep通過interrupt喚醒
使用wait 最主要的目標一定是不知道多少時間的前提下使用的,超時時間是為了兜底
使用sleep,一定是知道了多少時間的前提下使用的,雖然能提前喚醒,但是通過異常喚醒一般是程序出現一些特殊情況了
二.單例模式
單例模式是一種設計模式,遵守設計模式下限就有了兜底
1.餓漢模式
單例 == 單個實例(對象)
static 這個引用就是我們期望創建出唯一的實例的引用,static 靜態的 指的是“類屬性”
instance 就是 當前類對象里面持有的屬性
每個類的類對象,只存在一個,類對象中的static屬性,自然也是只有一個了
?這時instance所指向的對象就是唯一的一個對象,其他代碼要想使用這個類的實例,就需要通過這個方法來進行獲取,而不是直接new一個出來。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
這直接從根本上讓其他人想new都new不了了
Sington內部代碼早就把唯一的實例安排好了?
class Singleton{private static Singleton instance = new Singleton();public static Singleton getInstance(){return instance;}private Singleton() {}
}
public class ThreadDemo26 {public static void main(String[] args) {Singleton s = Singleton.getInstance();}
}
上述代碼成為“餓漢模式”單例模式一種簡單的寫法,在程序啟動時,實例就創建了,所以就是用餓漢,創建實例非常早。
?2.懶漢模式
創建實例的時機不一樣了,創建實例的時機更晚,直到第一次使用的時候,才會創建實例
class SingletonLazy{//這個引用指向唯一實例,這個引用先初始化null,而不是立即創建實例private volatile static SingletonLazy instance = null;private static Object locker = new Object();public static SingletonLazy getInstance(){if(instance == null){//如果 Instance 為 NULL,就說明時首次調用,首次調用就需要考慮線程安全問題,就要加鎖//如果非空的話就說明時后續調用就不必加鎖synchronized (locker){if(instance == null) instance = new SingletonLazy();}}return instance;}private SingletonLazy(){}
}
public class ThreadDemo27 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}
}
如果是首次調用 getinstance,那么instance 引用 為null,進入if語句創建實例出來,后續再次調用返回的就已經是創建好的引用了。
餓漢模式和懶漢模式是否屬于線程安全?
?餓漢模式屬于讀操作在多線程是安全的。
但加入了懶漢模式就不一定了
?這樣就會導致實例被new 了兩次,就又bug了。這時線程不安全,在多線程可能會new出多個實例
即使寫成這樣沒有線程安全問題,但還是因為已經創建了實例,但還是進行加鎖解鎖操作使得效率降低。
?所以if不一定要加在方法上,
我們加在了實例化對象上,這樣就不會木訥的進行加鎖,但是還有一個致命的問題就是整個方法不是原子了,這個時候我們就得考慮對象是否創建問題了。
我們得先理清一下思路了,現在我們第一個if是用來判斷是不是第一次進行創建對象的,我們所面臨的問題是在多線程下if后面的代碼執行是不確定的,可能已經調用其他線程并創建了對象,所以我們得加一個判斷是否創建了對象的語句
?兩個if代碼一樣但意義不同
第一個if是為了判定是否第一次創建對象,并加鎖,第二個if是判斷是否要創建對象
在創建對象過程中還涉及到了線程安全問題 ------- 指令重排序
調整原有代碼的執行順序,保證邏輯不管的前提下,提高程序的效率
創建對象這一行代碼可以拆為三個步驟
1.申請一段內存空間
2.在這個內存上調用的方法。創建出這個實例
3.把這個內存地址賦值給instance引用變量
正常情況下是按照1,2,3順序來執行的,但是編譯器可能優化成1,3,2的順序來執行
在單線程下1,2,3或者1,3,2都是可以的
但是如果是多線程就可能引入問題了!!!

在t1加鎖之后t2進行阻塞,t1解鎖后t2獲得了鎖,但在這個時候?t2判斷不為空直接返回,但這個時候instance并未初始化,如果使用instance里面的屬性或者方法就可能會出現錯誤,那難道2不是同時進行的嗎?
注意:在執行完1,3后線程有可能也被調度走了,并未進行初始化。要想執行2可能會隔一段時間
在之前解決內存可見性時我們用到了volatile它的功能有兩個
1.保證內存可見性,每次訪問變量必須都要重新讀取內存,而不會優化到寄存器/緩存中
2.禁止指令重排序,針對這個volatile 修飾的變量的讀寫操作相關指令,是不能被重排序的
回顧一些我們解決問題的步驟
首先
我們因為餓漢模式在多線程下,會出現二次實例化對象的操作,所以我們加上了鎖,
第二次
我們因為即使加上了鎖,但我們因為加鎖的位置太消耗效率所以我們將鎖的位置改變了,但我們無法判斷對象是否被創建了,所以我們又加上了一層if,
第三次
我們了解了指令重排序,我們會遇到,對象創建但并未初始化,然后導致使用了沒初始化對象的屬性或者方法,出現了失誤,這時候我們回想起之前解決內存可見性的volatile,它的另一個功能就是解決指令重排序所以我們加上了這個關鍵字,至此我們解決了這一系列問題