前言:要秋招了,複習一下應對秋招,糾結該先看啥,最後決定先學習《Java高併發編程詳解》,此博客爲看書所寫的筆記,因爲是筆記,所以會只記比較重要的東西,不適合初學者。
參考:https://blog.csdn.net/dufufd/article/details/80537638
目錄
第一章 ThreadAPI的詳細介紹
1.1 sleep和yield
sleep是一個靜態方法,有兩個重載方法,一個需要傳入毫秒數,另一個需要傳入毫秒與納秒數,休眠時不會放棄monitor鎖的所有權。
JDK1.5之後TimeUnit代替了Threa.sleep,使用TimeUnit更爲方便,如休眠3小時24分
TimeUnit.HOURS.sleep(3)
TimeUnit.MINUTES.sleep(24)
yield屬於一種啓發式的方法,其會提醒調度器願意放棄當前的CPU資源,如果CPU資源不緊張,則會忽略這種提醒,調用yield會使線程從RUNNING狀態切換到RUNNABLE狀態,一般這個方法不太常用。
在JDK1.5之前yield方法實際上是調用了sleep(0),但後面就不是了。
sleep和yield有着本質的區別,sleep會導致線程暫停指定的時間,沒有CPU時間片的消耗,會在指定時間釋放CPU資源,yield如果CPU調度器沒有忽略這個提示,會導致上下文的切換。
1.2 現成的優先級
thread.setPriority()
如果CPU比較忙,設置優先級會使線程獲得更多的時間片,不忙的話則幾乎無影響,不要再程序設計中使用線程優先級綁定某些特定的業務或者讓業務嚴重依賴於線程優先級。
線程的優先級不能大於1也不能小於10,如果線程的優先級大於線程所在group的優先級,那麼線程的優先級將會失效,取而代之的是group的優先級。
線程默認的優先級和所在的線程組優先級一致,main線程的優先級是5。
1.3 獲取線程ID與當前線程
getID()可以獲取線程ID,線程的ID在整個JVM進程中都會是唯一的,並且是從0開始遞增。
Thread.currentThread()可以獲取當前線程
1.4 interrupt
使用wait、sleep,join使得線程進入阻塞狀態時,另一個線程調用阻塞線程的interrupt方法,可以打斷阻塞,一旦線程在阻塞的情況下被打斷,都會拋出一個InterruptedException的異常。
interrupt方法到底做了什麼事呢?在線程內部有interrupt flag標識:
如果一個線程被interrupt,那麼它的flag將被設置。
如果線程正在執行可中斷方法被阻塞時,調用interrupt方法將其中斷,反而會導致flag被清除。
isInterrupted方法用來判斷當前線程是否被中斷,該方法僅是堆interrupt標識的一個判斷,當線程沒有被interrupt時,調用isInterrupted方法顯示是false,如果線程沒執行可中斷方法被阻塞時調用isInterrupted後顯示是true,如果線程執行可中斷方法被阻塞時調用isInterrupted後顯示是false,因爲會擦除flag。
interrupted也用於判斷當前線程是否被中斷,不同於isInterrupted的是,它會直接擦除線程的interrupt標識,需要注意的是,如果線程被打斷了,第一次調用interrupted會返回true,並且立即擦除interrupt標識,以後調用永遠都會返回false。
如果一個線程設置了interrupt標識,當執行到可中斷方法時,可中斷方法會立刻中斷並拋出異常。
1.5 join
B線程join線程A,會使B線程進入等待,直到A線程結束生命週期,或者到達給定時間。
應用場景:當我們像三個不同的網站請求航班信息,當請求信息結束後,將獲得信息進行整理然後返回給用戶。
一個一個請求網站太麻煩,如果併發請求可能不知道什麼時候能請求玩,這時我們可以使用join,等待三個線程請求完後再執行主線程進行整理。
1.6 關閉一個線程
有個stop方法,但是已被Deprecated了,因爲該方法在關閉時可能不會釋放掉monitor鎖,所以線程關閉有如下3個方法:
1.等待線程自然結束
2.捕獲中斷信號關閉線程。此方式派生成本較高,當線程循環執行某任務時,比如心跳檢查,不斷地接收網絡消息報文等,系統決定退出,可以藉助中斷線程的方式使其退出。
通過線程中用while(!isInterrupted){} 或者 try {} catch (InterruptedException e){ }來判斷是否退出,然後調用thread.interrupt來退出線程。
3.使用volatile開關控制。由於線程的interrupt標識可能被擦除,所以使用volatile修飾開關的flag關閉線程也是一種常用的方法。
while(!closed&&isInterrupted()){ //正在運行}
public void close(){
this.closed = true;
this.interrupt();
}
1.7 線程假死
進程假死就是進程雖然存在,但沒有日誌輸出,程序不進行任何的作業,看起來像死了一樣,但實際上它是沒有死的,絕大部分的原因就是某個線程阻塞了,線程出現了死鎖的情況。
第二章 線程安全與數據同步
2.1 共享數據不一致的問題
多個線程操作一個共享數據時可能會出現數據不一致的問題(具體爲啥自行百度)。
2.2 synchronized
可以採用synchronized關鍵字解決。
synchronized提供了一種鎖的機制,確保共享變量的互斥訪問,從而防止數據不一致問題出現。
synchronized關鍵字包括monitor eneter和monitor exit兩個JVM指令,它能保證在任何時候線程執行到monitor enter成功之前都必須從主內存中獲取數據,而不是從緩存中,在monitor exit運行成功之後,共享變量被更新後的值必須刷入主內存。
synchronized的指令嚴格遵守java happens-before規則,一個monitor exit指令之前必定要有一個monitor enter。
2.3 synchronized的用法
synchronized可以對代碼塊或者方法進行修飾,而不能對class以及變量進行修飾。
同步方法:
public synchronized void sync(){
}
public synchronized static void staticSync(){
}
同步代碼塊:
private final Object MUTEX = new Object()
public void sync()
{
synchronized(MUTEX)
{
}
}
2.4 深入分析synchronized關鍵字
synchronized關鍵字提供了一種互斥機制,同一時刻,只能有一個線程訪問同步資源,準確的說是某線程獲取了與mutex關聯的鎖。
將加了synchronized的字節碼進行反編譯,發現加了synchronized的代碼先會獲取mutex引用,然後執行monitor enter指令,然後執行synchronized裏的邏輯後,再執行monitor exit退出。
monitor enter:
每個對象都與monitor相關聯,當線程嘗試獲取與對象關聯的monitor時會發生幾件事情:
如果monitor計數器爲0,說明該monitor的lock還沒被獲得,某個線程獲得後立刻對該計數器加1,從此該線程就是這個monitor的擁有者了;如果一個已擁有monitor的線程重入,則會導致monitor計數器再次累加;如果monitor已被其它線程擁有,其它線程在嘗試獲取monitor所有權時,會陷入阻塞直到monitor計數器變爲0,纔再次嘗試。
注:關於具體monitor的實現原理,後面章節會進行講解
monitor exit:
想要釋放某個對象關聯的monitor的所有權的前提是,曾經獲得所有權,釋放過程較簡單,即將monitor計數器減一,與此同時,被該monitor block的線程將再次嘗試獲取對monitor的所有權。
2.5 使用synchronized需要注意的問題
1.與monitor關聯的對象不能爲空
2.synchronized作用域太大
3.不同的monitor企圖鎖相同的方法,即不同的對象monitor鎖一個方法,這樣沒用,需要一個對象的monitor鎖一個方法。
synchronized修飾某個方法時其實是用的this的monitor
synchronized修飾靜態方法時用的是類.class的實例引用作爲monitor,如:
public class classMonitor{
public static void method1(){
synchronized(classMonitor.class) { //...}
}
public synchronized static void method2(){ //... }
}
//以上兩個synchronized需要獲取的是同一個對象的monitor鎖,這個對象是classMonitor的實例引用
2.5.1 類.class的實例簡介
比如爲什麼classMonitor.class是一個對象的引用,這個對象是什麼時候建立的?有什麼作用?
參考:
每一個類都有一個Class對象,每當編譯一個新類就產生一個Class對象,基本類型 (boolean, byte, char, short, int, long, float, and double)有Class對象,數組有Class對象,就連關鍵字void也有Class對象(void.class)。Class對象對應着java.lang.Class類,如果說類是對象抽象和集合的話,那麼Class類就是對類的抽象和集合。
Class類沒有公共的構造方法,Class對象是在類加載的時候由Java虛擬機以及通過調用類加載器中的 defineClass 方法自動構造的,因此不能顯式地聲明一個Class對象。一個類被加載到內存並供我們使用需要經歷如下三個階段:
-
加載,這是由類加載器(ClassLoader)執行的。通過一個類的全限定名來獲取其定義的二進制字節流(Class字節碼),將這個字節流所代表的靜態存儲結構轉化爲方法去的運行時數據接口,根據字節碼在java堆中生成一個代表這個類的java.lang.Class對象。
-
鏈接。在鏈接階段將驗證Class文件中的字節流包含的信息是否符合當前虛擬機的要求,爲靜態域分配存儲空間並設置類變量的初始值(默認的零值),並且如果必需的話,將常量池中的符號引用轉化爲直接引用。
-
初始化。到了此階段,才真正開始執行類中定義的java程序代碼。用於執行該類的靜態初始器和靜態初始塊,如果該類有父類的話,則優先對其父類進行初始化。
所有的類都是在對其第一次使用時,動態加載到JVM中的(懶加載)。當程序創建第一個對類的靜態成員的引用時,就會加載這個類。使用new創建類對象的時候也會被當作對類的靜態成員的引用。因此java程序程序在它開始運行之前並非被完全加載,其各個類都是在必需時才加載的。這一點與許多傳統語言都不同。動態加載使能的行爲,在諸如C++這樣的靜態加載語言中是很難或者根本不可能複製的。
在類加載階段,類加載器首先檢查這個類的Class對象是否已經被加載。如果尚未加載,默認的類加載器就會根據類的全限定名查找.class文件。在這個類的字節碼被加載時,它們會接受驗證,以確保其沒有被破壞,並且不包含不良java代碼。一旦某個類的Class對象被載入內存,我們就可以它來創建這個類的所有對象。
答:每一個類都有一個Class對象;Class類沒有公共的構造方法,Class對象是在類加載的時候由Java虛擬機以及通過調用類加載器中的 defineClass 方法自動構造的,因此不能顯式地聲明一個Class對象;一旦某個類的Class對象被載入內存,我們就可以它來創建這個類的所有對象。
2.6 程序死鎖的原因以及如何診斷
可能導致死鎖的情況:
1.交叉鎖可導致程序出現死鎖
A持有R1鎖等待R2鎖,B持有R2鎖等待R1鎖
2.內存不足
兩個線程T1,T2都需要30MB內存,T1獲取了10M,T2獲取了10M,但剩下的內存只有10M了。
3.一問一答式的數據交換
服務端開啓端口等待客戶端請求,客服端發送請求等待服務端返回數據,服務端錯過了請求,然後變成了客戶端等待服務端返回數據,服務端等待客戶端請求數據。
4.數據庫鎖
如某個線程執行for update語句退出了事務,其它線程訪問數據庫都將陷入死鎖。
5.文件鎖
同理,某線程獲得文件鎖後意外退出,其它讀取文件的線程也將進入死鎖直到線程釋放文件句柄資源。
6.死循環引起的死鎖
處理不當進入死循環,查看你線程堆棧不會發現任何死鎖的跡象,但是程序不工作,CPU佔有率居高不下,一般稱之爲假死,是一種最致命也是最難排查的死鎖現象。
死鎖診斷:
交叉鎖引起的死鎖:可以打開jstack工具或者jconsole工具,Jstack-1PID會直接發現死鎖信息。
死循環引起的死鎖:也可以使用jstack、jconsole、jvisualvm、jProfiler進行診斷,但是不會有明顯的提示,因爲線程並未BLOCKED,而是始終處於RUNNABLE狀態,CPU使用率居高不下,甚至不能夠正常執行命令。使用jprofile可以發現某個方法線程運行狀態,如果某普通方法運行時間過長如100ms,可能是死循環。