併發程式設計-基礎
執行緒基礎
執行緒的三種建立方法
繼承Thread類.java不支援多繼承,子類不能繼承其他類
實現runnable介面:還可以繼承其他類
實現callable介面:有返回值
執行緒相關JVM語句
cmder 常用語句記錄:
軟體:cmder 就是個cmd,就是個命令列,不過她是支援複製貼上的
jcmd:
傳送診斷命令請求到正在執行的Java虛擬機器(JVM)。它必須和JVM執行在同一臺機器上,並且與啟動JVM使用者具有相同的組許可權。
jcmd 31275 Thread.print -l # 列印執行緒棧 jcmd 31275 VM.command_line # 列印啟動命令及引數 jcmd 31275 GC.heap_dump /data/31275.dump # dump heap jcmd 31275 GC.class_histogram #檢視類的統計資訊 jcmd 31275 VM.system_properties #檢視系統屬性內容 jcmd 31275 VM.uptime #檢視虛擬機器啟動時間 jcmd 31275 PerfCounter.print #檢視效能統計
jps -lvm 檢視所有在本機上執行java的程序
常用解析:
jstack
jstack -l 9552
打印出所有的執行緒堆疊
一般第一個就是銷燬執行緒,沒有daemon 守護執行緒標記,是一個使用者執行緒,是等待所有使用者執行緒執行完畢,用來銷燬java虛擬機器的執行緒
join()
注意:
join方法為Thread類直接提供的方法,而wait和notify為Object類中的方法
等待執行緒執行終止之後,繼續執行,比如
Thread t1 = new Thread(); t1.start(); t1.join(1000);//毫秒時間 System.out.println("t1's state:"+t1.getState());
就是先將t1執行緒start,有join,就阻塞主執行緒
這裡是主執行緒阻塞1秒,過了這個時間就繼續執行後面的程式碼
join()和join(1000)的區別
thread.Join把指定的執行緒加入到當前執行緒,可以將兩個交替執行的執行緒合併為順序執行的執行緒。
比如線上程B中呼叫了執行緒A的Join()方法,直到執行緒A執行完畢後,才會繼續執行執行緒B。
t.join(); //呼叫join方法,等待執行緒t執行完畢
t.join(1000); //等待 t 執行緒,等待時間是1000毫秒。
原始碼解析:
/** * Waits at most {@code millis} milliseconds for this thread to * die. A timeout of {@code 0} means to wait forever. * * <p> This implementation uses a loop of {@code this.wait} calls * conditioned on {@code this.isAlive}. As a thread terminates the * {@code this.notifyAll} method is invoked. It is recommended that * applications not use {@code wait}, {@code notify}, or * {@code notifyAll} on {@code Thread} instances. * * @param millis * the time to wait in milliseconds * * @throws IllegalArgumentException * if the value of {@code millis} is negative * * @throws InterruptedException * if any thread has interrupted the current thread. The * <i>interrupted status</i> of the current thread is * cleared when this exception is thrown. */ public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
方法解釋:
最多等待 ms 毫秒讓該執行緒死亡。 超時為 0 意味著永遠等待。 此實現使用以 this.isAlive 為條件的 this.wait 呼叫迴圈。 當執行緒終止時,呼叫 this.notifyAll 方法。 建議應用程式不要在 Thread 例項上使用 wait、notify 或 notifyAll。
底層呼叫isAlive()方法,
Tests if this thread is alive. A thread is alive if it has been started and has not yet died.
Returns:
true if this thread is alive; false otherwise.
測試此執行緒是否存活。 如果執行緒已啟動且尚未死亡,則該執行緒處於活動狀態。
返回:
如果此執行緒還活著,則為真; 否則為假。
總結:也就是如果t.join()就和wait類似,主執行緒要等待這個t執行緒結束才能執行下去,底層呼叫object的wait()方法
join(xxx)表示最多等待xxx毫秒,如果在這個時間內執行緒執行完畢,則立即執行join後面的程式碼。
sleep()
不釋放鎖——原子性
Thread類中的一個靜態方法,暫時讓出執行權,不參與CPU排程,但是。時間到了就進入到就緒狀態,一旦獲取到CPU時間片,則繼續執行。
是一個native static方法
可以用中斷異常
t1.interrupt();
yield()—假裝客氣
不釋放鎖——原子性
知識點:Thread類中的靜態native方法;讓出剩餘的時間片,本身進入就緒狀態,CPU再次排程還可能排程到本執行緒。
易混淆知識點:sleep是在一段時間內進入阻塞狀態,cpu不會排程它。而yield是讓出執行權,本身還處於就緒狀態,cpu還可能立即排程它,可以不理會它
wait()/notify-object
釋放鎖
notify不釋放鎖
有且僅有wait會釋放鎖
wait一般與synchronize+notify連用,因為會釋放鎖
interrupt()
執行緒中斷
知識點:執行緒中斷是執行緒間的一種協作模式,通過設定執行緒中斷標誌來實現,執行緒根據這個標誌來自行處理。
public void interrupt() 中斷標誌設定為true
public boolean isInterrupted() 獲取中斷標記
public static boolean interrupted() 將中斷標記取反,會改變當前執行緒的中斷標記狀態
獲取當前執行緒的狀態:容易混淆!,在主函式中就是獲取main執行緒
會導致sleep和join執行緒丟擲中斷異常
執行緒安全問題原理
可見性和原子性
1.執行緒安全的概念:當多個執行緒訪問某一個類、物件或方法時,這個類、物件或方法都能表現出與單
執行緒執行時一致的行為,那麼這個類、物件或方法就是執行緒安全的。
2.執行緒安全問題都是由全域性變數及靜態變數引起的。
3.若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒
安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。
synchronized關鍵字
原理就是保證原子性,維護可見性,維護秩序。
Synchronized的作用是加鎖,所有的synchronized方法都會順序執行,(這裡只佔用CPU的順序)。
Synchronized方法執行方式:
– 首先嚐試獲得鎖
– 如果獲得鎖,則執行Synchronized的方法體內容。
– 如果無法獲得鎖則等待,並且不斷的嘗試去獲得鎖,一旦鎖被釋放,則多個執行緒會同時去嘗
試獲得所,造成鎖競爭問題。
鎖競爭問題,在高併發、執行緒數量高時會引起CPU佔用居高不下,或者直接宕機。
物件鎖和類鎖--區別
Synchronized作用在非靜態方法上代表的物件鎖,一個物件一個鎖,多個物件之間不會發生鎖競爭。
Synchronized作用在靜態方法上則升級為類鎖,所有物件共享一把鎖,存在鎖競爭。
比如
User user1 = new User();
User user2 = new User();
synchronized(use1)
synchronized(use2)
鎖是不同的物件,不同物件之間吧劊發生鎖競爭
但是如果 static synchronize 是類鎖的話就會發生競爭
原理是類鎖的話,靜態變數和靜態方法是儲存在方法區中的,訪問的是同一個資料
但是物件鎖而言,物件是儲存在堆中的,比如上述的user1和user2都在堆中是不同的物件,不是同一個資源,所以不存在鎖競爭
一個類裡面如果方法上有synchronize表示當前的鎖是當前類物件
物件鎖的同步和非同步
同步:必須等待方法執行完畢,才能向下執行,共享資源訪問的時候,為了保證執行緒安全,必須同步。
非同步:不用等待其他方法執行完畢,即可立即執行,例如Ajax非同步。
物件鎖只針對synchronized修飾的方法生效、 物件中的所有synchronized方法都會同步執行、而非
synchronized方法非同步執行
避免誤區:類中有兩個synchronized方法,兩個執行緒分別呼叫兩個方法,相互之間也需要競爭鎖,
因為兩個方法從屬於一個物件,而我們是在物件上加鎖
髒讀問題
多個執行緒訪問同一個資源,在一個執行緒修改資料的過程中,有另外的執行緒來讀取資料,就會引起髒
讀的產生。
為了避免髒讀我們一定要保證資料修改操作的原子性、並且對讀取操作也要進行同步控制
鎖重入
同一個執行緒得到了一個物件的鎖之後,再次請求此物件時可以再次獲得該物件的鎖。
父子類可以鎖重入
public class DemoThread05{
public synchronized void run1(){
System.out.println(Thread.currentThread().getName()+">run1...");
//呼叫同類中的synchronized方法不會引起死鎖
run2();
}
public synchronized void run2(){
System.out.println(Thread.currentThread().getName()+">run2...");
}
public static void main(String[] args) {
final DemoThread05 demoThread05 = new DemoThread05();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demoThread05.run1();
}
});
thread.start();
}
}
拋異常和鎖的關係
一個執行緒在獲得鎖之後執行操作,發生錯誤丟擲異常,則自動釋放鎖
1、可以利用丟擲異常,主動釋放鎖
2、程式異常時防止資源被死鎖、無法釋放
3、異常釋放鎖可能導致資料不一致
synchronized程式碼塊
可以達到更細粒度的控制
當前物件鎖
類鎖
任意物件鎖
總結:
同類型鎖之間互斥,不同型別的鎖之間互不干擾
不要線上程內修改物件鎖的引用
不能修改鎖的指向地址。如果是物件,修改物件裡的值是不會引起鎖失效的
併發和死鎖
是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現
象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永
遠在互相等待的程序稱為死鎖程序。
執行緒間的通訊
每個執行緒都是獨立執行的個體,執行緒通訊能讓多個執行緒之間協同工作
Object類中的wait/notify方法可以 實現執行緒間通訊
Wait/notify必須與synchronized一同使用
Wait釋放鎖、notify不釋放鎖
notify與notifyAll的區別
notify本身不釋放鎖
Notify只會通知一個wait中的執行緒,並把鎖給他,不會產生鎖競爭問題,但是該執行緒處理完畢之後
必須再次notify或notifyAll,完成類似鏈式的操作。
NotifyAll會通知所有wait中的執行緒,會產生鎖競爭問題。
notify的通知依賴底層JVM的實現,這裡notify依賴的是native方法,找到原始碼,目前是先進先出,就是最早wait()的先被喚醒。
synchronized的wait和notify是位於ObjectMonitor.cpp中
這裡實際上是將_WaitSet中的第一個元素進行出隊操作
原來hotspot對notofy()的實現並不是我們以為的隨機喚醒, 而是“先進先出”的順序喚醒!`
實戰:實現阻塞式執行緒安全佇列
使用synchronized、wait、notify實現帶阻塞的執行緒安全佇列
功能描述:
功能1:在佇列元素滿的時候,put阻塞,在佇列元素空的時候,get阻塞
功能2 :執行緒安全
程式碼:DemoThread20
視訊中的程式碼 仍然有bug,不過我已經改正了
while (this.list.size() == this.maxSize) {
lock.wait();
}
問題就是wait中的執行緒被喚醒時和新執行緒競爭的時候破壞了資料的原子性,因為集合是非原子性的
守護執行緒和使用者執行緒
執行緒分類:daemon執行緒(守護執行緒)、user執行緒(使用者執行緒)
易混淆知識點:main函式所在的執行緒就是一個使用者執行緒
重要知識點1:最後一個user執行緒結束時,JVM會正常退出,不管是否有守護執行緒正在執行。反過來說:只要有一個使用者執行緒還沒結束,JVM程序就不會結束。
重要知識點2:父執行緒結束後,子執行緒還可以繼續存活,子執行緒的生命週期不受父執行緒的影響
shutdown和kill的區別
shutdown會將所有正在執行的請求執行完畢,而kill不會,尤其是kill -9
執行緒上下文切換
CPU採用時間片輪巡的策略,執行緒切換的過程就是上下文切換
當前執行緒使用完時間片後就會進入就緒狀態,讓出CPU執行權給其他執行緒,此時就是從當前執行緒的上下文切換到了其他執行緒。
當發生上下文切換的時候需要儲存執行現場,待下次執行時進行恢復。
所以頻繁的、大量的上下文切換會造成一定資源開銷
驗證
shutdown會將所有controller正在執行的請求執行完畢,而kill不會