實現并發最直接的方式是在操作系統級別使用進程。進程是運行在它自己地址空間內的自包容的程序
線程就是在進程中的一個單一的順序控制流,因此,單個進程可以擁有多個并發執行的任務。
靜態方法Thread.yield()的調用是對線程調度器(java線程機制的一部分,可以將CPU從一個線程轉移給另一個線程)的一種建議。
線程調度機制是非確定性的,所以一次運行的結果可能與另一次運行的結果不同 當創建Thread時,它并沒有捕獲任何對這些對象的引用。在使用普通對象時,這對于GC來說是公平的,但對于Thread時,情況就不同了。每個Thread都“注冊了自己”,因此確實會有一個對它的引用,而且在它的任務退出其run()并死亡之前,GC都無法清除它,因此,一個線程會創建一個單獨的執行線程,在對start()調用完成之后,它仍然會繼續存在
Executor:java.util.concurrent包中的執行器將為你管理Thread對象,從而簡化了并發編程。Exexutor在客戶端和任務執行之間提供了一個間接層;與客戶端直接執行任務不同,這個中介對象將執行任務。
Executor允許你管理異步任務的執行,而無須顯式地管理線程的生命周期
FixedThreadPool:一次性預先執行代價高昂的線程分配,從而限制線程的數量。Executors.newFixedThreadPool(5)
在任何線程池中,現有線程在可能的情況下,都會被自動復用
SingleThreadExecutor提交了多個任務,那么這些任務將排隊,每個任務都會在下一個任務開始之前運行結束,所有的任務將使用相同的線程。會序列化所有提交給它的任務,并會維護它自己(隱藏)的懸掛任務隊列
優先級:線程的優先級將該線程的重要性傳遞給調度器。盡管CPU處理現有線程集的順序是不確定的,但是調度器將傾向于讓優先權最高的線程先執行。然而,這并不意味著優先權較低的線程將得不到執行(優先權不會導致死鎖)。優先級較低的線程僅僅是執行的頻率較低
JDK有10個優先級,但它與多數操作系統都不能映射的很好。比如,Windows有7個優先級并且不是固定的,所以這種映射關系也是不確定的。Sun的Solaris有2的31次方個優先級。唯一可移植的方法是當調整優先級的時候,只使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三種級別
通過yield()方法來作出暗示:你的工作已經做的差不多了,可以讓別的線程使用CPU了(沒有任何機制保證它將會被采納)。調用時,你也是在建議具有相同優先級的其他線程可以運行。但大體上,對于任何重要的控制或在調整應用時,都不能依賴于yield(),實際上,yield()經常被誤用。
后臺線程是指在程序運行的時候在后臺提供一種通用服務的線程,并且這種線程并不屬于程序中不可或缺的部分。因此,當所有非后臺線程結束時,程序也就終止了,同時殺死所有后臺線程。反過來說,只要有任何非后臺線程還在運行,程序就不會終止。main()就是一個非后臺線程
當最后一個非后臺線程終止時,后臺線程會“突然”終止。因此一旦main()退出,JVM就會關閉所有的后臺進程,而不會有任何你希望出現的確認形式。
* join()方法*:如果某個線程在另一個線程t上調用t.join(),此線程將被掛起,直到目標線程t結束才恢復(即t.isAlive()返回為假)
解決共享資源競爭:對于某個對象來說,其所有synchronized方法共享一個鎖,這可以被用來防止多個任務同時訪問被編碼為對象內存。
使用并發時,將域設置為private是非常重要的,否則synchronized關鍵字就不能防止其他任務直接訪問域,這樣就會產生沖突
一個任務可以多次獲得對象的鎖。如果一個方法在同一個對象上調用了第二個方法,后者又調用了同一對象上的另一個方法,就會發生這種情況。JVM負責跟蹤對象被加鎖的次數。如果變為1。每當這個相同的任務在這個對象上獲得鎖時,計數都會遞增。顯然,只有首先獲得了鎖的時候,鎖被完全釋放,此時別的任務就可以使用此資源
針對每個類,也有一個鎖(作為Class對象的一部分),所以synchronized static方法可以在類的范圍內防止對static數據的并發訪問
如果你正在寫一個變量,它可能接下來將被另一個線程讀取,或者正在讀取一個上一次已經被另一個線程寫過的變量,那么你必須使用同步,并且,讀寫線程都必須用相同的監視器鎖同步
如果你的類中有超過一個方法在處理臨界數據,那么你必須同步所有相關的方法。如果只同步一個方法,那么其他方法將會隨意地忽略這個對象鎖,并可以在無任何懲罰的情況下被調用。這是很重要的一點:每個訪問臨界共享資源的方法都必須被同步,否則它們就不會正確的工作。
原子性可以應用于除long和double之外的所有基本類型之上的“簡單操作”。對于讀取和寫入除long和double之外的基本類型變量這樣的操作,可以保證它們會被當作不可分(原子)的操作來操作內存。
但JVM可以將64位(long 和 double變量)的讀取和寫入當作兩個分離的32為操作來執行,這就產生了在一個讀取和寫入操作中間發生上下文切換,從而導致不同的任務可以看到不正確結果的可能性(字撕裂)
如果在定義long和double變量時,如果使用volatile關鍵字,就會獲得(簡單的賦值與返回操作的)原子性。不同的JVM可以任意地提供更強的保證,但是你不應該依賴于平臺相關的特性。
volatile關鍵字確保了應用中的可視性。如果將一個域聲明為volatile的,那么只要對這個域產生了寫操作,那么所有的操作就都可以看到這個修改。即便使用了本地緩存,情況也是如此,volatile域會立即被寫入到主存中,而讀取操作就發生在主存中。
在非volatile域上的原子操作不必刷新到主存中去,因此其他讀取該域的任務也不必看到這個新值。如果多個任務在同時訪問某個域,那么這個域就應該是volatile的,否則這個域就應該只能經由同步來訪問。同步也會導致向主存中刷新,因此如果一個域完全由synchronized方法或語句塊來防護,那么就不必將其設置為是volatile的
一個任務所作的任何寫入操作對這個任務來說都是可視的,因此如果它只需要在這個任務內部可視,那么就不需要設置為volatile
自遞增自遞減操作不是原子性操作
線程本地存儲:ThreadLocal對象創建時候,只能通過get和set方法訪問該對象內容。get返回當前線程相關聯對象的副本。set會將參數插入到為其線程存儲的對象中,并返回存儲中原有的對象。每個單獨的線程都被分配了自己的存儲。
終結任務: ExecutorService.awaitTermination():等待每個任務結束,如果所有任務在超時之前全部結束,返回true,否則返回false
線程的四種狀態:
新建:當線程被創建時,它只會短暫地處于這種狀態。此時它已經分配了必需的系統資源,并執行了初始化。此刻線程已經有資格獲取CPU時間了,之后調度器將把這個線程轉變為運行狀態或阻塞狀態。就緒:這種狀態下,只要調度器把時間片分配給線程,線程就可以運行。也就是說,在任意時刻,線程可以運行也可以不運行。只要調度器能分配時間片給線程,它就能運行,這不同于阻塞和死亡狀態。阻塞:線程能夠運行,但有某個條件阻止它的運行。當線程處于阻塞狀態時,調度器將忽略線程,不會分配給線程任何CPU時間。直到線程重新進入了就緒狀態,它才有可能執行操作死亡:處于死亡或者終止狀態的線程將不再是可調度的,并且再也不會得到CPU時間,它的任務已結束,或不再是可運行的。任務死亡的方式通常是從run()方法返回,但是任務的線程還可以被中斷。進入阻塞狀態的原因:
調用sleep()調用wait(),直到線程獲得了notify()或notifyAll()(或者在java.util.concurrent類庫中調用等價的signal()或signalAll),才會進入就緒狀態任務在等待某個輸入/輸出完成任務試圖在某個對象上調用同步控制方法,但對象鎖不可用,因為另一個任務已經獲取了這個鎖中斷:
Thread,interrupt(),如果線程被阻塞,或者執行一個阻塞操作,那么設置這個線程的中斷狀態將拋出InterruptedException當拋出該異常或者該任務調用Thread.interrupted時,中斷狀態復位。interrupted()提供了離開run()循環而不拋出異常的第二種方法(第一種是維護一個cancel布爾值進行判斷)Executor.shutdownNow()將發送一個interrupt()調用給它啟動的所有線程。如果使用Executor,調用submit()而不是executor來啟動任務,將持有該任務的上下文。submit()返回一個Future,可以調用cancel()來中斷某個特定任務。將true傳給cancel,就有權限調用該線程的interrupt可以中斷對sleep()的調用(或者任何要求拋出InterruptedException的調用)。但是不能中斷試圖獲取synchronized鎖或者試圖執行I/0操作的線程,它們不需要InterruptedException處理器被互斥所阻塞
一個任務能夠調用在同一個對象中的其他synchronized方法,這個任務已經持有鎖了。也就是說同一個互斥可以被同一個任務多次獲得。ReentrantLock上阻塞的任務具備可以被中斷的能力。檢查中斷
檢查中斷可以通過調用interrupted()來檢查中斷狀態,還可以幫助清除中斷狀態 被設計用來響應interrupt()的類必須建立一種策略,來確保它將保持一致的狀態。這通常意味著所有需要清理的對象創建操作的后面,都必須緊跟try-finally子句,從而使得無論run()循環如何退出,清理都會發生
線程之間的協作
wait()使你可以等待某個條件發生變化,而改變這個條件超出了當前方法的控制能力。wait()提供了一種在任務之間對活動同步的方式。調用wait()時候,線程執行被掛起,對象上的鎖被釋放,對象內其他synchronized方法可以在wait()期間被調用
wait()、notify()以及notifyAll()是基類Object的一部分,而不是作為Thread的一部分,這樣做是有道理的,因為這些方法操作的鎖也是所有對象的一部分
實際上只能在同步控制方法或者同步控制塊中調用wait()、notify()或者notifyAll(),sleep()不用操作鎖,所以可以在非同步控制方法里調用
當notifyAll()因某個特定鎖而被調用時,只有等待這個鎖的任務才會被喚醒。
使用互斥并允許任務掛起的基本類是Condition,你可以通過在Condition上調用await()來掛起一個任務。當外部條件發生變化,意味著某個任務應該繼續執行時,你可以通過調用signal()來通知這個任務,從而喚醒一個任務,或者調用signalAll()來喚醒所有在這個Condition上被其自身掛起的任務(與使用notifyAll相比,signalAll更安全)
使用Lock通常會比使用synchronized高效的多,而且synchronized的開銷看起來變化范圍太大,而Lock相對比較一致。
Vector和HashTable有許多synchronized方法,當它們應用于非多線程的程序中時,將會導致不可接受的開銷。
CopyOnWriteArrayList具有免鎖行為,寫入將導致創建整個底層數組的副本,而源數組將保留在原地,使得復制的數組在被修改的時候,讀取操作可以安全執行。修改完成后,一個原子性的操作將把新的數組換入,使得新的讀取操作可以看到這個新的修改。
CopyOnWriteArraySet將使用CopyOnWriteArrayList來實現免鎖行為,ConcurrentHashMap和ConcurrentLinkedQueue使用類似技術,允許并發的讀取和寫入,但是容器中只有部分內容而不是整個容器可以被復制和修改。然而,任何修改在完成之前,讀取者仍舊不能看到它們,不會拋出ConcurrentModificationException
產生死鎖需要同時滿足的四個條件:
互斥條件至少有一個任務它必須持有一個資源且正在等待獲取一個當前被別的任務持有的資源資源不能被任務搶占必須有循環等待本章源碼地址:并發代碼
新聞熱點
疑難解答