文章目錄
前言
一、初識多執行緒
1.1 並行、併發、序列
1.2 上下文切換
1.2.1 上下分切換的分類
1.2.2 減少上下文切換的方式
1.2.3 上下文切換的最佳化示例
1.3 併發程式設計的優缺點
1.3.1 併發程式設計的優點*
1.3.2 併發程式設計的缺點*
1.4 併發程式設計三要素
1.4.1 原子性*
1.4.2 可見性*
1.4.3 有序性*
1.5 同步與非同步
1.6 程序與執行緒
1.7 執行緒排程
1.8 相關問題
1.8.1 編寫多執行緒程式的時候你會遵循哪些最佳實踐
1.8.2 什麼是執行緒排程器和時間分片
1.8.3 Linux環境下如何查詢哪個執行緒使用CPU最長
1.8.4 多程序和多執行緒的區別
二、執行緒的基本使用
2.1 建立執行緒
2.1.1 繼承Thread類*
2.1.2 實現Runnable介面*
2.1.3 實現Callable介面*
2.1.4 建立執行緒池*
2.1.5 4種建立方式對比
2.2 啟動執行緒
2.2.1 執行緒每次只能使用一次*
2.2.2 執行緒的run和start有什麼區別*
2.2.3 為什麼不能直接呼叫run()方法*
2.2.4 執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
2.3 執行緒屬性
2.3.1 執行緒優先順序*
2.3.2 守護執行緒和使用者執行緒*
2.3.3 執行緒名稱
2.4 執行緒的生命週期*
2.4.1 從程式碼角度理解
2.4.2 從使用角度理解*
2.5 Thread類的常用方法
2.5.1 interrupt*
2.5.1 interrupted*
2.5.2 join*
2.5.3 sleep*
2.5.4 yield
2.6 執行緒相關的一些問題
2.6.1 interrupt、interrupted和isInterrupted方法的區別*
2.6.2 sleep方法和yield方法有什麼區別*
2.6.3 執行緒怎麼處理異常
2.6.4 Thread.sleep(0)的作用是什麼*
2.6.5 一個執行緒如果出現了運行時異常會怎麼樣
2.6.6 終止執行緒執行的幾種情況
2.6.7 如何優雅地設定睡眠時間
2.6.8 如何設定上下文類載入器
2.6.9 如何停止一個正在執行的執行緒
2.6.10 為什麼Thread類的sleep()和yield()方法是靜態的
2.6.11 怎麼檢測一個執行緒是否擁有鎖
2.6.12 執行緒的排程策略
2.6.13 執行緒的排程策略
2.6.14 join可以保證執行緒執行順序的原理
2.6.15 stop()方法和interrupt()方法的區別
2.6.16 有三個執行緒T1,T2,T3,如何保證順序執行*
2.6.17 執行緒中斷是否能直接呼叫stop
三、執行緒的活性故障
3.1 死鎖*
3.1.1 死鎖的產生條件*
3.1.2 死鎖的規避*
3.2 執行緒飢餓和活鎖
3.3 死鎖與活鎖的區別,死鎖與飢餓的區別
本系列文章:
多執行緒(一)執行緒與程序、Thread
多執行緒(二)Java記憶體模型、同步關鍵字
多執行緒(三)執行緒池
多執行緒(四)顯式鎖、佇列同步器
多執行緒(五)可重入鎖、讀寫鎖
多執行緒(六)執行緒間通訊機制
多執行緒(七)原子操作、阻塞佇列
多執行緒(八)併發容器
多執行緒(九)併發工具類
多執行緒(十)多執行緒程式設計示例
前言
計算機的組成
一個程式要執行,首先要被載入到記憶體,然後資料被運送到CPU的暫存器裡。暫存器用來儲存資料;PC為程式計數器,用來記錄要執行的程式的位置;算術邏輯單元執行具體的計算,然後將結果再傳送給記憶體。
CPU執行運算的大致過程:CPU讀取指令,然後程式計數器儲存程式的執行位置,然後從暫存器中讀取原始資料,計算完成後,再將結果返回給記憶體,一直迴圈下去。
執行緒之間的排程由執行緒排程器負責,確定在某一時刻執行哪個執行緒。
執行緒上下文切換,簡單來說,指的是CPU儲存現場,執行新執行緒,恢復現場,繼續執行原執行緒的一個過程。
一、初識多執行緒
多執行緒可以理解為在同一個程式中能夠同時執行多個不同的執行緒來執行不同的任務,這些執行緒可以同時利用CPU的多個核心執行。多執行緒程式設計能夠最大限度的利用CPU的資源。如果某一個執行緒的處理不需要佔用CPU資源時(例如IO執行緒),可以使當前執行緒讓出CPU資源來讓其他執行緒能夠獲取到CPU資源,進而能夠執行其他執行緒對應的任務,達到最大化利用CPU資源的目的。
1.1 並行、併發、序列
併發
多個任務在同一個CPU核上,按細分的時間片輪流執行,從邏輯上來看任務是同時執行。
並行
單位時間內,多個處理器或多核處理器同時處理多個任務,是真正意義上的“同時進行”。
序列
有n個任務,由一個執行緒按順序執行。由於任務、方法都在一個執行緒執行所以不存線上程不安全情況,也就不存在臨界區的問題。
圖示:
可以看出:序列是利用一個資源,依次、首尾相接地把不同的事情做完;併發也是利用一個資源,在做一件事時的空閒時間去做另一件事;並行是投入多個資源,去做多件事。
多執行緒程式設計的實質就是將任務的處理方式由序列改成併發。
1.2 上下文切換
上下文,指某一時間點CPU暫存器和程式計數器的內容。
暫存器,是CPU內部的數量較少但是速度很快的記憶體(與之對應的是 CPU 外部相對較慢的RAM主記憶體)。
程式計數器,是一個專用的暫存器,用於表明指令序列中CPU正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置。
一個CPU核心在任意時刻只能被一個執行緒使用,為了讓這些執行緒都能得到有效執行,CPU採取的策略是交替地為每個執行緒分配時間片,當一個執行緒的時間片用完的時候就會重新處於就緒狀態讓給其他執行緒使用。
概括來說:當前任務在執行完CPU時間片切換到另一個任務之前會先儲存自己的狀態,以便下次再切換回這個任務時,可以再載入這個任務的狀態。任務從儲存到再載入的過程就是一次上下文切換。
在時間片切換到別的任務和切換到當前任務的時候,作業系統需要儲存和恢復相應執行緒的進度資訊。這個進度資訊就是上下文,它一般包括通用暫存器的內容和程式計數器的內容。
使用vmstat可以測量上下文切換的次數。示例:
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 127876 398928 2297092 0 0 0 4 2 2 0 0 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 595 1171 0 1 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 590 1180 1 0 100 0 0
0 0 0 127868 398928 2297092 0 0 0 0 567 1135 0 1 99 0 0
CS(Content Switch)表示上下文切換的次數,例子中的上下文每1秒切換1000多次。
1.2.1 上下分切換的分類
上下文切換可以分為自發性上下文切換和非自發性上下文切換(通常說的上下文切換指的是第一種):
型別 含義 原因
自發性上下文切換 由於自身因素導致的切出 Thread.sleep(long mills);
Object.wait();
Thread.yiels();
Thread.join();
LockSupport.park();
執行緒發起了IO操作;
等待其他執行緒持有的鎖 。
非自發性上下文切換 由於執行緒排程器的原因被迫切出 當前執行緒的時間片用完;
有一個比當前執行緒優先順序更高的執行緒需要執行;
Java虛擬機器的垃圾回收動作。
1.2.2 減少上下文切換的方式
1、無鎖併發程式設計
類似ConcurrentHashMap鎖分段的思想,不同的執行緒處理不同段的資料,這樣在多執行緒競爭的條件下,可以減少上下文切換的時間。
2、CAS演算法
利用Atomic下使用CAS演算法來更新資料,使用了樂觀鎖,可以有效的減少一部分不必要的鎖競爭帶來的上下文切換。
3、使用最少執行緒
避免建立不需要的執行緒,比如任務很少,但是建立了很多的執行緒,這樣會造成大量的執行緒都處於等待狀態。
4、協程
在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換。
1.2.3 上下文切換的最佳化示例
1、用jstack命令dump執行緒資訊
此處檢視pid為3117的程序:
sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
2、統計所有執行緒分別處於什麼狀態
發現300多個執行緒處於WAITING(onobject-monitor)狀態:
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
3、開啟dump檔案檢視處於WAITING(onobjectmonitor)的執行緒在做什麼
發現這些執行緒基本全是JBOSS的工作執行緒,在await。說明JBOSS執行緒池裡執行緒接收到的任務太少,大量執行緒都閒著:
"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Object.wait() [0x0000000052423000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at java.lang.Object.wait(Object.java:485)
at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464) - locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
at java.lang.Thread.run(Thread.java:662)
4、做出最佳化
減少JBOSS的工作執行緒數,找到JBOSS的執行緒池配置資訊,將maxThreads降到100:
<maxThreads="250" maxHttpHeaderSize="8192"
emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75"
maxPostSize="512000" protocol="HTTP/1.1"
enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384"
connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI= "true">
5、驗證
重啟JBOSS,再dump執行緒資訊,然後統計WAITING(onobjectmonitor)的執行緒,發現減少了175個。WAITING的執行緒少了,系統上下文切換的次數就會少,因為每一次從WAITTING到RUNNABLE都會進行一次上下文的切換。
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
44 RUNNABLE
22 TIMED_WAITING(onobjectmonitor)
9 TIMED_WAITING(parking)
36 TIMED_WAITING(sleeping)
130 WAITING(onobjectmonitor)
1 WAITING(parking)
1.3 併發程式設計的優缺點
1.3.1 併發程式設計的優點*
1、充分利用多核CPU的計算能力
可以真正發揮出多核CPU的優勢來,達到充分利用CPU的目的,採用多執行緒的方式去同時完成幾件事情而不互相干擾。
2、方便進行業務拆分,提升應用效能
多執行緒併發程式設計是開發高併發系統的基礎,利用好多執行緒機制可以大大提高系統整體的併發能力以及效能。
1.3.2 併發程式設計的缺點*
1、頻繁的上下文切換
時間片是CPU分配給各個執行緒的時間,因為時間非常短,所以CPU不斷透過切換執行緒,達到一種"不同應用似乎是同時執行的錯覺",時間片一般是幾十毫秒。每次切換時,需要儲存當前的狀態起來,以便能夠進行恢復先前狀態,而這個切換時非常損耗效能,過於頻繁反而無法發揮出多執行緒程式設計的優勢。
2、產生執行緒安全問題
即死鎖、執行緒飢餓等問題。
1.4 併發程式設計三要素
執行緒安全:多執行緒訪問同一程式碼,不會產生不確定的結果。
執行緒安全問題概括來說表現為3個方面:原子性、可見性和有序性。
1.4.1 原子性*
1、如何理解原子性
對於涉及共享變數的操作,若該操作從其執行執行緒以外的任意執行緒來看是不可分割的,那麼該操作就是原子操作,相應地我們稱該操作具有原子性。
原子性問題由執行緒切換導致。
原子性指的是一個或者多個操作,要麼全部執行並且在執行的過程中不被其他操作打斷,要麼就全部都不執行。
在理解原子操作時有兩點需要注意:
原子操作是針對共享變數的操作而言的;
原子操作是在多執行緒環境下才有意義。
原子操作的“不可分割”具有兩層含義:
1、訪問(讀、寫)某個共享變數的操作,從其執行執行緒以外的任何執行緒來看,該操作要麼已經執行結束要麼尚未發生,不會“看到”該操作執行部分的中間效果。
2、訪問同一組共享變數的原子操作是不能夠被交錯的。
在Java中,對基本資料型別資料(long/double除外,僅包括byte、boolean、short、char、float和int)的變數和引用型變數的寫操作都是原子的。
虛擬機器將沒有被volatile修飾的64位資料(long/double)的讀寫操作劃分為兩次32位的操作來進行。
如果要保證long/double的寫操作具有原子性,可以使用volatile變數修飾long/double變數。值得注意的是:volatile關鍵字僅能保證變數寫操作的原子性,並不能保證其他操作(如read-modify-write操作和check-then-act操作)的原子性。
Java中任何變數的讀操作都是原子操作。
2、原子性問題的例子
一個關於原子性的典型例子:counter++這並不是一個原子操作,包含了三個步驟:
讀取變數counter的值;
對counter加一;
將新值賦值給變數counter。
3、解決原子性問題方法
Atomic開頭的原子類、synchronized、LOCK等(即:鎖機制和無鎖CAS機制),都可以解決原子性問題。
1.4.2 可見性*
1、如何理解可見性
如果一個執行緒對某個共享變數進行更新後,後續訪問該變數的執行緒可以讀取到本次更新的結果,那麼就稱這個執行緒對該共享變數的更新對其它執行緒可見(一個執行緒對共享變數的修改,另一個執行緒能夠立刻看到)。
可見性問題由快取導致。
2、如何實現可見性
主要有三種實現可見性的方式:
volatile,透過在組合語言中新增lock指令,來實現記憶體可見性。
synchronized,當執行緒獲取鎖時會從主記憶體中獲取共享變數的最新值,釋放鎖的時候會將共享變數同步到主記憶體中。
final,被final關鍵字修飾的欄位在構造器中一旦初始化完成,並且沒有發生this逃逸(其它執行緒透過 this 引用訪問到初始化了一半的物件),那麼其它執行緒就能看見 final 欄位的值。
3、一些可見性場景
Java中預設的兩種可見性的存在場景:
父執行緒在啟動子執行緒之前對共享變數的更新對於子執行緒來說是可見的。
一個執行緒終止後該執行緒對共享變數的更新對於呼叫該執行緒的join方法的執行緒而言是可見的。
1.4.3 有序性*
有序性指的是:程式執行的順序按照程式碼的先後順序執行。有序性問題由編譯最佳化導致。
volatile和synchronized都可以保證有序性:
volatile關鍵字透過新增記憶體屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到記憶體屏障之前。
synchronized關鍵字同樣可以保證有序性,它保證每個時刻只有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼。
1.5 同步與非同步
同步
當一個同步呼叫發出去後,呼叫者要一直等待呼叫結果的返回後,才能進行後續的操作。
非同步
當一個非同步呼叫發出去後,呼叫者不用管被呼叫方法是否完成,都會繼續執行後面的程式碼。 非同步呼叫,要想獲得結果,一般有兩種方式:
主動輪詢非同步呼叫的結果;
被呼叫方透過callback來通知呼叫方呼叫結果(常用)。
比如在超市購物,如果一件物品沒了,你等倉庫人員跟你調貨,直到倉庫人員跟你把貨物送過來,你才能繼續去收銀臺付款,這就類似同步呼叫。而非同步呼叫就像網購,在網上付款下單後就不用管了,當貨物到達後你收到通知去取就好。
1.6 程序與執行緒
程式:含有指令和資料的檔案,被儲存在磁碟或其他的資料儲存裝置中,也就是說程式是靜態的程式碼。
程序:程式的一次執行過程,是系統執行程式的基本單位,因此程序是動態的。系統執行一個程式即是一個程序從建立,執行到消亡的過程。簡單來說,一個程序就是一個執行中的程式,它在計算機中一個指令接著一個指令地執行著,同時,每個程序還佔有某些系統資源如 CPU 時間,記憶體空間,檔案,輸入輸出裝置的使用權等等。
執行緒:與程序相似,但執行緒是一個比程序更小的執行單位。程序是程式向作業系統申請資源的基本單位,執行緒是程序中可獨立執行的最小單位。通常一個程序可以包含多個執行緒,至少包含一個執行緒,同一個程序中所有執行緒共享該程序的資源。系統在產生一個執行緒,或是在各個執行緒之間作切換工作時,負擔要比程序小得多,也正因為如此,執行緒也被稱為輕量級程序(使用多執行緒而不是多程序去進行併發程式的設計,是因為執行緒間的切換和排程的成本遠遠小於程序)。
1、根本區別
程序是作業系統資源分配的基本單位,而執行緒是處理器任務排程和執行的基本單位。
2、資源開銷
每個程序都有獨立的程式碼和資料空間(程式上下文),程式之間的切換會有較大的開銷;執行緒可以看做輕量級的程序,同一類執行緒共享程式碼和資料空間,每個執行緒都有自己獨立的執行棧和程式計數器,執行緒之間切換的開銷小。
3、包含關係
一個程序裡可以包含多個執行緒。
4、記憶體分配
同一程序的執行緒共享本程序的地址空間和資源,而執行緒之間的地址空間和資源是相互獨立的。
5、影響關係
一個程序崩潰後,在保護模式下不會對其他程序產生影響,但是一個執行緒崩潰整個程序都死掉。所以多程序要比多執行緒健壯。
6、執行過程
每個獨立的程序有程式執行的入口、順序執行序列和程式出口。但是執行緒不能獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制,兩者均可併發執行。
1.7 執行緒排程
一個CPU,在任意時刻只能執行一條機器指令,每個執行緒只有獲得CPU的使用權才能執行指令。多執行緒的併發執行,指從宏觀上看,各個執行緒輪流獲得CPU的使用權,分別執行各自的任務。
執行緒排程模型有兩種:
1、分時排程模型
分時排程模型是指讓所有的執行緒輪流獲得CPU的使用權,並且平均分配每個執行緒佔用的CPU的時間片。如果一個執行緒編寫有問題,執行到一半就一直堵塞,那麼可能導致整個系統崩潰。
2、搶佔式排程模型
搶佔式排程模型是指優先讓執行池中優先順序高的執行緒佔用CPU,如果執行池中的執行緒優先順序相同,那麼就隨機選擇一個執行緒,使其佔用CPU。處於執行狀態的執行緒會一直執行,直至它不得不放棄CPU。在這種機制下,一個執行緒的堵塞不會導致整個程序堵塞。
Java虛擬機器(JVM)採用搶佔式排程模型。 Java 中執行緒會按優先順序分配 CPU 時間片執行,且優先順序越高越優先執行,但優先順序高並不代表能獨自佔用執行時間片,可能是優先順序高得到越多的執行時間片,反之,優先順序低的分到的執行時間少但不會分配不到執行時間。
執行緒讓出 cpu 的情況:
當前執行執行緒主動放棄 CPU,JVM 暫時放棄 CPU 操作,例如呼叫 yield()方法。
當前執行執行緒因為某些原因進入阻塞狀態,例如阻塞在 I/O 上。
當前執行執行緒結束,即執行完 run()方法裡面的任務。
1.8 相關問題
1.8.1 編寫多執行緒程式的時候你會遵循哪些最佳實踐
1)給執行緒命名,這樣可以幫助除錯。
2)最小化同步的範圍,而不是將整個方法同步,只對關鍵部分做同步。
3)如果可以,更偏向於使用volatile而不是synchronized。
4)使用更高層次的併發工具,而不是使用wait()和notify()來實現執行緒間通訊,如BlockingQueue、CountDownLatch及Semeaphore。
5)優先使用併發集合,而不是對集合進行同步。併發集合提供更好的可擴充套件性。
6)使用執行緒池。
1.8.2 什麼是執行緒排程器和時間分片
執行緒排程器是一個作業系統服務,它負責為Runnable狀態的執行緒分配CPU時間。一旦建立一個執行緒並啟動它,它的執行便依賴於執行緒排程器的實現。
時間分片是指將可用的CPU時間分配給可用的Runnable執行緒的過程。分配CPU時間可以基於執行緒優先順序或者執行緒等待的時間。
執行緒排程並不受到Java虛擬機器控制,所以由應用程式來控制它是更好的選擇。
1.8.3 Linux環境下如何查詢哪個執行緒使用CPU最長
1、獲取專案的pid,jps或者ps -ef | grep java。
2、top -H -p pid,順序不能改變。
使用"top -H -p pid"+"jps pid"可以很容易地找到某條佔用CPU高的執行緒的執行緒堆疊,從而定位佔用CPU高的原因,一般是因為不當的程式碼操作導致了死迴圈。
最後提一點,"top -H -p pid"打出來的LWP是十進位制的,"jps pid"打出來的本地執行緒號是十六進位制的,轉換一下,就能定位到佔用CPU高的執行緒的當前執行緒堆疊了。
1.8.4 多程序和多執行緒的區別
多程序
程序是程式在計算機上的一次執行活動,即正在執行中的應用程式,通常稱為程序。當你執行一個程式,你就啟動了一個程序。每個程序都有自己獨立的地址空間(記憶體空間),每當使用者啟動一個程序時,作業系統就會為該程序分配一個獨立的記憶體空間,讓應用程式在這個獨立的記憶體空間中執行。
在同一個時間裡,同一個計算機系統中如果允許兩個或兩個以上的程序處於執行狀態,這便是多程序,也稱多工。現代的作業系統幾乎都是多工作業系統,能夠同時管理多個程序的執行。
多工帶來的好處是明顯的,比如你可以邊聽mp3邊上網,與此同時甚至可以將下載的文件列印出來,而這些任務之間絲毫不會相互干擾。
多執行緒
執行緒是一個輕量級的子程序,是最小的處理單元;是一個單獨的執行路徑。可以說:執行緒是程序的子集(部分),一個程序可能由多個執行緒組成。
執行緒是獨立的。如果在一個執行緒中發生異常,則不會影響其他執行緒。它使用共享記憶體區域。
多執行緒是一種執行模型,它允許多個執行緒存在於程序的上下文中,以便它們獨立執行但共享其程序資源。
區別:
維度 多程序 多執行緒 總結
資料共享、同步 資料是分開的,共享複雜,需要用IPC(程序間通訊);同步簡單 多執行緒共享程序資料,共享簡單;同步複雜 各有優勢
記憶體、CPU 佔用記憶體多,切換複雜,CPU利用率低 佔用記憶體少,切換簡單,CPU利用率高 執行緒佔優
建立銷燬、切換 建立銷燬、切換複雜,速度慢 建立銷燬、切換簡單,速度快 執行緒佔優
程式設計除錯 程式設計簡單,除錯簡單 程式設計複雜,除錯複雜 程序佔優
可靠性 程序間不會相互影響 一個執行緒掛掉將導致整個程序掛掉 程序佔優
分散式 適應於多核、多機分佈 ;如果一臺機器不夠,擴充套件到多臺機器比較簡單 適應於多核分佈 執行緒佔優
二、執行緒的基本使用
在Java中建立一個執行緒,可以理解建立一個Thread類(或其子類)的例項。執行緒的任務處理邏輯可以在Thread類的run例項方法中實現,執行一個執行緒實際上就是讓Java虛擬機器執行該執行緒的run方法。run方法相當於執行緒的任務處理邏輯的入口方法,它由虛擬機器在執行相應執行緒時直接呼叫,而不是由相應程式碼進行呼叫。
啟動一個執行緒的方法是呼叫start方法,其實質是請求Java虛擬機器執行相應的執行緒,而這個執行緒具體何時執行是由執行緒排程器決定的。因此,start方法呼叫結束並不意味著相應執行緒已經開始執行。
2.1 建立執行緒
建立執行緒有4種方式。
2.1.1 繼承Thread類*
繼承Thread類,作為執行緒物件存在。使用方式:
繼承Thread類;
重寫run方法;
建立Thread物件;
透過start()方法啟動執行緒。
示例:
/*繼承Thread類*/
public class WelcomeThread extends Thread{
@Override
public void run() {
System.out.printf("test");
}
}
public class ThreadTest1 {
public static void main(String[] args) {
// 建立執行緒
Thread welcomeThread = new WelcomeThread();
// 啟動執行緒
welcomeThread.start();
}
}
也可以使用匿名類的方式的來建立。示例:
new Thread(){
@Override
public void run() {
System.out.println("執行緒執行了...");
}
}.start();
//JDK1.8後,可以使用Lambda表示式來建立
new Thread(()->{
System.out.println("Lambda Thread Test!");
}).start();
2.1.2 實現Runnable介面*
實現runnable介面,作為執行緒任務存在。使用方式:
實現Runnable介面;
重寫run方法;
建立Thread物件,將實現Runnable介面的類作為Thread的構造引數;
透過start()進行啟動。
此種方式用到了代理模式,示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
Runnable只是來修飾執行緒所執行的任務,它不是一個執行緒物件。想要啟動Runnable物件,必須將它放到一個執行緒物件裡。
前兩種比較的話, 推薦使用第二種方式,原因:
Java是單繼承,將繼承關係留給最需要的類。
Runnable可以實現多個相同的程式程式碼的執行緒去共享同一個資源。當以Thread方式去實現資源共享時,實際上Thread內部,依然是以Runnable形式去實現的資源共享。
2.1.3 實現Callable介面*
前兩種方式比較常見,Callable的使用方式:
建立實現Callable介面的類;
以Callable介面的實現類為引數,建立FutureTask物件;
將FutureTask作為引數建立Thread物件;
呼叫執行緒物件的start()方法。
示例:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法執行中...");
return 1;
}
}
public class CallableTest {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
}
}
使用該方法建立執行緒時,核心方法是call(),該方法有返回值,其返回值型別就是Callable介面中泛型對應的型別。
2.1.4 建立執行緒池*
由於執行緒的建立、銷燬是一個比較消耗資源的過程,所以在實際使用時往往使用執行緒池。
在建立執行緒池時,可以使用現成的Executors工具類來建立,該工具類能建立的執行緒池有4種:newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool。此處以newSingleThreadExecutor為例,其步驟為:
使用Executors類中的newSingleThreadExecutor方法建立一個執行緒池;
呼叫執行緒池中的execute()方法執行由實現Runnable介面建立的執行緒;或者呼叫submit()方法執行由實現Callable介面建立的執行緒;
呼叫執行緒池中的shutdown()方法關閉執行緒池。
示例:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法執行中...");
}
}
public class SingleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyRunnable runnableTest = new MyRunnable();
for (int i = 0; i < 5; i++) {
executorService.execute(runnableTest);
}
System.out.println("執行緒任務開始執行");
executorService.shutdown();
}
}
2.1.5 4種建立方式對比
1、繼承Thread類
優點 :程式碼簡單 。
缺點 :該類無法繼承別的類。
如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用 this 即可獲得當前執行緒。
2、實現Runnable介面
實現Runnable介面,比繼承Thread類所具有的優勢:
1)適合多個相同的程式程式碼的執行緒去處理同一個資源;
2)可以避免Java中的單繼承的限制;
3)增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立 ;
4)執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類;
5)Runnable實現執行緒可以對執行緒進行復用,因為Runnable是輕量級的物件,重複new不會耗費太大資源,而Thread則不然,它是重量級物件,而且執行緒執行完就完了,無法再次利用。
3、實現Callable介面
優點:可以獲得非同步任務的返回值。
如果要訪問當前執行緒,則必須使用Thread.currentThread()方法。
執行緒類實現Runnable介面或Callable介面,還可以繼承其他類。在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況。
4、執行緒池
優點:實現自動化裝配,易於管理,迴圈利用資源。
2.2 啟動執行緒
2.2.1 執行緒每次只能使用一次*
當執行緒的run方法執行結束,相應的執行緒的執行也就結束了。
執行緒每次只能使用一次,即只能呼叫一次start方法。線上程未結束前,多次呼叫start方法會丟擲IllegalThreadStateException,Thread類中的start方法中可以看出該邏輯:
public synchronized void start() {
checkNotStarted();
hasBeenStarted = true;
nativeCreate(this, stackSize, daemon);
}
private void checkNotStarted() {
if (hasBeenStarted) {
throw new IllegalThreadStateException("Thread already started");
}
}
可以看出:start()方法使用synchronized關鍵字修飾,說明start()方法是同步的,它會在啟動執行緒前檢查執行緒的狀態,如果不是初始化狀態,則直接丟擲異常。所以,一個執行緒只能啟動一次,多次啟動是會丟擲異常的。
2.2.2 執行緒的run和start有什麼區別*
start()方法用於啟動執行緒,run()方法用於實現具體的業務邏輯。
run()可以重複呼叫,而start()只能呼叫一次。
透過呼叫Thread類的start方法來啟動一個執行緒,無需等待run()方法體程式碼執行完畢,可以直接繼續執行其他的程式碼, 此時執行緒是處於就緒狀態,並沒有執行。 只有呼叫了start()方法,才會表現出多執行緒的特性,不同執行緒的run()方法裡面的程式碼交替執行。
run方法執行結束, 此執行緒終止。然後CPU再排程其它執行緒。
當直接呼叫run()方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動,start()方法才會啟動新執行緒。
2.2.3 為什麼不能直接呼叫run()方法*
JVM執行start方法,會另起一條執行緒執行thread的run方法,這才起到多執行緒的效果。如果直接呼叫Thread的run()方法,其方法還是執行在主執行緒中,沒有起到多執行緒效果。
新建一個執行緒,執行緒進入了新建狀態。呼叫 start()方法,會啟動一個執行緒並使執行緒進入了就緒狀態,當分配到時間片後就可以開始執行了。 start()會執行執行緒的相應準備工作,然後自動執行run()方法的內容,這是真正的多執行緒工作。
如果直接執行run()方法,會把run方法當成一個main執行緒下的普通方法去執行,並不會在某個執行緒中執行它,所以這並不是多執行緒工作。示例:
public class JavaTest {
public static void main(String[] args) {
System.out.println("main方法中的執行緒名:"
+Thread.currentThread().getName()); //main方法中的執行緒名:main
Thread welcomeThread = new WelcomeThread();
System.out.println("以start方法啟動執行緒");
welcomeThread.start(); //Thread子類中的執行緒名:Thread-0
System.out.println("以run方法啟動執行緒");
welcomeThread.run(); //Thread子類中的執行緒名:main
}
}
class WelcomeThread extends Thread{
@Override
public void run() {
System.out.println("Thread子類中的執行緒名:"
+Thread.currentThread().getName());
}
}
總結: 呼叫start方法方可啟動執行緒並使執行緒進入就緒狀態,而run方法只是thread的一個普通方法呼叫,還是在主執行緒裡執行。
2.2.4 執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
執行緒類的構造方法、靜態塊是被new這個執行緒類所在的執行緒所呼叫的,而run方法里面的程式碼才是被執行緒自身所呼叫的。
舉個例子,假設Thread2中new了Thread1,main函式中new了Thread2,那麼:
Thread2的構造方法、靜態塊是main執行緒呼叫的,Thread2的run()方法是Thread2自己呼叫的
Thread1的構造方法、靜態塊是Thread2呼叫的,Thread1的run()方法是Thread1自己呼叫的。
2.3 執行緒屬性
Thread類的私有屬性有許多,瞭解幾個常用的即可:執行緒的編號(ID)、名稱(Name)、執行緒類別(Daemon)和優先順序(Priority)。
這幾個屬性中,ID僅可讀,其他都是可讀寫。具體:
屬性 屬性型別 用途 注意事項
編號(ID) long 用於標識不同的執行緒,不同的執行緒擁有不同的編號 某個編號的執行緒執行結束後,該編號可能被後續建立的執行緒使用,因此該屬性的值不適合用作某種唯一標識
名稱(Name) String 用於區分不同的執行緒,預設值的格式為“Thread-執行緒編號” 儘量為不同的執行緒設定不同的值
執行緒類別(Daemon) boolean 值為true表示相應的執行緒為守護執行緒,否則表示相應的執行緒為使用者執行緒。該屬性的預設值與相應執行緒的父執行緒的該屬性的值相同 該屬性必須在相應執行緒啟動之前設定,即呼叫setDaemon方法必須在呼叫start方法之前,否則會出現IllegalThreadStateException
優先順序(Priority) int 優先順序高的執行緒一般會被優先執行。優先順序從1到10,預設值一般為5(普通優先順序),數字越大,優先順序越高。
對於具體的一個執行緒而言,其優先順序的預設值與其父執行緒的優先順序值相等。 一般使用預設的優先順序即可,不恰當地設定該屬性值可能會導致嚴重的問題(執行緒飢餓)
獲取4個屬性值示例:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
//10,Thread-0,5,false
System.out.println(Thread.currentThread().getId()+","
+Thread.currentThread().getName()+","
+Thread.currentThread().getPriority()+","
+Thread.currentThread().isDaemon());
}
}).start();
}
2.3.1 執行緒優先順序*
Java執行緒的優先順序屬性本質上只是一個給執行緒排程器的提示資訊,以便於執行緒排程器決定優先排程哪些執行緒執行。每個執行緒的優先順序都在1到10之間,1的優先順序為最低,10的優先順序為最高,在預設情況下優先順序都是Thread.NORM_PRIORITY(常數 5)。
雖然開發者可以定義執行緒的優先順序,但是這並不能保證高優先順序的執行緒會在低優先順序的執行緒前執行。
執行緒優先順序特性:
1、繼承性
比如A執行緒啟動B執行緒,則B執行緒的優先順序與A是一樣的。
2、規則性
高優先順序的執行緒總是大部分先執行完,但不代表高優先順序執行緒全部先執行完。
3、隨機性
優先順序較高的執行緒不一定每一次都先執行完。
在不同的JVM以及OS上,執行緒規劃會存在差異,有些OS會忽略對執行緒優先順序的設定。
設定和獲取執行緒優先順序的方法:
//為執行緒設定優先順序
public final void setPriority(int newPriority)
//獲取執行緒的優先順序
public final int getPriority()
示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
int nowPriority = Thread.currentThread().getPriority();
System.out.println("1.優先順序:"+nowPriority); //1.優先順序:5
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
nowPriority = Thread.currentThread().getPriority();
System.out.println("2.優先順序:"+nowPriority); //2.優先順序:10
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
2.3.2 守護執行緒和使用者執行緒*
Java中的執行緒分為兩種:守護執行緒和使用者執行緒。任何執行緒都可以設定為守護執行緒和使用者執行緒,透過方法setDaemon(true)可以把該執行緒設定為守護執行緒,反之則為使用者執行緒。
使用者執行緒:執行在前臺,執行具體的任務,如程式的主執行緒、連線網路的子執行緒等都是使用者執行緒。
守護執行緒:執行在後臺,為其他前臺執行緒服務,比如垃圾回收執行緒,JIT(編譯器)執行緒就可以理解為守護執行緒。一旦所有使用者執行緒都結束執行,守護執行緒會隨 JVM 一起結束工作。
守護執行緒應該永遠不去訪問固有資源 ,如檔案、資料庫,因為它會在任何時候甚至在一個操作的中間發生中斷。
注意事項:
setDaemon(true)必須在start()方法前執行,否則會丟擲IllegalThreadStateException。
在守護執行緒中產生的新執行緒也是守護執行緒。
不是所有的任務都可以分配給守護執行緒來執行,比如讀寫操作或者計算邏輯。
守護執行緒中不能依靠finally塊的內容來確保執行關閉或清理資源的邏輯。因為我們上面也說過了一旦所有使用者執行緒都結束執行,守護執行緒會隨JVM一起結束工作,所以守護執行緒中的finally語句塊可能無法被執行。
設定和獲取執行緒是否是守護執行緒的方法:
//設定執行緒是否為守護執行緒,true則把該執行緒設定為守護執行緒,反之則為使用者執行緒
public final void setDaemon(boolean on)
//判斷執行緒是否是守護執行緒
public final boolean isDaemon()
當程式中所有的使用者執行緒執行完畢之後,不管守護執行緒是否結束,系統都會自動退出。
2.3.3 執行緒名稱
相比於上面的兩個屬性,實際運用中,往往執行緒名稱會被修改,目的是為了除錯。獲取和設定執行緒名稱的方法:
//獲取執行緒名稱
public final String getName()
//設定執行緒名稱
public final synchronized void setName(String name)
示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
String nowName = Thread.currentThread().getName();
System.out.println("1.執行緒名稱:"+nowName); //1.執行緒名稱:Thread-0
Thread.currentThread().setName("測試執行緒");
nowName = Thread.currentThread().getName();
System.out.println("2.執行緒名稱:"+nowName); //2.執行緒名稱:測試執行緒
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
2.4 執行緒的生命週期*
當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。線上程的生命週期中,它要經過新建(New)、就緒(Runnable)、執行(Running)、阻塞(Blocked)和死亡(Dead)5 種狀態。尤其是當執行緒啟動以後,它不可能一直"霸佔"著CPU獨自執行,所以CPU需要在多條執行緒之間切換,於是執行緒狀態也會多次在執行、阻塞之間切換。
2.4.1 從程式碼角度理解
在Thread類中,執行緒狀態是一個列舉型別:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED
}
執行緒的狀態可以透過public State getState()來獲取,該方法的返回值是一個列舉型別,執行緒狀態定義如下:
1、NEW
一個已建立而未啟動(即沒呼叫start方法)的執行緒處於該狀態。
2、RUNNABLE
該狀態可以被看成一個複合狀態。它包括兩個子狀態:READY和RUNNING。前者表示處於該狀態的執行緒可以被執行緒排程器進行排程而使之處於RUNNING狀態,後者表示執行緒正在執行狀態。
執行Thread.yield()的執行緒,其狀態可能由RUNNING轉換為READY。
3、BLOCKED
處於BLOCKED狀態的執行緒並不會佔處理器資源,當阻塞式IO操作完成後,或執行緒獲得了其申請的資源,狀態又會轉換為RUNNABLE。
4、WAITING
一個執行緒執行了某些特定方法之後就會處於這種等待其他執行緒執行另外一些特定操作的狀態。
能夠使執行緒變成WAITING狀態的方法包括:Object.wait()、Thread.join(),能夠使執行緒從WAITING狀態變成RUNNABLE狀態的方法有:Object.notify()、Object.notifyAll()。
5、TIMED_WAITING
該狀態和WAITING類似,差別在於處於該狀態的執行緒是處於帶有時間限制的等待狀態。
當其他執行緒沒有在特定時間內執行該執行緒所期待的特定操作時,該執行緒的狀態自動轉換為RUNNABLE。
6、TERMINATED
已經執行結束的執行緒處於該狀態。
Thread.run()正常返回或由於丟擲異常而提前終止都會導致相應執行緒處於該狀態。
6種狀態的轉換:
2.4.2 從使用角度理解*
在實際開發中,往往將執行緒的狀態理解為5種:新建、可執行、執行、阻塞、死亡。
1、新建(new)
新建立了一個執行緒物件。用new方法建立一個執行緒後,執行緒物件就處於新建狀態。此時僅由JVM為其分配記憶體,並初始化其成員變數的值。
2、可執行(runnable)
執行緒物件建立後,當呼叫執行緒物件的start()方法,該執行緒處於就緒狀態,但還沒分配到CPU,處於執行緒就緒佇列,等待系統為其分配CPU。Java 虛擬機器會為其建立方法呼叫棧和程式計數器,等待排程執行。
3、執行(running)
可執行狀態(runnable)的執行緒獲得了CPU時間片,執行程式程式碼。
就緒狀態是進入到執行狀態的唯一入口,也就是說,執行緒要想進入執行狀態執行,首先必須處於就緒狀態中;
如果在給定的時間片內沒有執行結束,就會被系統給換下來回到等待執行狀態。
4、阻塞(block)
處於執行狀態中的執行緒由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒(runnable)狀態,才有機會再次被CPU呼叫以進入到執行狀態。
阻塞的情況分三種:
等待阻塞(位於物件等待池中的阻塞)
執行狀態中的執行緒執行wait()方法,JVM會把該執行緒放入等待佇列中,使本執行緒進入到等待阻塞狀態;
同步阻塞(位於物件鎖池中的阻塞)
執行緒在獲取synchronized同步鎖失敗(因為鎖被其它執行緒所佔用),則JVM會把該執行緒放入鎖池中,執行緒會進入同步阻塞狀態;
其他阻塞
透過呼叫執行緒的sleep()或 join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
5、死亡(dead)
執行緒run()、main()方法執行結束,或者因異常退出了run()方法,則該執行緒結束生命週期。
2.5 Thread類的常用方法
以下是Thread類中較常用的幾個方法,並不包含執行緒間協作的方法(如await、notify等),這些方法的使用隨後介紹。其中的yield方法並不常用,但常常拿來和sleep、await等方法進行比較,所以也介紹下。
方法 功能 備註
static Thread currentThread() 返回當前執行緒,即當前程式碼的執行執行緒
void run() 用於實現執行緒的任務處理邏輯 該方法由Java虛擬機器直接呼叫
void start() 啟動執行緒 呼叫該方法並不代表相應的執行緒已經被啟動,執行緒是否啟動是由虛擬機器去決定的
void join() 等待相應執行緒執行結束 若執行緒A呼叫執行緒B的join方法,那麼執行緒A的執行會被暫停,直到執行緒B執行結束
static void yield() 使當前執行緒主動放棄其對處理器的佔用,這可能導致當前執行緒被暫停 這個方法是不可靠的,該方法被呼叫時當前執行緒仍可能繼續執行
void interrupt() 中斷執行緒
static void sleep(long millis) 使當前執行緒休眠(暫停執行)指定的時間
2.5.1 interrupt*
中斷一個執行緒,其本意是給這個執行緒一個通知訊號,會影響這個執行緒內部的一箇中斷標識位。這個執行緒本身並不會因此而改變狀態(如阻塞,終止等)。
呼叫interrupt()方法並不會中斷一個正在執行的執行緒。也就是說處於Running狀態的執行緒並不會因為被中斷而被終止,僅僅改變了內部維護的中斷標識位而已。
若呼叫sleep()而使執行緒處於TIMED-WATING狀態,這時呼叫interrupt()方法,會丟擲InterruptedException,從而使執行緒提前結束TIMED-WATING狀態。
許多宣告丟擲InterruptedException的方法(如 Thread.sleep(long mills 方法)),丟擲異常前,都會清除中斷標識位。
中斷狀態是執行緒固有的一個標識位,可以透過此標識位安全的終止執行緒。比如,你想終止一個執行緒thread的時候,可以呼叫thread.interrupt()方法,線上程的run方法內部可以根據 thread.isInterrupted()的值來優雅的終止執行緒。
2.5.1 interrupted*
測試當前執行緒是否已經中斷。中斷可以理解為執行緒的一個標誌位,它表示了一個執行中的執行緒是否被其他執行緒進行了中斷操作,常常被用於執行緒間的協作。
其他執行緒可以呼叫指定執行緒的interrupt()方法對其進行中斷操作,同時指定執行緒可以呼叫isInterrupted()來感知其他執行緒對其自身的中斷操作,從而做出響應。
另外,也可以呼叫Thread的靜態方法interrupted()對當前執行緒進行中斷操作,該方法會清除中斷標誌位。需要注意的是,當丟擲InterruptedException時候,會清除中斷標誌位,此時再呼叫isInterrupted,會返回false。
和中斷相關的方法有3個:
方法名 詳細解釋 備註
public void interrupt() 中斷一個執行緒 如果該執行緒被呼叫了Object wait/Object wait(long),或者被呼叫sleep(long),join()/join(long)方法時會丟擲interruptedException並且中斷標誌位將會被清除
public boolean isinterrupted() 測試該執行緒物件是否被中斷 中斷標誌位不會被清除
public static boolean interrupted() 檢視當前中斷訊號是true還是false並且清除中斷訊號 中斷標誌位會被清除
關於interrupt和isinterrupted的使用,示例:
public class JavaTest {
public static void main(String[] args) throws InterruptedException {
//sleepThread睡眠1000ms
final Thread sleepThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
};
//busyThread一直執行死迴圈
Thread busyThread = new Thread() {
@Override
public void run() {
while (true) ;
}
};
sleepThread.start();
busyThread.start();
sleepThread.interrupt();
busyThread.interrupt();
while (sleepThread.isInterrupted()) ;
System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
}
}
測試結果:
在上面的程式碼中,開啟了兩個執行緒分別為sleepThread和BusyThread, sleepThread睡眠1s,BusyThread執行死迴圈。然後分別對著兩個執行緒進行中斷操作,可以看出sleepThread丟擲InterruptedException後清除標誌位,而busyThread就不會清除標誌位。
另外,可以透過中斷的方式實現執行緒間的簡單互動,因為可以透過isInterrupted()方法監控某個執行緒的中斷標誌位是否清零,針對不同的中斷標誌位進行不同的處理。
2.5.2 join*
join方法也是一種執行緒間協作的方式,很多時候,一個執行緒的輸入可能非常依賴於另一個執行緒的輸出。如果在一個執行緒threadA中執行了threadB.join(),其含義是:當前執行緒threadA會等待threadB執行緒終止後,threadA才會繼續執行。
方法名 詳細註釋 備註
public final void join() throws InterruptedException 等待這個執行緒死亡。 如果任何執行緒中斷當前執行緒,如果丟擲InterruptedException異常時,當前執行緒的中斷狀態將被清除
public final void join(long millis) throws InterruptedException 等待這個執行緒死亡的時間最多為millis毫秒。
如果引數為 0,意味著永遠等待。 如果millis為負數,丟擲IllegalArgumentException異常
public final void join(long millis, int nanos) throws InterruptedException 等待最多millis毫秒加上這nanos納秒。 如果millis為負數或者nanos不在0-999999範圍丟擲IllegalArgumentException異常
看個例子:
public class JoinDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 1; i <= 10; i++) {
Thread curThread = new JoinThread(previousThread);
curThread.start();
previousThread = curThread;
}
}
static class JoinThread extends Thread {
private Thread thread;
public JoinThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
System.out.println(thread.getName() + " terminated.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
測試結果:
在上面的例子中一個建立了10個執行緒,每個執行緒都會等待前一個執行緒結束才會繼續執行。可以通俗的理解成接力,前一個執行緒將接力棒傳給下一個執行緒,然後又傳給下一個執行緒…
2.5.3 sleep*
public static native void sleep(long millis)
sleep是Thread的靜態方法,它的作用是:讓當前執行緒按照指定的時間休眠,其休眠時間的精度取決於處理器的計時器和排程器。需要注意的是如果當前執行緒獲得了鎖,sleep方法並不會失去鎖。
一定是當前執行緒呼叫此方法,當前執行緒進入TIMED_WAITING狀態,讓出cpu資源,但不釋放物件鎖,指定時間到後又恢復執行。作用:給其它執行緒執行機會的最佳方式。
Thread.sleep方法經常拿來與Object.wait()方法進行比較,sleep和wait兩者主要的區別:
sleep()方法是Thread的靜態方法,而wait是Object例項方法;
wait()方法必須要在同步方法或者同步塊中呼叫,也就是必須已經獲得物件鎖。而sleep()方法沒有這個限制可以在任何地方使用。
wait()方法會釋放佔有的物件鎖,使得該執行緒進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU並不會釋放掉物件鎖;
sleep()方法在休眠時間達到後,如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知後,才會離開等待池,並且再次獲得CPU時間片才會繼續執行。
關於sleep方法的使用,示例:
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第一個執行緒的執行時間:"+new Date());
}
}).start();
System.out.println("sleep2秒");
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第二個執行緒的執行時間:"+new Date());
}
}).start();
}
結果示例:
可以看出,第2個執行緒的執行時間是晚於第1個執行緒2秒的。
2.5.4 yield
public static native void yield()
yield方法的作用:使當前執行緒從執行狀態(執行狀態)變為可執行態(就緒狀態)。
yield方法是一個靜態方法,一旦執行,它會是當前執行緒讓出CPU。但是,讓出了CPU並不是代表當前執行緒不再執行了。執行緒排程器可能忽略此此訊息,並且如果在下一次競爭中,又獲得了CPU時間片當前執行緒依然會繼續執行。另外,讓出的時間片只會分配給大於等於當前執行緒優先順序的執行緒。
線上程中,用priority來表示優先順序,priority的範圍從1~10。在構建執行緒的時候可以透過 setPriority(int) 方法進行設定,預設優先順序為5,優先順序高的執行緒相較於優先順序低的執行緒優先獲得處理器時間片。
需要注意的是,sleep()和yield()方法,同樣都是當前執行緒會交出處理器資源,而它們不同的是,sleep()交出來的時間片其他執行緒都可以去競爭,也就是說都有機會獲得當前執行緒讓出的時間片。而yield()方法只允許大於等於當前執行緒優先順序的執行緒,競爭CPU時間片。
2.6 執行緒相關的一些問題
2.6.1 interrupt、interrupted和isInterrupted方法的區別*
interrupt:用於中斷執行緒。呼叫該方法的執行緒的狀態為將被置為”中斷”狀態。
呼叫目標執行緒的interrupt()方法,給目標執行緒發一箇中斷訊號,執行緒被打上中斷標記。
執行緒中斷僅僅是設定執行緒的中斷狀態標識,不會停止執行緒。需要使用者自己去監視執行緒的狀態為並做處理。支援執行緒中斷的方法(也就是執行緒中斷後會丟擲interruptedException 的方法)就是在監視執行緒的中斷狀態,一旦執行緒的中斷狀態標識被置為“中斷狀態”,就會丟擲中斷異常。
interrupted:是靜態方法,檢視當前中斷訊號是true還是false並且清除中斷訊號。如果一個執行緒被中斷了,第一次呼叫 interrupted 則返回 true,第二次和後面的就返回 false 了。判斷目標執行緒是否被中斷,不會清除中斷標記。
isInterrupted:檢視當前中斷訊號是true還是false。判斷目標執行緒是否被中斷,不會清除中斷標記。
2.6.2 sleep方法和yield方法有什麼區別*
1、sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
2、執行緒執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉入就緒(ready)狀態;
3、sleep()方法宣告丟擲 InterruptedException(其它執行緒可以使用 interrupt 方法打斷正在睡眠的執行緒,這時 sleep 方法會丟擲 InterruptedException),而 yield()方法沒有宣告任何異常;
4、sleep()方法比 yield()方法具有更好的可移植性,通常不建議使用yield()方法來控制併發執行緒的執行;
5、建議用TimeUnit的sleep代替Thread的sleep來獲得更好的可讀性。
6、sleep()方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的。
7、sleep()方法導致了程式暫停執行指定的時間,讓出cpu該其他執行緒,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復執行狀態。在呼叫sleep()方法的過程中,執行緒不會釋放物件鎖。
8、當呼叫wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件呼叫notify()方法後本執行緒才進入物件鎖定池準備,獲取物件鎖進入執行狀態。
2.6.3 執行緒怎麼處理異常
如果執行緒執行中產生了異常,首先會生成一個異常物件。我們平時throw丟擲異常,就是把異常交給JVM處理。JVM首先會去找有沒有能夠處理該異常的處理者(首先找到當前丟擲異常的呼叫者,如果當前呼叫者無法處理,則會沿著方法呼叫棧一路找下去),能夠處理的呼叫者實際就是看方法的catch關鍵字,JVM會把該異常物件封裝到catch入參,允許開發者手動處理異常。
若找不到能夠處理的處理者(實際就是沒有手動catch異常,比如未受檢異常),就會交該執行緒處理;JVM會呼叫Thread類的dispatchUncaughtException()方法,該方法呼叫了getUncaughtExceptionHandler(),uncaughtExceptoin(this,e)來處理了異常,如果當前執行緒設定了自己的UncaughtExceptionHandler,則使用該handler,呼叫自己的uncaughtException方法。如果沒有,則使用當前執行緒所在的執行緒組的Handler的uncaughtExceptoin()方法,如果執行緒中也沒有設定,則直接把異常定向到System.err中,列印異常資訊(控制檯紅色字型輸出的異常就是被定向到System.err的異常)。
2.6.4 Thread.sleep(0)的作用是什麼*
由於Java採用搶佔式的執行緒排程演算法,因此可能會出現某條執行緒常常獲取到CPU控制權的情況,為了讓某些優先順序比較低的執行緒也能獲取到CPU控制權,可以使用Thread.sleep(0)手動觸發一次作業系統分配時間片的操作,這也是平衡CPU控制權的一種操作。
Thread.sleep(0) 和 Thread.yield()
Thread.sleep(0) 和 Thread.yield() 主要取決於JVM的實現。這兩種方式都可以讓出cpu時間片,以允許其他執行緒獲取執行的機會。
不同的地方:
1、sleep()方法給其他執行緒執行機會的時候,不考慮執行緒的優先順序,因此當高優先順序執行緒sleep()後,低優先順序任務有機會執行;但是yield()只會給同優先順序或更高優先順序執行緒執行的機會,甚至可能是自己繼續執行。
2、執行緒呼叫sleep()後,轉入阻塞狀態,而呼叫yield()後轉入了就緒狀態。
3、sleep方法宣告丟擲InterruptedException,而yield沒有宣告任何異常。
2.6.5 一個執行緒如果出現了運行時異常會怎麼樣
如果這個異常沒有被捕獲的話,這個執行緒就停止執行了。
另外重要的一點是:如果這個執行緒持有某個物件的監視器器,那麼這個物件監視器器會被立即釋放。
2.6.6 終止執行緒執行的幾種情況
執行緒體中呼叫了yield方法讓出了對CPU的佔用權利;
執行緒體中呼叫了sleep方法使執行緒進入睡眠狀態;
執行緒由於IO操作受到阻塞;
另外一個更高優先順序執行緒出現,導致當前執行緒未分配到時間片;
在支援時間片的系統中,該執行緒的時間片用完。
使用stop方法強行終止,但是不推薦這個方法,因為stop是過期作廢的方法。
使用interrupt方法中斷執行緒。
有兩種情況。
1、執行緒處於阻塞狀態:當呼叫執行緒的 interrupt()方法時,會丟擲InterruptException異常。阻塞中的那個方法丟擲這個異常,透過程式碼捕獲該異常,然後 break 跳出迴圈狀態,從而讓我們有機會結束這個執行緒的執行。通常很多人認為只要呼叫 interrupt 方法執行緒就會結束,實際上是錯的, 一定要先捕獲 InterruptedException 異常之後透過 break 來跳出迴圈,才能正常結束 run 方法。
2、執行緒未處於阻塞狀態:使用 isInterrupted()判斷執行緒的中斷標誌來退出迴圈。當使用interrupt()方法時,中斷標誌就會置 true。示例:
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞過程中透過判斷中斷標誌來退出
try{
Thread.sleep(5*1000);//阻塞過程捕獲中斷異常來退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕獲到異常之後,執行 break 跳出迴圈
}
}
}
}
2.6.7 如何優雅地設定睡眠時間
JDK1.5之後,引入了一個列舉TimeUnit,對sleep方法提供了很好的封裝。
比如要休眠2小時22分55秒899毫秒,兩種寫法:
Thread.sleep(8575899);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);
2.6.8 如何設定上下文類載入器
獲取執行緒上下文類載入器:
public ClassLoader getContextClassLoader()
設定執行緒類載入器(可以打破Java類載入器的父類委託機制):
public void setContextClassLoader(ClassLoader cl)
2.6.9 如何停止一個正在執行的執行緒
當run方法完成後執行緒自動終止
使用stop方法強行終止
不推薦這個方法,因為stop和suspend及resume一樣都是過期作廢的方法。
可以使用共享變數的方式
在這種方式中,之所以引入共享變數,是因為該變數可以被多個執行相同任務的。執行緒用來作為是否中斷的訊號,通知中斷執行緒的執行。
在一般情況下,在 run 方法執行完畢的時候,執行緒會正常結束。然而,有些執行緒是後臺執行緒,需要長時間執行,只有在系統滿足某些特殊條件後,才能退出這些執行緒。這時可以使用一個變數來控制迴圈,比如設定一個 Boolean 型別的標誌,並透過設定這個標誌為 true 或 false 來控制 while 迴圈是否退出。
public class ThreadDemo extends Thread {
public volatile boolean exit = false;
@Override
public void run() {
while (!exit) {
//業務邏輯程式碼
}
}
}
使用interrupt方法終止執行緒
如果一個執行緒由於等待某些事件的發生而被阻塞,又該怎樣停止該執行緒呢?建議是不要使用stop()方法,而是使用Thread提供的interrupt()方法,因為該方法雖然不會中斷一個正在執行的執行緒,但是它可以使一個被阻塞的執行緒丟擲一箇中斷異常,從而使執行緒提前結束阻塞狀態,退出堵塞程式碼。
當執行緒處於阻塞狀態時,呼叫執行緒的interrupt()例項方法,執行緒內部會觸發InterruptedException異常,並且會清除執行緒內部的中斷標誌(即將中斷標誌置為false)。
示例:
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true; // 在異常處理程式碼中修改共享變數的狀態
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true; // 設定共享變數為true
m1.interrupt(); // 阻塞時退出阻塞狀態
Thread.sleep(3000); // 主執行緒休眠3秒以便觀察執行緒m1的中斷情況
System.out.println("Stopping application...");
}
}
2.6.10 為什麼Thread類的sleep()和yield()方法是靜態的
Thread類的sleep()和yield()方法將在當前正在執行的執行緒上執行。所以在其他處於等待狀態的執行緒上呼叫這些方法是沒有意義的。這就是為什麼這些方法是靜態的。它們可以在當前正在執行的執行緒中工作,並避免程式設計師錯誤的認為可以在其他非執行執行緒呼叫這些方法。
2.6.11 怎麼檢測一個執行緒是否擁有鎖
在Thread類中有一個靜態方法叫holdsLock(Object o),返回true表示:當且僅當當前執行緒擁有某個具體物件的鎖。
2.6.12 執行緒的排程策略
執行緒排程器選擇優先順序最高的執行緒執行,但是,如果發生以下情況,就會終止執行緒的執行:
1、執行緒體中呼叫了yield方法讓出了對CPU的佔用權利;
2、執行緒體中呼叫了sleep方法使執行緒進入睡眠狀態;
3、執行緒由於IO操作受到阻塞;
4、另外一個更高優先順序執行緒出現;
5、在支援時間片的系統中,該執行緒的時間片用完。
2.6.13 執行緒的排程策略
1)使用top命令查詢java命令下cpu佔用最高的程序:
例如pid為9595的程序是佔用cpu使用率最大的。
2)使用top -H -p 9595檢視當前pid為9595程序下各執行緒佔用cpu情況:
可以看到,pid為10034的執行緒佔用cpu是最高的。
3)將執行緒的pid由10進位制轉成16進位制:
4)把程序的全部堆疊資訊匯入到臨時檔案中:
jstack 9595 > /tmp/a.txt
5)透過vi /tmp/a/txt檢視該檔案:
2.6.14 join可以保證執行緒執行順序的原理
Thread的join()方法:
public final void join() throws InterruptedException {
join(0);
}
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;
}
}
}
可以看到,有一個long型別引數的join()方法使用了synchroinzed修飾,說明這個方法同一時刻只能被一個例項或者方法呼叫。由於,傳遞的引數為0,所以,程式會進入如下程式碼邏輯。
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
首先,在程式碼中以while迴圈的方式來判斷當前執行緒是否已經啟動處於活躍狀態,如果已經啟動處於活躍狀態,則呼叫同類中的wait()方法,並傳遞引數0。繼續跟進wait()方法,如下所示。
public final native void wait(long timeout) throws InterruptedException;
wait()方法是一個本地方法,透過JNI的方式呼叫JDK底層的方法來使執行緒等待執行完成。
呼叫執行緒的wait()方法時,會使主執行緒處於等待狀態,等待子執行緒執行完成後再次向下執行。也就是說,在ThreadSort02類的main()方法中,呼叫子執行緒的join()方法,會阻塞main()方法的執行,當子執行緒執行完成後,main()方法會繼續向下執行,啟動第二個子執行緒,並執行子執行緒的業務邏輯,以此類推。
2.6.15 stop()方法和interrupt()方法的區別
1、stop()方法
stop()方法會真的殺死執行緒。如果執行緒持有ReentrantLock鎖,被stop()的執行緒並不會自動呼叫ReentrantLock的unlock()去釋放鎖,那其他執行緒就再也沒機會獲得ReentrantLock鎖, 這樣其他執行緒就再也不能執行ReentrantLock鎖鎖住的程式碼邏輯。
2、interrupt()方法
interrupt()方法僅僅是通知執行緒,執行緒有機會執行一些後續操作,同時也可以無視這個通知。被interrupt的執行緒,有兩種方式接收通知:一種是異常, 另一種是主動檢測。
1)透過異常接收通知
當執行緒A處於WAITING、 TIMED_WAITING狀態時, 如果其他執行緒呼叫執行緒A的interrupt()方法,則會使執行緒A返回到RUNNABLE狀態,同時執行緒A的程式碼會觸發InterruptedException異常。執行緒轉換到WAITING、TIMED_WAITING狀態的觸發條件,都是呼叫了類似wait()、join()、sleep()這樣的方法, 我們看這些方法的簽名時,發現都會throws InterruptedException這個異常。這個異常的觸發條件就是:其他執行緒呼叫了該執行緒的interrupt()方法。
當執行緒A處於RUNNABLE狀態時,並且阻塞在java.nio.channels.InterruptibleChannel上時, 如果其他執行緒呼叫執行緒A的interrupt()方法,執行緒A會觸發java.nio.channels.ClosedByInterruptException這個異常;當阻塞在java.nio.channels.Selector上時,如果其他執行緒呼叫執行緒A的interrupt()方法,執行緒A的java.nio.channels.Selector會立即返回。
2)主動檢測通知
如果執行緒處於RUNNABLE狀態,並且沒有阻塞在某個I/O操作上,例如中斷計算基因組序列的執行緒A,此時就得依賴執行緒A主動檢測中斷狀態了。如果其他執行緒呼叫執行緒A的interrupt()方法, 那麼執行緒A可以透過isInterrupted()方法, 來檢測自己是不是被中斷了。
2.6.16 有三個執行緒T1,T2,T3,如何保證順序執行*
在多執行緒中有多種方法讓執行緒按特定順序執行,你可以用執行緒類的join()方法在一個執行緒中啟動另一個執行緒,另外一個執行緒完成該執行緒繼續執行。為了確保三個執行緒的順序你應該先啟動最後一個(T3呼叫T2,T2呼叫T1),這樣T1就會先完成而T3最後完成。
public class JoinTest {
// 1.現在有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t1執行緒,等待t1執行緒執行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t2執行緒,等待t2執行緒執行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();//這裡三個執行緒的啟動順序可以任意,大家可以試下!
t2.start();
t1.start();
}
}
2.6.17 執行緒中斷是否能直接呼叫stop
Java提供的終止方法只有一個stop,但是不建議使用此方法。
stop方法是過時的,已經過時的方式不建議採用。
stop方法會導致程式碼邏輯不完整。stop方法是一種"惡意"的中斷,一旦執行stop方法,即終止當前正在執行的執行緒,不管執行緒邏輯是否完整,這是非常危險的。
三、執行緒的活性故障
執行緒活性故障是由於資源稀缺性或程式自身的問題和缺陷導致執行緒一直處於非RUNNABLE狀態,或者執行緒雖然處於RUNNABLE狀態,但是其要執行的任務卻一直無法進展的故障現象。
3.1 死鎖*
死鎖是指兩個或兩個以上的程序(執行緒)在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。
如圖所示,執行緒A持有資源2,執行緒B持有資源1,他們同時都想申請對方的資源,所以這兩個執行緒就會互相等待而進入死鎖狀態:
3.1.1 死鎖的產生條件*
當執行緒產生死鎖時,這些執行緒及相關的資源將滿足如下全部條件:
1、互斥
一個資源只能被一個執行緒(程序)佔用,直到被該執行緒(程序)釋放。
2、請求與保持(不主動釋放)條件
一個執行緒(程序)因請求被佔用資源(鎖)而發生阻塞時,對已獲得的資源保持不放。
3、不剝奪(不能被強佔)條件
執行緒(程序)已獲得的資源,在末使用完之前不能被其他執行緒強行剝奪,只有自己使用完畢後才釋放資源。
4、迴圈等待(互相等待)條件
當發生死鎖時,所等待的執行緒(程序)必定會形成一個環路(類似於死迴圈),造成永久阻塞。
用一句話該概括:
兩個或多個執行緒持有並且不釋放獨有的鎖,並且還需要競爭別的執行緒所持有的鎖,導致這些執行緒都一直阻塞下去。
這些條件是死鎖產生的必要條件而非充分條件,也就是說只要產生了死鎖,那麼上面的這些條件一定同時成立,但是上述條件即便同時成立也不一定產生死鎖。
可能產生死鎖的特徵就是在持有一個鎖的情況下去申請另外一個鎖,通常是鎖的巢狀,示例:
//內部鎖
public void deadLockMethod1(){
synchronized(lockA){
//...
synchronized(lockB){
//...
}
}
}
//顯式鎖
public void deadLockMethod2(){
lockA.lock();
try{
//...
lockB.lock();
try{
//...
}finally{
lockB.unlock();
}
}finally{
lockA.unlock();
}
}
示例:
public class DeadLockDemo {
private static Object resource1 = new Object();//資源 1
private static Object resource2 = new Object();//資源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "執行緒 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "執行緒 2").start();
}
}
結果:
Thread[執行緒 1,5,main]get resource1
Thread[執行緒 2,5,main]get resource2
Thread[執行緒 1,5,main]waiting get resource2
Thread[執行緒 2,5,main]waiting get resource1
執行緒A透過synchronized (resource1)獲得resource1的監視器鎖,然後讓執行緒A休眠1s,為的是讓執行緒B得到執行然後獲取到resource2的監視器鎖。執行緒A和執行緒B休眠結束了都開始企圖請求獲取對方的資源,然後這兩個執行緒就會陷入互相等待的狀態,這也就產生了死鎖。
3.1.2 死鎖的規避*
由上文可知,要產生死鎖需要同時滿足四個條件。所以,只要打破其中一個條件就可以避免死鎖的產生(第一個條件 “互斥” 是不能破壞的,因為加鎖就是為了保證互斥)。常用的規避方法有如下幾種:
1、粗鎖法
用一個粒度較粗的鎖替代原來的多個粒度較細的鎖,這樣涉及的執行緒都只需要申請一個鎖從而避免了死鎖。粗鎖法的缺點是它明顯降低了併發性並可能導致資源浪費。
2、鎖排序法
相關執行緒使用全域性統一的順序申請鎖。假設有多個執行緒需要申請鎖(資源),那麼只需要讓這些執行緒依照一個全域性(相對於使用這種資源的所有執行緒而言)統一的順序去申請這些資源,就可以消除“迴圈等待資源”這個條件,從而規避死鎖。一般,可以使用物件的hashcode作為資源的排序依據。
3、使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 申請鎖
ReentrantLock.tryLock(long timeout, TimeUnit unit) 允許為申請鎖這個操作加上一個超時時間。在超時事件內,如果相應的鎖申請成功,該方法返回true。如果在tryLock執行的那一刻相應的鎖正在被其他執行緒持有,那麼該方法會使當前執行緒暫停,直到這個鎖申請成功(此時該方法返回true)或者等待時間超過指定的超時時間(此時該問題返回false)。因此,使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 來申請鎖可以避免一個執行緒無限制地等待另外一個執行緒持有的資源,從而最終能夠消除死鎖產生的必要條件中的“佔用並等待資源”。示例:
boolean locked = false;
try {
locked = lock.tryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(locked) lock.unlock();
}
4、使用鎖的替代品
使用一些鎖的替代品(無狀態物件、執行緒特有物件以及volatile關鍵字等),在條件允許的情況下,使用這些替代品在保障執行緒安全的前提下不僅能夠避免鎖的開銷,還能夠直接避免死鎖。
3.2 執行緒飢餓和活鎖
執行緒飢餓是指一直無法獲得其所需的資源而導致任務一直無法進展的一種活性故障。
執行緒飢餓的一個典型例子是在爭用的情況下使用非公平模式的讀寫鎖。此種情況下,可能會導致某些執行緒一直無法獲取其所需的資源(鎖),即導致執行緒飢餓。
把鎖看作一種資源的話,其實死鎖也是一種執行緒飢餓。死鎖的結果是故障執行緒都無法獲得其所需的全部鎖中的一個鎖,從而使其任務一直無法進展,這相當於執行緒無法獲得其所需的全部資源(鎖)而使得其任務一直無法進展,即產生了執行緒飢餓。由於執行緒飢餓的產生條件是一個(或多個)執行緒始終無法獲得其所需的資源,顯然這個條件的滿足並不意味著死鎖的必要條件(而不是充分條件)的滿足,因此執行緒飢餓並不會導致死鎖。
Java中導致飢餓的原因:
高優先順序執行緒搶佔了所有的低優先順序執行緒的 CPU 時間。
執行緒被永久堵塞在一個等待進入同步塊的狀態,因為其他執行緒總是能在它之前持續地對該同步塊進行訪問。
執行緒在等待一個本身也處於永久等待完成的物件(比如呼叫這個物件的 wait 方法),因為其他執行緒總是被持續地獲得喚醒。
執行緒飢餓涉及的執行緒,其生命週期不一定就是WAITING或BLOCKED狀態,其狀態也可能是RUNNING(說明涉及的執行緒一直在申請寧其所需的資源),這時飢餓就會演變成活鎖。
活鎖指執行緒一直處於執行狀態,但是其任務卻一直無法進展的一種活性故障。
3.3 死鎖與活鎖的區別,死鎖與飢餓的區別
死鎖
是指兩個或兩個以上的程序(或執行緒)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。
活鎖
任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試,卻一直獲得不了鎖。
飢餓
一個或者多個執行緒因為種種原因無法獲得所需要的資源,導致一直無法執行的狀態。
1、活鎖與死鎖的區別
活鎖和死鎖的區別在於:處於活鎖的實體是在不斷的改變狀態,這就是所謂的“活”, 而處於死鎖的實體表現為等待;活鎖有可能自行解開,死鎖則不能。活鎖可以認為是一種特殊的飢餓。
2、死鎖活鎖與飢餓的區別
程序會處於飢餓狀態是因為持續地有其它優先順序更高的程序請求相同的資源。不像死鎖或者活鎖,飢餓能夠被解開。例如,當其它高優先順序的程序都終止時並且沒有更高優先順序的程序強佔資源。