線程狀態(tài)
新創(chuàng)建
剛new出來的Thread還沒有被運行
可運行
一旦調用start 方法苹粟,線程處于runnable狀態(tài)有滑。一個可運行的線桿可能正在運行也可能沒有運行, 這取決于操作系統(tǒng)給線程提供運行的時間六水。
被阻塞線程和等待線程
當線程處于被阻塞或等待狀態(tài)時俺孙, 它暫時不活動辣卒。它不運行任何代碼且消耗最少的資源。直到線程調度器重新激活它睛榄。細節(jié)取決于它是怎樣達到非活動狀態(tài)的
- 當一個線程試圖獲取一個內部的對象鎖(而不是javiutiUoncurrent 庫中的鎖)荣茫,而該鎖被其他線程持有, 則該線程進人阻塞狀態(tài)场靴。當所有其他線程釋放該鎖啡莉,并且線程調度器允許本線程持有它的時候,該線程將變成非阻塞狀態(tài)旨剥。
- 當線程等待另一個線程通知調度器一個條件時咧欣,它自己進入等待狀態(tài)。在調用Object.wait方法或Thread.join方法轨帜,或者是等待java,util.concurrent 庫中的Lock 或Condition 時魄咕,就會出現(xiàn)這種情況。實際上蚌父,被阻塞狀態(tài)與等待狀態(tài)是有很大不同的哮兰。
- 有幾個方法有一個超時參數。調用它們導致線程進人計時等待(timed waiting) 狀態(tài)苟弛。這一狀態(tài)將一直保持到超時期滿或者接收到適當的通知喝滞。帶有超時參數的方法有Thread.sleep 和Object.wait、Thread.join膏秫、Lock,tryLock以及Condition.await的計時版右遭。
被終止的線程
線程因如下兩個原因之一而被終止:
- 因為run方法正常退出而自然死亡。
- 因為一個沒有捕獲的異常終止了run方法而意外死亡缤削。
同一個線程被 start() 兩次(2019-7-13更新)
Java 的線程是不允許啟動兩次的窘哈,第二次調用必然會拋出 IllegalThreadStateException,這是
一種運行時異常亭敢,多次調用 start 被認為是編程錯誤宵距。在第二次調用 start() 方法的時候,線程可能處于終止或者其他(非 NEW)狀態(tài)吨拗,但是不論如何满哪,都是不可以再次啟動的。
創(chuàng)建一個新線程的三種方法
通過Runnable接口創(chuàng)建線程類
- 定義runnable接口的實現(xiàn)類劝篷,并重寫該接口的run()方法哨鸭,該run()方法的方法體同樣是該線程的線程執(zhí)行體。
- 創(chuàng)建 Runnable實現(xiàn)類的實例娇妓,并依此實例作為Thread的target來創(chuàng)建Thread對象像鸡,該Thread對象才是真正的線程對象。
- 調用線程對象的start()方法來啟動該線程。
public class RunnableThreadTest implements Runnable {
private int i;
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " " + i);
if (i == 20) {
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt, "新線程1").start();
new Thread(rtt, "新線程2").start();
}
}
}
}
繼承Thread類創(chuàng)建線程類
- 定義Thread類的子類只估,并重寫該類的run方法志群,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為執(zhí)行體蛔钙。
- 創(chuàng)建Thread子類的實例锌云,即創(chuàng)建了線程對象。
- 調用線程對象的start()方法來啟動該線程吁脱。
public class FirstThreadTest extends Thread {
int i = 0;
//重寫run方法桑涎,run方法的方法體就是現(xiàn)場執(zhí)行體
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " : " + i);
if (i == 20) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
Callable 接口
Callable 與 Runable 有兩點不同:
- 可以通過 call() 獲得返回值。
- call() 可以拋出異常
Thread 和 Runnable 的區(qū)別
如果一個類繼承 Thread兼贡, 則不適合資源共享攻冷。但是如果實現(xiàn)了 Runnable 接口的話,則很容易實現(xiàn)資源共享遍希。
總結:
實現(xiàn) Runnable 接口比繼承 Thread 類具有的優(yōu)勢:
- 適合多個和相同的程序代碼的線程去共享同一個資源等曼。
- 可以避免 java 中的單繼承的局限性。
- 增加程序的健壯性凿蒜,實現(xiàn)解耦操作涉兽,代碼可以被多個線程共享,代碼和線程獨立篙程。
- 線程池只能放入實現(xiàn) Runnable或 Callable 類線程,不能直接放入繼承Thread 的類别厘。
中斷線程
沒有任何語言方面的需求要求一個被中斷的線程應該終止虱饿。中斷一個線程不過是引起它的注意。被中斷的線程可以決定如何響應中斷触趴。某些線程是如此重要以至于應該處理完異常后氮发, 繼續(xù)執(zhí)行, 而不理會中斷冗懦。但是爽冕,更普遍的情況是,線程將簡單地將中斷作為一個終止的請求披蕉。
線程屬性
線程優(yōu)先級
每當線程調度器有機會選擇新線程時颈畸,它首先選擇具有較高優(yōu)先級的線程。但是没讲,線程優(yōu)先級是高度依賴于系統(tǒng)的眯娱。當虛擬機依賴于宿主機平臺的線程實現(xiàn)機制時,Java 線程的優(yōu)先級被映射到宿主機平臺的優(yōu)先級上爬凑,優(yōu)先級個數也許更多徙缴,也許更少。
static void yield( )
導致當前執(zhí)行線程處于讓步狀態(tài)嘁信。如果有其他的可運行線程具有至少與此線程同樣高的優(yōu)先級于样,那么這些線程接下來會被調度疏叨。注意,這是一個靜態(tài)方法穿剖。
守護線程(2019-7-13更新)
有的時候應用中需要一個長期駐留的服務程序蚤蔓,但是不希望其影響應用退出,就可以將其設置為守護線程携御,如果 JVM 發(fā)現(xiàn)只有守護線程存在時昌粤,將結束進程,具體可以參考下面代碼段啄刹。注意涮坐,必須在線程啟動之前設置。
Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();
同步
為了避免多線程引起的對共享數據的訛誤誓军,必須學習如何同步存取袱讹。
競爭條件詳解
當兩個線程試圖同時更新同一個賬戶的時候,這個問題就出現(xiàn)了昵时。假定兩個線程同時執(zhí)行指令accounts[to] += amount;
問題在于這不是原子操作捷雕。該指令可能被處理如下:
- 將
accounts[to]
加載到寄存器。 - 增加
amount
壹甥。 - 將結果寫回
accounts[to]
救巷。
現(xiàn)在,假定第1個線程執(zhí)行步驟1和2, 然后句柠,它被剝奪了運行權浦译。假定第2個線程被喚醒并修改了accounts 數組中的同一項。然后溯职,第1個線程被喚醒并完成其第3步精盅。
這樣,這一動作擦去了第二個線程所做的更新谜酒。于是叹俏,總金額不再正確。
因此加入同步鎖以避免在該線程沒有完成操作之前僻族,被其他線程的調用粘驰,從而保證了該變量的唯一性和準確性。
鎖同步
鎖Lock
有兩種機制防止代碼塊受并發(fā)訪問的干擾述么。Java語言提供一個 synchronized 關鍵字達到這一目的晴氨,并且Java SE 5.0引入了ReentrantLock 類。
public class Bank{
private Lock bankLock = new ReentrantLock0 碉输;// ReentrantLock implements the Lock interface
public void transfer(int from, intto, int amount){
bankLock.lock();
try
{
System.out.print(Thread.currentThread0);
accounts[from] -= amount;
System.out.printf(" %10.2f from %A to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
}
finally
{
banklock.unlockO;
}
}
}
重入Lock是一個更強大的工具籽前,他有一個重入功能————當一個線程得到一個對象后,再次請求該對象鎖時是可以再次得到該對象的鎖的。
具體概念就是:自己可以再次獲取自己的內部鎖枝哄。因為線程可以重復地獲得已經持有的鎖肄梨。鎖保持一個持有計數(holdcount) 來跟蹤對lock 方法的嵌套調用。線程在每一次調用lock 都要調用unlock 來釋放鎖挠锥。由于這一特性众羡,被一個鎖保護的代碼可以調用另一個使用相同的鎖的方法。
假定一個線程調用transfer, 在執(zhí)行結束前被剝奪了運行權蓖租。假定第二個線程也調用transfer, 由于第二個線程不能獲得鎖粱侣,將在調用lock 方法時被阻塞。它必須等待第一個線程完成transfer 方法的執(zhí)行之后才能再度被激活蓖宦。當第一個線程釋放鎖時齐婴,那么第二個線程才能開始運行
公平鎖CPU在調度線程的時候是在等待隊列里隨機挑選一個線程,由于這種隨機性所以是無法保證線程先到先得的(synchronized控制的鎖就是這種非公平鎖)稠茂。但這樣就會產生饑餓現(xiàn)象柠偶,即有些線程(優(yōu)先級較低的線程)可能永遠也無法獲取CPU的執(zhí)行權,優(yōu)先級高的線程會不斷的強制它的資源睬关。那么如何解決饑餓問題呢诱担,這就需要公平鎖了。公平鎖可以保證線程按照時間的先后順序執(zhí)行电爹,避免饑餓現(xiàn)象的產生蔫仙。但公平鎖的效率比較低,因為要實現(xiàn)順序執(zhí)行丐箩,需要維護一個有序隊列摇邦。
條件對象Condition
假定一個線程已經獲得鎖,將要執(zhí)行雏蛮,但是他所需要的條件還沒有滿足(例如在余額不足的情況下取錢),便會造成有鎖卻不執(zhí)行阱州,其他能夠提供滿足條件的線程(例如存錢)卻只能等待挑秉,陷入僵局。
一個鎖對象可以有一個或多個相關的條件對象苔货。你可以用newCondition 方法獲得一個條件對象犀概。習慣上給每一個條件對象命名為可以反映它所表達的條件的名字。例如夜惭,在此設置一個條件對象來表達“ 余額充足”條件姻灶。
class Bank{
private Condition sufficientFunds;
···
public Bank(){
···
sufficientFunds=bankLock.newCondition();
}
}
如果transfer方法發(fā)現(xiàn)余額不足,它調用下面這個方法
sufficientFunds.await();
當前線程現(xiàn)在被阻塞了诈茧,并放棄了鎖产喉。我們希望這樣可以使得另一個線程可以進行增加賬戶余額的操作。等待獲得鎖的線程和調用await 方法的線程存在本質上的不同。一旦一個線程調用await方法曾沈,它進人該條件的等待集这嚣。當鎖可用時,該線程不能馬上解除阻塞塞俱。相反姐帚,它處于阻塞狀態(tài),直到另一個線程調用同一條件上的signalAll 方法時為止障涯。
synchronized關鍵字
在前面一節(jié)中罐旗,介紹了如何使用 Lock 和 Condition 對象。在進一步深人之前唯蝶,總結一下有關鎖和條件的關鍵之處:
- 鎖用來保護代碼片段九秀,任何時刻只能有一個線程執(zhí)行被保護的代碼。
- 鎖可以管理試圖進入被保護代碼段的線程生棍。
- 鎖可以擁有一個或多個相關的條件對象颤霎。
- 每個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。
Lock 和 Condition 接口為程序設計人員提供了高度的鎖定控制涂滴。然而友酱,大多數情況下,并不需要那樣的控制柔纵,并且可以使用一種嵌人到Java 語言內部的機制缔杉。
如果一個方法用synchronized 關鍵字聲明,那么對象的鎖將保護整個方法搁料。也就是說或详,要調用該方法,線程必須獲得內部的對象鎖郭计。
換句話說
public synchronized void method()
{
method body
}
其實方法鎖其實鎖的是實例對象霸琴,可以等價于
public void method(){
//相當于鎖實例
synchronized(this){
//需要同步的代碼塊
}
}
將靜態(tài)方法聲明為synchronized 也是合法的。如果調用這種方法昭伸,該方法獲得相關的類對象的內部鎖梧乘。例如,如果Bank類有一個靜態(tài)同步的方法庐杨,那么當該方法被調用時选调,Bankxlass對象的鎖被鎖住。因此灵份,沒有其他線程可以調用同一個類的這個或任何其他的同步靜態(tài)方法仁堪。
public static void method(){
//相當于鎖的整個類
synchronized(xxx.class){
//需要同步的代碼塊
}
}
靜態(tài)方法同步與非靜態(tài)方法同步區(qū)別:
- 靜態(tài)同步:因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用填渠,從而保證了該變量的唯一性和準確性弦聂。
- 非靜態(tài)同步:鎖住的是該對象,類的其中一個實例鸟辅,當該對象(僅僅是這一個對象)在不同線程中執(zhí)行這個同步方法時,線程之間會形成互斥横浑。達到同步效果剔桨,但如果不同線程同時對該類的不同對象執(zhí)行這個同步方法時,則線程之間不會形成互斥徙融,因為他們擁有的是不同的鎖洒缀。
內部鎖和條件存在一些局限。包括:
- 不能中斷一個正在試圖獲得鎖的線程欺冀。
- 試圖獲得鎖時不能設定超時树绩。
- 每個鎖僅有單一的條件,可能是不夠的
synchronized和ReentrantLock的比較
區(qū)別:
- Lock是一個接口隐轩,是通過 JDK 來實現(xiàn)的饺饭,而 synchronized 是 Java 中的關鍵字,synchronized 是內置的語言實現(xiàn)职车,是 JVM 實現(xiàn)的瘫俊;
- synchronized 在發(fā)生異常時,會自動釋放線程占有的鎖悴灵,因此不會導致死鎖現(xiàn)象發(fā)生扛芽;而 Lock 在發(fā)生異常時,如果沒有主動通過
unLock()
去釋放鎖积瞒,則很可能造成死鎖現(xiàn)象川尖,因此使用 Lock 時需要在 finally 塊中釋放鎖; - Lock可以讓等待鎖的線程響應中斷茫孔,而 synchronized 卻不行叮喳,使用 synchronized 時,等待的線程會一直等待下去缰贝,不能夠響應中斷馍悟;
- 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到剩晴。
- Lock可以提高多個線程進行讀操作的效率锣咒。
兩者在鎖的相關概念上區(qū)別:
- 可中斷鎖
顧名思義,就是可以響應中斷的鎖李破。
在Java中宠哄,synchronized就不是可中斷鎖壹将,而Lock是可中斷鎖嗤攻。如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖诽俯,可能由于等待時間過長妇菱,線程B不想等待了承粤,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它闯团,這種就是可中斷鎖辛臊。
lockInterruptibly()
的用法體現(xiàn)了Lock的可中斷性。 - 公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖房交。比如同是有多個線程在等待一個鎖彻舰,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該鎖(并不是絕對的候味,大體上是這種順序)刃唤,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的白群。這樣就可能導致某個或者一些線程永遠獲取不到鎖尚胞。
在Java中,synchronized 就是非公平鎖帜慢,它無法保證等待的線程獲取鎖的順序笼裳。ReentrantLock可以設置成公平鎖。 - 讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖粱玲,一個讀鎖和一個寫鎖躬柬。
正因為有了讀寫鎖,才使得多個線程之間的讀操作可以并發(fā)進行密幔,不需要同步楔脯,而寫操作需要同步進行,提高了效率胯甩。
ReadWriteLock就是讀寫鎖昧廷,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口偎箫。
可以通過readLock()獲取讀鎖木柬,通過writeLock()獲取寫鎖。 - 綁定多個條件
一個ReentrantLock對象可以同時綁定多個Condition對象淹办,而在synchronized中眉枕,鎖對象的wait()和notify()或notifyAll()方法可以實現(xiàn)一個隱含的條件,如果要和多余一個條件關聯(lián)的時候怜森,就不得不額外地添加一個鎖速挑,而ReentrantLock則無須這么做,只需要多次調用new Condition()方法即可副硅。
在新版的 JDK 中姥宝, synchronize 也逐漸有了很多優(yōu)化,除非我們需要用到 ReentrantLock 的高級功能(比如上述幾個鎖)恐疲,我們盡量選用 synchronize 關鍵詞腊满。
final
還有一種情況可以安全地訪問一個共享域套么,即這個域聲明為final 時√嫉埃考慮以下聲明:
final Map<String, Double〉accounts = new HashMap<>() 胚泌;
其他線程會在構造函數完成構造之后才看到這個accounts變量。
線程間協(xié)作
join
在線程中調用另一個線程的 join() 方法肃弟,會將當前線程掛起玷室,而不是忙等待,直到目標線程結束笤受。
wait阵苇、notify、notifyall
調用 wait() 使得線程等待某個條件滿足感论,線程在等待時會被掛起绅项,當其他線程的運行使得這個條件滿足時,其它線程會調用 notify() 或者 notifyAll() 來喚醒掛起的線程比肄。
它們都屬于 Object 的一部分快耿,而不屬于 Thread。
只能用在同步方法或者同步控制塊中使用芳绩,否則會在運行時拋出 IllegalMonitorStateException掀亥。
使用 wait() 掛起期間,線程會釋放鎖妥色。這是因為搪花,如果沒有釋放鎖,那么其它線程就無法進入對象的同步方法或者同步控制塊中嘹害,那么就無法執(zhí)行 notify() 或者 notifyAll() 來喚醒掛起的線程撮竿,造成死鎖。
死鎖
產生條件
- 互斥條件:一個資源每次只能被一個線程使用笔呀。
- 請求與保持條件:一個線程因請求資源而阻塞時幢踏,對已獲得的資源保持不放。
- 不剝奪條件:線程已獲得的資源许师,在未使用完之前房蝉,不能強行剝奪。
- 循環(huán)等待條件:若干線程之間形成一種頭尾相接的循環(huán)等待資源關系微渠。
有3種典型的死鎖類型:
靜態(tài)的鎖順序死鎖
a和b兩個方法都需要獲得A鎖和B鎖搭幻。一個線程執(zhí)行a方法且已經獲得了A鎖,在等待B鎖逞盆;另一個線程執(zhí)行了b方法且已經獲得了B鎖檀蹋,在等待A鎖。這種狀態(tài)纳击,就是發(fā)生了靜態(tài)的鎖順序死鎖续扔。
經典面試問題:寫一個死鎖
class StaticLockOrderDeadLock{
private final Object lockA = new Object();
private final Object lockB = new Object();
public void a(){
synchronized(lockA){
synchronized(lockB){
System.out.println("function a");
}
}
}
public void b(){
synchronized(lockB){
synchronized(lockA){
System.out.println("function b");
}
}
}
}
解決靜態(tài)的鎖順序死鎖的方法就是:所有需要多個鎖的線程,都要以相同的順序來獲得鎖焕数。
動態(tài)的鎖順序死鎖
動態(tài)的鎖順序死鎖是指兩個線程調用同一個方法時纱昧,傳入的參數顛倒造成的死鎖。
如下代碼堡赔,一個線程調用了transferMoney方法并傳入參數accountA,accountB识脆;另一個線程調用了transferMoney方法并傳入參數accountB,accountA。此時就可能發(fā)生在靜態(tài)的鎖順序死鎖中存在的問題善已,即:第一個線程獲得了accountA鎖并等待accountB鎖灼捂,第二個線程獲得了accountB鎖并等待accountA鎖。
動態(tài)的鎖順序死鎖解決方案如下:使用System.identifyHashCode來定義鎖的順序换团。確保所有的線程都以相同的順序獲得鎖悉稠。
協(xié)作對象之間發(fā)生的死鎖
有時,死鎖并不會那么明顯艘包,比如兩個相互協(xié)作的類之間的死鎖的猛,比如下面的代碼:一個線程調用了Taxi對象的setLocation方法,另一個線程調用了Dispatcher對象的getImage方法想虎。此時可能會發(fā)生卦尊,第一個線程持有Taxi對象鎖并等待Dispatcher對象鎖,另一個線程持有Dispatcher對象鎖并等待Taxi對象鎖舌厨。
上面的代碼中岂却,我們在持有鎖的情況下調用了外部的方法,這是非常危險的(可能發(fā)生死鎖)裙椭。為了避免這種危險的情況發(fā)生躏哩,我們使用開放調用。如果調用某個外部方法時不需要持有鎖揉燃,我們稱之為開放調用震庭。解決協(xié)作對象之間發(fā)生的死鎖:需要使用開放調用,即避免在持有鎖的情況下調用外部的方法你雌。
鎖優(yōu)化
多線程
更多關于Java并發(fā)多線程請點擊Java進階學習多線程