多線程的創(chuàng)建和啟動
點擊上方“IT那活兒”公眾號,關(guān)注后了解更多內(nèi)容,不管IT什么活兒,干就完了!??!
- Java語言的JVM允許程序運行多個線程,多線程可以通過Java中的java.lang.Thread類來體現(xiàn)。
每個線程都是通過某個特定的Thread對象的run()方法來完成操作,經(jīng)常把run()方法的主體作為線程體。 通過Thread方法的start()方法來啟動這個線程,而非直接調(diào)用run()。2. 多線程的創(chuàng)建
2.1 繼承Thread類
- 創(chuàng)建一個繼承于Thread類的子類;
- 通過此對象調(diào)用start()來啟動一個線程。
示例三--創(chuàng)建Thread匿名子類:2.2 實現(xiàn)Runnable接口
- 創(chuàng)建一個實現(xiàn)Runnable接口的類;
- 實現(xiàn)類去實現(xiàn)Runnable接口中的抽象方法:run();
- 將此對象作為參數(shù)傳到Thread類的構(gòu)造器中,創(chuàng)建Thread類的對象;
- 通過Thread類的對象調(diào)用start()方法。
2.3 兩種創(chuàng)建方式比較
- Java中只允許單進(jìn)程,以賣票程序TiketSales類來說,很有可能這個類本來就有父類,這樣一來就不可以繼承Thread類來完成多線程了,但是一個類可以實現(xiàn)多個接口,因此實現(xiàn)的方式?jīng)]有類的單繼承性的局限性,用實現(xiàn)Runnable接口的方式來完成多線程更加實用。
- 實現(xiàn)Runnable接口的方式天然具有共享數(shù)據(jù)的特性(不用static變量)。因為繼承Thread的實現(xiàn)方式,需要創(chuàng)建多個子類的對象來進(jìn)行多線程,如果子類中有變量A,而不使用static約束變量的話,每個子類的對象都會有自己獨立的變量A,只有static約束A后,子類的對象才共享變量A。而實現(xiàn)Runnable接口的方式,只需要創(chuàng)建一個實現(xiàn)類的對象,要將這個對象傳入Thread類并創(chuàng)建多個Thread類的對象來完成多線程,而這多個Thread類對象實際上就是調(diào)用一個實現(xiàn)類對象而已。實現(xiàn)的方式更適合來處理多個線程有共享數(shù)據(jù)的情況。
- 聯(lián)系:Thread類中也實現(xiàn)了Runnable接口。
- 相同點:兩種方式都需要重寫run()方法,線程的執(zhí)行邏輯都在run()方法中。
2.4 通過實現(xiàn)Callable接口
與Runnable相比,Callable功能更加強(qiáng)大:相比run()方法,可以有返回值;
- 需要借助FutureTask類,比如獲取返回結(jié)果。
2.5 通過線程池創(chuàng)建
經(jīng)常創(chuàng)建和銷毀、使用量特別大的資源,比如并發(fā)情況下的線程,對性能影響很大。提前創(chuàng)建好多個線程,放入線程池中,使用時直接獲取,使用完放回線程池中??梢员苊忸l繁的創(chuàng)建銷毀,實現(xiàn)重復(fù)利用。3)優(yōu)點
- 提高響應(yīng)速度(減少了創(chuàng)建新線程的時間);
- 降低資源消耗(重復(fù)利用線程池中線程,不需要每次都創(chuàng)建);
- start():啟動當(dāng)前線程, 調(diào)用當(dāng)前線程的run()方法;
- run() : 通常需要重寫Thread類中的此方法, 將創(chuàng)建的線程要執(zhí)行的操作聲明在此方法中;
- currentThread() : 靜態(tài)方法, 返回當(dāng)前代碼執(zhí)行的線程;
- getName() : 獲取當(dāng)前線程的名字;
- setName() : 設(shè)置當(dāng)前線程的名字;
- yield() : 釋放當(dāng)前CPU的執(zhí)行權(quán);
- join() : 在線程a中調(diào)用線程b的join(), 此時線程a進(jìn)入阻塞狀態(tài), 知道線程b完全執(zhí)行完以后, 線程a才結(jié)束阻塞狀態(tài);
- stop() : 已過時. 當(dāng)執(zhí)行此方法時,強(qiáng)制結(jié)束當(dāng)前線程;
- sleep(long militime) : 讓線程睡眠指定的毫秒數(shù),在指定時間內(nèi),線程是阻塞狀態(tài);
- isAlive() :判斷當(dāng)前線程是否存活。
1. CPU的調(diào)度策略
時間片:cpu正常情況下的調(diào)度策略。即CPU分配給各個程序的時間,每個線程被分配一個時間段,稱作它的時間片,即該進(jìn)程允許運行的時間,使各個程序從表面上看是同時進(jìn)行的。
如果在時間片結(jié)束時進(jìn)程還在運行,則CPU將被剝奪并分配給另一個進(jìn)程。如果進(jìn)程在時間片結(jié)束前阻塞或結(jié)束,則CPU當(dāng)即進(jìn)行切換。而不會造成CPU資源浪費。在宏觀上:我們可以同時打開多個應(yīng)用程序,每個程序并行不悖,同時運行。
在微觀上:由于只有一個CPU,一次只能處理程序要求的一部分,如何處理公平,一種方法就是引入時間片,每個程序輪流執(zhí)行。
2. Java的調(diào)度算法
- 同優(yōu)先級線程組成先進(jìn)先出隊列(先到先服務(wù)),使用時間片策略。
- 堆高優(yōu)先級,使用優(yōu)先調(diào)度的搶占式策略。
1)線程的優(yōu)先級等級(一共有10檔)
2)獲取和設(shè)置當(dāng)前線程的優(yōu)先級
- setPriority(int p)設(shè)置。
說明:高優(yōu)先級的線程要搶占低優(yōu)先級線程cpu的執(zhí)行權(quán)。但是只是從概率上講,高優(yōu)先級的線程高概率的情況下被執(zhí)行。并不意味著只有高優(yōu)先級的線程執(zhí)行完成以后,低優(yōu)先級的線程才執(zhí)行。
1. JDK中用Thread State類定義了線程的幾種狀態(tài)
- 新建:當(dāng)一個Thread類或其子類的對象被聲明并創(chuàng)建時,新的線程對象處于新建狀態(tài)。
- 就緒:處于新建狀態(tài)的線程被start()后,將進(jìn)入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源。
- 運行:當(dāng)就緒的線程被調(diào)度并獲得CPU資源時,便進(jìn)入運行狀態(tài),run()方法定義了線程的操作和功能。
- 阻塞:在某種特殊情況下,被認(rèn)為掛起或執(zhí)行輸入輸出操作時,讓出CPU并臨時中止自己的執(zhí)行,進(jìn)入阻塞狀態(tài)。
- 死亡:線程完成了它的全部工作或線程被提前強(qiáng)制性的中止或出現(xiàn)異常倒置導(dǎo)致結(jié)束。
1. 多線程的安全性問題解析
1.1 線程的安全問題
- 多個線程執(zhí)行的不確定性引起執(zhí)行結(jié)果的不穩(wěn)定性;
- 多個線程對賬本的共享, 會造成操作的不完整性, 會破壞數(shù)據(jù);
- 多個線程訪問共享的數(shù)據(jù)時可能存在安全性問題。
當(dāng)票數(shù)為1的時候,三個線程中有線程被阻塞沒有執(zhí)行票數(shù)-1的操作,這是其它線程就會通過if語句的判斷,這樣一來就會造成多賣了一張票,出現(xiàn)錯票的情況。極端情況為,當(dāng)票數(shù)為1時,三個線程同時判斷通過,進(jìn)入阻塞,然后多執(zhí)行兩側(cè)賣票操作。如果t1在輸出票號22和票數(shù)-1的操作之間被阻塞,這就導(dǎo)致這時候t1賣出了22號票,但是總票數(shù)沒有減少。在t1被阻塞期間,如果t2運行到輸出票號時,那么t2也會輸出和t1相同的票號22。通過以上兩種情況可以看出,線程的安全性問題時因為多個線程正在執(zhí)行代碼的過程中,并且尚未完成的時候,其他線程參與進(jìn)來執(zhí)行代碼所導(dǎo)致的。2. 多線程安全性問題解決
當(dāng)一個線程在操作共享數(shù)據(jù)的時候,其他線程不能參與進(jìn)來。知道這個線程操作完共享數(shù)據(jù)的時候,其他線程才可以操作。即使當(dāng)這個線程操作共享數(shù)據(jù)的時候發(fā)生了阻塞,依舊無法改變這種情況。在Java中,我們通過同步機(jī)制,來解決線程的安全問題。synchronized(同步監(jiān)視器){需要被同步的代碼塊}
- 優(yōu)點:同步的方式,解決了線程安全的問題。
- 缺點:操作同步代碼時,只能有一個線程參與,與其他線程等待。相當(dāng)于是一個單線程的過程,效率低。
將所要同步的代碼放到一個方法中,將方法聲明為synchronized同步方法。之后可以在run()方法中調(diào)用同步方法。要點:
- 同步方法仍然涉及到同步監(jiān)視器,只是不需要我們顯示的聲明。
- 非靜態(tài)的同步方法,同步監(jiān)視器是:this。
- 靜態(tài)的同步方法,同步監(jiān)視器是:當(dāng)前類本身。
JDK5.0之后,可以通過實例化ReentrantLock對象,在所需要同步的語句前,調(diào)用ReentrantLock對象的lock()方法,實現(xiàn)同步鎖,在同步語句結(jié)束時,調(diào)用unlock()方法結(jié)束同步鎖。建議使用順序:Lock->同步代碼塊(已經(jīng)進(jìn)入了方法體,分配了相應(yīng)的資源)->同步方法(在方法體之外)。2.3 線程同步的死鎖問題
1)原理
- 不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了死鎖。
- 出現(xiàn)死鎖后,并不會出現(xiàn)異常,不會出現(xiàn)提示,只是所有的線程都處于阻塞狀態(tài),無法繼續(xù)。
- 使用同步時應(yīng)避免出現(xiàn)死鎖。
一個線程T1持有鎖L1并且申請獲得鎖L2,而另一個線程T2持有鎖L2并且申請獲得鎖L1,因為默認(rèn)的鎖申請操作都是阻塞的,所以線程T1和T2永遠(yuǎn)被阻塞了。導(dǎo)致了死鎖。這是最容易理解也是最簡單的死鎖的形式。但是實際環(huán)境中的死鎖往往比這個復(fù)雜的多??赡軙卸鄠€線程形成了一個死鎖的環(huán)路,比如:線程T1持有鎖L1并且申請獲得鎖L2,而線程T2持有鎖L2并且申請獲得鎖L3,而線程T3持有鎖L3并且申請獲得鎖L1,這樣導(dǎo)致了一個鎖依賴的環(huán)路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而導(dǎo)致了死鎖。線程在獲得一個鎖L1的情況下再去申請另外一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在獲得了鎖L1,并且沒有釋放鎖L1的情況下,又去申請獲得鎖L2,這個是產(chǎn)生死鎖的最根本原因。另一個原因是默認(rèn)的鎖申請操作是阻塞的。3)死鎖的解決辦法
很多情況下,盡管我們創(chuàng)建了多個線程,也會出現(xiàn)幾乎一個線程執(zhí)行完所有操作的時候,這時候我們就需要讓線程間相互交流。
當(dāng)一個線程執(zhí)行完成其所應(yīng)該執(zhí)行的代碼后,手動讓這個線程進(jìn)入阻塞狀態(tài),這樣一來,接下來的操作只能由其他線程來操作。當(dāng)其他線程執(zhí)行的開始階段,再手動讓已經(jīng)阻塞的線程停止阻塞,進(jìn)入就緒狀態(tài),雖說這時候阻塞的線程停止了阻塞,但是由于現(xiàn)在正在運行的線程拿著同步鎖,所以停止阻塞的線程也無法立馬執(zhí)行。2. 所用到的方法
- wait():一旦執(zhí)行此方法,當(dāng)前線程就會進(jìn)入阻塞,一旦執(zhí)行wait()會釋放同步監(jiān)視器。
- notify():一旦執(zhí)行此方法,將會喚醒被wait的一個線程。如果有多個線程被wait,就喚醒優(yōu)先度最高的。
- notifyAll() :一旦執(zhí)行此方法,就會喚醒所有被wait的線程。
三個方法的調(diào)用者必須是同步代碼塊或同步方法中的同步監(jiān)視器。 這三個方法并不時定義在Thread類中的,而是定義在Object類當(dāng)中的。因為所有的對象都可以作為同步監(jiān)視器,而這三個方法需要由同步監(jiān)視器調(diào)用,所以任何一個類都要滿足,那么只能寫在Object類中。相同點:兩個方法一旦執(zhí)行,都可以讓線程進(jìn)入阻塞狀態(tài)。不同點:
- 兩個方法聲明的位置不同:Thread類中聲明sleep(),Object類中聲明wait()。
- 調(diào)用要求不同:sleep()可以在任何需要的場景下調(diào)用。wait()必須在同步代碼塊中調(diào)用。
- 關(guān)于是否釋放同步監(jiān)視器:如果兩個方法都使用在同步代碼塊呵呵同步方法中,sleep不會釋放鎖,wait會釋放鎖。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/129430.html