蘇州JAVA數(shù)據(jù)庫培訓?
來源:教育聯(lián)展網(wǎng) 編輯:佚名 發(fā)布時間:2018-04-08
蘇州JAVA數(shù)據(jù)庫培訓
前言
借用 Java 并發(fā)編程實踐中的話:編寫正確的程序并不容易,而編寫正常的并發(fā)程序就更難了;相比于順序執(zhí)行的情況,多線程的線程安全問題是微妙而且出乎意料的,因為在沒有進行適當同步的情況下多線程中各個操作的順序是不可預期的。
并發(fā)編程相比 Java 中其他知識點學習起來門檻相對較高,學習起來比較費勁,從而導致很多人望而卻步;而無論是職場面試和高并發(fā)高流量的系統(tǒng)的實現(xiàn)卻都還離不開并發(fā)編程,從而導致能夠真正掌握并發(fā)編程的人才成為市場比較迫切需求的。
本 Chat 作為 Java 并發(fā)編程之美系列的開篇,首先**通俗易懂的方式先來和大家聊聊多線程并發(fā)編程線程有關(guān)基礎(chǔ)知識(本文結(jié)合示例進行講解,定會讓你耳目一新),具體內(nèi)容如下:
什么是線程?線程和進程的關(guān)系。
線程創(chuàng)建與運行。創(chuàng)建一個線程有那幾種方式?有何區(qū)別?
線程通知與等待,多線程同步的基礎(chǔ)設(shè)施。
線程的虛假喚醒,以及如何避免。
等待線程執(zhí)行終止的 join 方法。想讓主線程在子線程執(zhí)行完畢后在做一點事情?
讓線程睡眠的 sleep 方法,sleep 的線程會釋放持有的鎖?
線程中斷。中斷一個線程,被中斷的線程會自己終止?
理解線程上下文切換。線程多了一定好?
線程死鎖,以及如何避免。
守護線程與用戶線程。當 main 函數(shù)執(zhí)行完畢,但是還有用戶線程存在的時候,JVM 進程會退出?
什么是線程
在討論什么是線程前有必要先說下什么是進程,因為線程是進程中的一個實體,線程本身是不會獨立存在的。進程是代碼在數(shù)據(jù)集合上的一次運行活動,是系統(tǒng)進行資源分配和調(diào)度的基本單位,線程則是進程的一個執(zhí)行路徑,一個進程至少有一個線程,進程中的多個線程是共享進程的資源的。
操作系統(tǒng)在分配資源時候是把資源分配給進程的,但是 CPU 資源就比較特殊,它是分派到線程的,因為真正要占用 CPU 運行的是線程,所以也說線程是 CPU 分配的基本單位。
Java 中當我們啟動 main 函數(shù)時候其實就啟動了一個 JVM 的進程,而 main 函數(shù)所在線程就是這個進程中的一個線程,也叫做主線程。
如圖一個進程中有多個線程,多個線程共享進程的堆和方法區(qū)資源,但是每個線程有自己的程序計數(shù)器,棧區(qū)域。
其中程序計數(shù)器是一塊內(nèi)存區(qū)域,用來記錄線程當前要執(zhí)行的指令地址,那么程序計數(shù)器為何要設(shè)計為線程私有的呢?前面說了線程是占用 CPU 執(zhí)行的基本單位,而 CPU 一般是使用時間片輪轉(zhuǎn)方式讓線程輪詢占用的,所以當前線程 CPU 時間片用完后,要讓出 CPU,等下次輪到自己時候在執(zhí)行,那么如何知道之前程序執(zhí)行到哪里了?其實程序計數(shù)器就是為了記錄該線程讓出 CPU 時候的執(zhí)行地址,待再次分配到時間片時候就可以從自己私有的計數(shù)器指定地址繼續(xù)執(zhí)行了。
另外每個線程有自己的棧資源,用于存儲該線程的局部變量,這些局部變量是該線程私有的,其它線程是訪問不了的,另外棧還用來存放線程的調(diào)用棧幀。
堆是一個進程中**大的一塊內(nèi)存,堆是被進程中的所有線程共享的,是進程創(chuàng)建時候分配的,堆里面主要存放使用 new 操作創(chuàng)建的對象實例。
方法區(qū)則是用來存放進程中的代碼片段的,是線程共享的。
線程創(chuàng)建與運行
Java 中有三種線程創(chuàng)建方法,分別為實現(xiàn) Runnable 接口的run方法、繼承 Thread 類并重寫 run 方法、使用 FutureTask 方式。
當調(diào)用了 start 方法后才是真正啟動了線程。其實當調(diào)用了 start 方法后線程并沒有馬上執(zhí)行而是處于就緒狀態(tài),這個就緒狀態(tài)是指該線程已經(jīng)獲取了除 CPU 資源外的其它資源,等獲取 CPU 資源后才會真正處于運行狀態(tài)。
當 run 方法執(zhí)行完畢,該線程就處于終止狀態(tài)了。使用繼承方式好處是 run 方法內(nèi)獲取當前線程直接使用 this 就可以,無須使用 Thread.currentThread() 方法,不好的地方是 Java 不支持多繼承,如果繼承了 Thread 類那么就不能再繼承其它類,另外任務與代碼沒有分離,當多個線程執(zhí)行一樣的任務時候需要多份任務代碼,而 Runable 則沒有這個限制。
注:每種方式都有自己的優(yōu)缺點,應該根據(jù)實際場景進行選擇。
線程通知與等待
下面講解下 Object 中關(guān)于線程同步的通知等待函數(shù)。
void wait() 方法
當一個線程調(diào)用一個共享對象的 wait() 方法時候,調(diào)用線程會被阻塞掛起,直到下面幾個事情之一發(fā)生才返回:
其它線程調(diào)用了該共享對象的 notify() 或者 notifyAll() 方法;
其它線程調(diào)用了該線程的 interrupt() 方法設(shè)置了該線程的中斷標志,該線程會拋出 InterruptedException 異常返回。
另外需要注意的是如果調(diào)用 wait() 方法的線程沒有事先獲取到該對象的監(jiān)視器鎖,則調(diào)用 wait() 方法時候調(diào)用線程會拋出 IllegalMonitorStateException 異常。
那么一個線程如何獲取到一個共享變量的監(jiān)視器那?
(1)執(zhí)行使用 synchronized 同步代碼塊時候,使用該共享變量作為參數(shù):
(2)調(diào)用該共享變量的方法,并且該方法使用了 synchronized 修飾:
另外需要注意的是一個線程可以從掛起狀態(tài)變?yōu)榭梢赃\行狀態(tài)(也就是被喚醒)即使該線程沒有被其它線程調(diào)用 notify(),notifyAll() 進行通知,或者被中斷,或者等待超時,這就是所謂的虛假喚醒。
雖然虛假喚醒在應用實踐中很少發(fā)生,但是還是需要防范于未然的,做法就是不停的去測試該線程被喚醒的條件是否滿足,不滿足則繼續(xù)等待,也就是說在一個循環(huán)中去調(diào)用 wait() 方法進行防范,退出循環(huán)的條件是條件滿足了喚醒該線程。
共享變量 wait() 方法**同步塊獲取 obj 上面的監(jiān)視器鎖,然后** while 循環(huán)內(nèi)調(diào)用 obj 的 wait() 方法。
另外當一個線程調(diào)用了共享變量的 wait() 方法后該線程會被掛起,同時該線程會暫時釋放對該共享變量監(jiān)視器的持有,直到另外一個線程調(diào)用了共享變量的 notify() 或者 notifyAll() 方法才有可能會重新獲取到該共享變量的監(jiān)視器的持有權(quán)(這里說有可能,是因為考慮到多個線程**次都調(diào)用了 wait() 方法,所以多個線程會競爭持有該共享變量的監(jiān)視器)。
借用上面這個例子來講解下調(diào)用共享變量 wait() 方法后當前線程會釋放持有的共享變量的鎖的理解。
如上代碼假如生產(chǎn)線程 A 首先** synchronized 獲取到了 queue 上的鎖,那么其它生產(chǎn)線程和所有消費線程都會被阻塞,線程 A 獲取鎖后發(fā)現(xiàn)當前隊列已滿會調(diào)用 queue.wait() 方法阻塞自己,然后會釋放獲取的 queue 上面的鎖,這里考慮下為何要釋放該鎖?
如果不釋放,由于其它生產(chǎn)線程和所有消費線程已經(jīng)被阻塞掛起,而線程 A 也被掛起,這就處于了死鎖狀態(tài)。這里線程 A 掛起自己后釋放共享變量上面的鎖就是為了打破死鎖必要條件之一的持有并等待原則。關(guān)于死鎖下面章節(jié)會有講到,線程 A 釋放鎖后其它生產(chǎn)線程和所有消費線程中會有一個線程獲取 queue 上的鎖進而進入同步塊,這就打破了死鎖。
void wait(long timeout) 方法
需要注意的是如果在調(diào)用該函數(shù)時候 timeout 傳遞了負數(shù)會拋出 IllegalArgumentException 異常。
void wait(long timeout, int nanos) 方法
void notify() 方法
另外被喚醒的線程不能馬上從 wait 返回繼續(xù)執(zhí)行,它必須獲取了共享對象的監(jiān)視器后才可以返回,也就是喚醒它的線程釋放了共享變量上面的監(jiān)視器鎖后,被喚醒它的線程也不一定會獲取到共享對象的監(jiān)視器,這是因為該線程還需要和其它線程一塊競爭該鎖,只有該線程競爭到了該共享變量的監(jiān)視器后才可以繼續(xù)執(zhí)行。
類似 wait 系列方法,只有當前線程已經(jīng)獲取到了該共享變量的監(jiān)視器鎖后,才可以調(diào)用該共享變量的 notify() 方法,否者會拋出 IllegalMonitorStateException 異常。
void notifyAll() 方法
咨詢聯(lián)系方式:13861302024(楊老師)或者QQ:2589245390 還可以直接在線咨詢
更多JAVA課程推薦:
Java 中 Object 類是所有類的父類,鑒于繼承機制,Java 把所有類都需要的方法放到了 Object 類里面,其中就包含本節(jié)要講的通知等待系列函數(shù),這些通知等待函數(shù)是組成并發(fā)包中線程同步組件的基礎(chǔ)。
首先談下什么是共享資源,所謂共享資源是說該資源被多個線程共享,多個線程都可以去訪問或者修改的資源。另外本文當講到的共享對象就是共享資源。
synchronized(共享變量){
//doSomething
}
synchronized void add(int a,int b){
//doSomething}
該方法相比 wait() 方法多一個超時參數(shù),不同在于如果一個線程調(diào)用了共享對象的該方法掛起后,如果沒有在指定的 timeout ms 時間內(nèi)被其它線程調(diào)用該共享變量的 notify() 或者 notifyAll() 方法喚醒,那么該函數(shù)還是會因為超時而返回。
內(nèi)部是調(diào)用 wait(long timeout),如下代碼:只是當 nanos>0 時候讓參數(shù)一遞增1。
一個線程調(diào)用共享對象的 notify() 方法后,會喚醒一個在該共享變量上調(diào)用 wait 系列方法后被掛起的線程,一個共享變量上可能會有多個線程在等待,具體喚醒哪一個等待的線程是隨機的。
不同于 nofity() 方法在共享變量上調(diào)用一次就會喚醒在該共享變量上調(diào)用 wait 系列方法被掛起的一個線程,notifyAll() 則會喚醒所有在該共享變量上由于調(diào)用 wait 系列方法而被掛起的線程。