在java中,線程的終止,是一種“軟性”操作,必須要對應的線程配合,才能把終止落實下去
然而,系統原生的api其實還提供了,強制終止線程的操作,無論線程執行到哪,都能強行把這個線程干掉。
這樣的操作Java的api中沒有提供的,上述的做法弊大于利,強行取結束一個線程,很可能線程執行到一半,會出現一些殘留的臨時性質的“錯誤”數據。
public class ThreadDemo12 {public static void main(String[] args) {boolean isQuit = false;Thread t = new Thread(() -> {while (!isQuit){System.out.println("我是一個線程,工作中!!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}//當前是死循環,給了個錯誤指示/* System.out.println("線程工作完畢!");*/});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("讓t線程結束!");isQuit = true;}
}
我們將變量isQuit作為main方法中的局部變量。
彈出了警告,這就涉及到lambda表達式的變量捕獲了,當前捕獲的變量是isQuit所以對于isQuit來說,它要么加上final,要么不去進行修改。?
isQuit是局部變量的時候,是屬于main方法的棧幀中,但是Thread lambda是又自己獨立的棧幀的,這兩個棧幀的生命周期是不一致的
這就可能導致main方法執行完了,棧幀就銷毀了,同時Thread的棧幀還在,還想繼續使用isQuit--
在java中,變量捕獲的本質就是傳參,就是讓lambda表達式在自己的棧幀創建一個新的isQuit并把外面的isQuit的值拷貝過來(為了避免isQuit的值不同步,java就不讓isQuit來進行修改)
等待線程
多個線程的執行順序是隨機的,雖然線程的調度是無序的,但是可以通過一些api來影響線程執行的順序。
join就可以,
public class ThreadDemo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("我是一個線程,正在工作中...");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("線程執行結束");});t.start();/* Thread.sleep(5000);*///這個操作就是線程等待t.join();System.out.println("這是主線程,期望這個日志在 t 結束后打印");}
}
這種方法比sleep方法要好很多,畢竟誰也不知道t線程啥時候結束,用join可以讓線程等 t 線程結束后再執行,這時候main線程的狀態就是“阻塞”狀態了。
Thread類基本的使用
1.啟動線程? ? ? ? start方法
理解 run 和 start 區別
2.終止線程? ? ? ? ? ? ? 核心讓run方法能夠快速結束
非常依賴 run 內部的代碼邏輯
Thread? ?isInterrupted(判定標志位)/interrupt(設置標志位)
如果提前喚醒sleep會清楚標志位
3.等待線程 join 讓一個線程等待另一個線程結束
線程之間的順序我們無法控制,但我們可以控制結束順序
獲取線程引用
Thread.currentThread()獲取到當前線程的 引用(Thread 的引用)
如果是繼承Thread,直接使用 this 拿到線程實例
如果不是則需要使用??Thread.currentThread();
線程的狀態
就緒:這個線程隨時可以去 cpu 上執行
阻塞:這個線程暫時不方便去cpu上執行
java中線程又以下幾種狀態:
1.NEW Thread 對象創建好了,但是還沒有調用 start 方法在系統中創建線程.
2.TERMINATED Thread 對象仍然存在,但是系統內部的線程已經執行完畢了
3.RUNNABLE 就緒狀態.表示這個線程正在 cpu 上執行,或者準備就緒隨時可以去 cpu 上執行4.TIMED WAITING 指定時間的阻塞, 就在到達一定時間之后自動解除阻塞.使用 sleep 會進入這個狀態.使用帶有超時時間的join也會
5.WAITING ?不帶時間的阻塞 (死等),必須要滿足一定的條件,才會解除阻塞
6.BLOCKED 由于鎖競爭,引起的阻塞,
?線程安全問題,來看下面的一段代碼
public class ThreadDemo19 {private static int count = 0;public static void main(String[] args) throws InterruptedException {//隨便創建個對象都行/* Object locker = new Object();*///創建兩個線程,每個線程都針對上述 count 變量循環自增 5w次Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {/* synchronized(locker) {count++;}*/count++;}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {/* synchronized(locker) {count++;}*/count++;}});t1.start();t2.start();t1.join();t2.join();//打印count結果System.out.println("count = " + count);}
}
?我們發現這個結果是錯的,我們計算的結果應該是100000.
這就涉及到線程安全問題了
count++是由三個指令構成的
1.load? ? ?從內存中讀取數據到cpu寄存器
2.add? ? ? 把寄存器中的值 + 1
3.save? ? ? 把寄存器的值寫回到內存中
對于單個線程是沒有這種問題的,但是對于多線程就會冒出來問題
我們發現預期是進行兩次count++后返回的count為2,但是因為兩個線程在讀取時出現了問題,第二個線程讀取的數據是還未進行更新的數據,這就導致出現了錯誤。
如果是這樣的順序自然沒有問題了
?我們需要的進行順序應該時等第一個線程save后第二個線程再進行load。
本質時因為線程之間的調度時無序的時搶占式執行
這就不得不提到String這個“不可變對象”了
1.方便JVM進行緩存(放到字符串常量池中)
2.hash值固定
3.線程安全的
線程不安全原因
1.根本原因? 操作系統上的線程時“搶占式執行”“隨即調度” => 線程之間執行順序帶來了很多變數
2.代碼結構? 代碼中多個線程,同時修改同一個變量
1.一個線程修改一個變量
2.多個線程讀取同一個變量
3.多個線程修改不同變量
這些都不會有事
3.直接原因 上述的線程修改操作本身不是’原子的‘
4.內存可見性問題
5.指令重排序問題
對于3這個問題我們可以找辦法來解決
1.對于搶占式執行修改,這是無法改變的事
2.對代碼結構進行調整,這是個辦法,但在有些情況下也是不適用的
3.可以通過特殊手段將著三個指令打包為一個“整體”,我們可以對其進行加鎖
加鎖
目的:把三個操作,打包成一個原子操作
進行加鎖的時候需要先準備好鎖對象,一個線程針對一個鎖對象加鎖后,當其他線程對鎖對象進行加鎖,則會產生阻塞(BLOCKED)(鎖沖突/鎖競爭),一直到前一個線程釋放鎖為止
要加鎖得用到synchronized。
?進入()就會加鎖(lock),出了{ }就會解鎖(unlock),synchronnized 是調用系統的 api 進行加鎖,系統api本質上是靠 cpu 上特定指令完成加鎖
當t1加鎖后,在沒解鎖的情況下,t2再想進行加鎖就會出現阻塞
在t1沒有解鎖的情況下,即使t1被調度出cpu,t2也還是在阻塞
即使這樣會影響到執行效率,但也比串行要快不少。
我們只是對count加鎖使得count串行,但for循環還是可以進行“并發”執行的
?
加鎖之后結果就正確了。
?對于對象的話只要不是同一個對象就不會有競爭這一說。
1.如果一個線程加鎖,一個不加,是不會出現鎖競爭的
2.如果兩個線程,針對不同的對象加鎖,也還是會存在線程安全問題
3.
把count放到一個 Test t 對象中,通過add來修改鎖對象的時候可以寫作this
相當于給this加鎖(鎖對象 this)
對于靜態方法的話相當于給類對象加鎖
我們可不可以加兩個鎖呢?
是否會打印hello?
?為啥會打印成功?不應該出現鎖沖突嗎?
當前由于是同一個線程,此時鎖對象,就知道了第二次加鎖的線程,就是持有鎖的線程,第二次操作,就可以直接放行不會出現阻塞。
這個特性被稱為“可重入”
一旦上述的代碼出現了阻塞,就稱為“死鎖”
可重入鎖就是為了防止我們在“不小心”中引入的問題
當我們在第一次加鎖的時候,計數器會進行加一操作,當第二次進行加鎖的時候,大仙加鎖的線程和持有鎖線程是一個線程,這個時候就會加鎖成功,并且計數器加一。
等到了計數器為0的時候才是真正的解鎖了,對于可重入鎖來說:
1.當前這個鎖是被哪個線程持有的
2.加鎖次數的計數器
計數器可以幫助線程清楚的記錄有幾個鎖。
加鎖能夠解決線程安全問題,但同時也引入了一個新的問題就是死鎖。
死鎖的三種典型場景
1.一個線程一把鎖
如果鎖是不可重入鎖,并且對一個線程對這把鎖進行加鎖兩次
2.兩個線程,兩把鎖
線程? 1? 獲得 鎖A
線程? ?2? 獲得 鎖B
接下來 1 嘗試獲取B, 2 嘗試獲取 A就同樣出現死鎖了!!!? ? ?
一旦出現“死鎖”,線程就“卡住了”無法繼續工作
public class ThreadDemo22 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A){//sleep一下是給t2時間讓t2也能拿到Btry {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//嘗試獲取B,并沒有釋放Asynchronized (B){System.out.println("t1拿到兩把鎖");}}});Thread t2 = new Thread(() -> {synchronized (B){//sllep一下,是給t1時間,讓t1能拿到Atry {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//嘗試獲取A并沒有獲取Bsynchronized (A){System.out.println("t2拿到了兩把鎖");}}});t1.start();t2.start();}
}
就像這樣。
?3.N個線程M把鎖
哲學家就餐
?解決死鎖問題的方案
產生死鎖的四個必要條件
1.互斥使用,獲取鎖的過程是互斥的,一個線程拿到了這把鎖,另一個線程也想獲取,就需要阻塞等待。
2.不可搶占,一個線程拿到了鎖之后,只能主動解鎖,不能讓別的線程強行把鎖搶走。
3.請求保持,一個線程拿到了鎖 A 之后,在持有A的前提下,嘗試獲取B
4.循環等待,環路等待
由于四個都是必要條件,所以只要破環一個就解決問題了。
1,2.鎖最為基本的特性
3.代碼結構要看實際需求
4.代碼結構的,最為容易破壞
指定一定的規則,就可以有效的避免循環等待
1.引入額外的筷子
2.去掉一個線程
3.引入計數器,限制最多同時所少人吃飯
4.引入加鎖順序的規則
內存可見性引起的線程安全問題
public class ThreadDemo23 {private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 0){}System.out.println("t1線程結束");});Thread t2 = new Thread(() -> {System.out.println("請輸入flag的值:");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}
}
運行代碼發現,并沒有我們想象的打印t1線程結束,而是直接不動了。
在這個過程中有兩個關鍵的點
1.load 操作執行的結果,每次都是一樣的(要想輸入,過幾秒才能輸入,在這幾秒都不知道循環都已經執行了上百億次了)
2.load 操作開銷遠遠超過 條件跳轉
訪問寄存器的操作速度,遠遠超過訪問內存
由于load開銷大,并且load的結果又一直沒有變化,所以jvm就會懷疑load操作有必要存在的必要嗎?
此時jvm就可能做出代碼優化,把上述load操作,給優化掉(只有前幾次進行load,后續發現,load反正都一樣,靜態分析代碼,也沒看到哪里改了flag,因此就把load操作,干掉了),干掉之后,就相當于不再重復讀內存直接使用寄存器之前”緩存“的值,大幅度的提高循環的執行速度
多線程的情況下很容易出現誤判,這里相當于 t2 修改了內存,但是 t1 沒有看到這個內存優化,就稱為”內存可見性“問題
我們發現在剛剛的代碼加上sleep就會執行成功,即使sleep時間有多小。?因為不加sleep一秒鐘可能循環上百億次,load開銷非常大,優化迫切程度就更高。
加了sleep,一秒鐘可能循環的次數就可能變為1000次,這樣load開銷相對來說就小了,所以優化迫切程度就想對來說就低了。
內存可見性問題,其實是個高度依賴編譯器優化的問題,啥時候觸發這個問題,都不知道
所以干脆希望不要出現內存可見性問題,將上述優化給關閉了
這就要使用關鍵字 volatile 來對上述的優化進行強制的關閉(雖然開銷大了,效率低了。但是數據準去性/邏輯正確性提高了)。
volatile 關鍵字
核心功能就是保證內存可見性(另一個功能進制指令重排序)
在上述的代碼中,編譯器發現,每次循環都要讀取內存,開銷太大,于是就把讀取內存操作優化成讀取寄存器操作,提高效率
在JMM模型的表述下
在上述代碼中,編譯器發現,每次循環都要讀取”主內存“,就會把數據從”主內存“中復制到”工作內存“中,后續每次都是讀取”工作內存“。?