Java 高并發(fā)二:多線程基礎(chǔ)詳細(xì)介紹
本系列基于煉數(shù)成金課程,為了更好的學(xué)習(xí),做了系列的記錄。 本文主要介紹 1.什么是線程 2.線程的基本操作 3.守護(hù)線程 4.線程優(yōu)先級(jí) 5.基本的線程同步操作
1. 什么是線程
線程是進(jìn)程內(nèi)的執(zhí)行單元
某個(gè)進(jìn)程當(dāng)中都有若干個(gè)線程。
線程是進(jìn)程內(nèi)的執(zhí)行單元。
使用線程的原因是,進(jìn)程的切換是非常重量級(jí)的操作,非常消耗資源。如果使用多進(jìn)程,那么并發(fā)數(shù)相對(duì)來說不會(huì)很高。而線程是更細(xì)小的調(diào)度單元,更加輕量級(jí),所以線程會(huì)較為廣泛的用于并發(fā)設(shè)計(jì)。
在Java當(dāng)中線程的概念和操作系統(tǒng)級(jí)別線程的概念是類似的。事實(shí)上,Jvm將會(huì)把Java中的線程映射到操作系統(tǒng)的線程區(qū)。
2. 線程的基本操作
2.1 線程狀態(tài)圖
上圖是Java中線程的基本操作。
當(dāng)new出一個(gè)線程時(shí),其實(shí)線程并沒有工作。它只是生成了一個(gè)實(shí)體,當(dāng)你調(diào)用這個(gè)實(shí)例的start方法時(shí),線程才真正地被啟動(dòng)。啟動(dòng)后到Runnable狀態(tài),Runnable表示該線程的資源等等已經(jīng)被準(zhǔn)備好,已經(jīng)可以執(zhí)行了,但是并不表示一定在執(zhí)行狀態(tài),由于時(shí)間片輪轉(zhuǎn),該線程也可能此時(shí)并沒有在執(zhí)行。對(duì)于我們來說,該線程可以認(rèn)為已經(jīng)被執(zhí)行了,但是是否真實(shí)執(zhí)行,還得看物理cpu的調(diào)度。當(dāng)線程任務(wù)執(zhí)行結(jié)束后,線程就到了Terminated狀態(tài)。
有時(shí)候在線程的執(zhí)行當(dāng)中,不可避免的會(huì)申請(qǐng)某些鎖或某個(gè)對(duì)象的監(jiān)視器,當(dāng)無法獲取時(shí),這個(gè)線程會(huì)被阻塞住,會(huì)被掛起,到了Blocked狀態(tài)。如果這個(gè)線程調(diào)用了wait方法,它就處于一個(gè)Waiting狀態(tài)。進(jìn)入Waiting狀態(tài)的線程會(huì)等待其他線程給它notify,通知到之后由Waiting狀態(tài)又切換到Runnable狀態(tài)繼續(xù)執(zhí)行。當(dāng)然等待狀態(tài)有兩種,一種是無限期等待,直到被notify。一直則是有限期等待,比如等待10秒還是沒有被notify,則自動(dòng)切換到Runnable狀態(tài)。
2.2 新建線程
Thread thread = new Thread();
thread.start();
這樣就開啟了一個(gè)線程。
有一點(diǎn)需要注意的是
Thread thread = new Thread();
thread.run();
直接調(diào)用run方法是無法開啟一個(gè)新線程的。
start方法其實(shí)是在一個(gè)新的操作系統(tǒng)線程上面去調(diào)用run方法。換句話說,直接調(diào)用run方法而不是調(diào)用start方法的話,它并不會(huì)開啟新的線程,而是在調(diào)用run的當(dāng)前的線程當(dāng)中執(zhí)行你的操作。
Thread thread = new Thread("t1") { @Override public void run() { // TODO Auto-generated method stub System.out.println(Thread.currentThread().getName()); } }; thread.start(); 如果調(diào)用start,則輸出是t1 Thread thread = new Thread("t1") { @Override public void run() { // TODO Auto-generated method stub System.out.println(Thread.currentThread().getName()); } }; thread.run();
如果是run,則輸出main。(直接調(diào)用run其實(shí)就是一個(gè)普通的函數(shù)調(diào)用而已,并沒有達(dá)到多線程的作用)
run方法的實(shí)現(xiàn)有兩種方式
第一種方式,直接覆蓋run方法,就如剛剛代碼中所示,最方便的用一個(gè)匿名類就可以實(shí)現(xiàn)。
Thread thread = new Thread("t1") { @Override public void run() { // TODO Auto-generated method stub System.out.println(Thread.currentThread().getName()); } };
第二種方式
Thread t1=new Thread(new CreateThread3());
CreateThread3()實(shí)現(xiàn)了Runnable接口。
在張孝祥的視頻中,推薦第二種方式,稱其更加面向?qū)ο蟆?/p>
2.3 終止線程
Thread.stop() 不推薦使用。它會(huì)釋放所有monitor
在源碼中已經(jīng)明確說明stop方法被Deprecated,在Javadoc中也說明了原因。
原因在于stop方法太過"暴力"了,無論線程執(zhí)行到哪里,它將會(huì)立即停止掉線程。
當(dāng)寫線程得到鎖以后開始寫入數(shù)據(jù),寫完id = 1,在準(zhǔn)備將name = 1時(shí)被stop,釋放鎖。讀線程獲得鎖進(jìn)行讀操作,讀到的id為1,而name還是0,導(dǎo)致了數(shù)據(jù)不一致。
最重要的是這種錯(cuò)誤不會(huì)拋出異常,將很難被發(fā)現(xiàn)。
2.4 線程中斷
線程中斷有3種方法
public void Thread.interrupt() // 中斷線程
public boolean Thread.isInterrupted() // 判斷是否被中斷
public static boolean Thread.interrupted() // 判斷是否被中斷,并清除當(dāng)前中斷狀態(tài)
什么是線程中斷呢?
如果不了解Java的中斷機(jī)制,這樣的一種解釋極容易造成誤解,認(rèn)為調(diào)用了線程的interrupt方法就一定會(huì)中斷線程。
其實(shí),Java的中斷是一種協(xié)作機(jī)制。也就是說調(diào)用線程對(duì)象的interrupt方法并不一定就中斷了正在運(yùn)行的線程,它只是要求線程自己在合適的時(shí)機(jī)中斷自己。每個(gè)線程都有一個(gè)boolean的中斷狀態(tài)(不一定就是對(duì)象的屬性,事實(shí)上,該狀態(tài)也確實(shí)不是Thread的字段),interrupt方法僅僅只是將該狀態(tài)置為true。對(duì)于非阻塞中的線程, 只是改變了中斷狀態(tài), 即Thread.isInterrupted()將返回true,并不會(huì)使程序停止;
public void run(){//線程t1 while(true){ Thread.yield(); } } t1.interrupt();
這樣使線程t1中斷,是不會(huì)有效果的,只是更改了中斷狀態(tài)位。
如果希望非常優(yōu)雅地終止這個(gè)線程,就該這樣做
public void run(){ while(true) { if(Thread.currentThread().isInterrupted()) { System.out.println("Interruted!"); break; } Thread.yield(); } }
使用中斷,就對(duì)數(shù)據(jù)一致性有了一定的保證。
對(duì)于可取消的阻塞狀態(tài)中的線程, 比如等待在這些函數(shù)上的線程, Thread.sleep(), Object.wait(), Thread.join(), 這個(gè)線程收到中斷信號(hào)后, 會(huì)拋出InterruptedException, 同時(shí)會(huì)把中斷狀態(tài)置回為false.
對(duì)于取消阻塞狀態(tài)中的線程,可以這樣抒寫代碼:
public void run(){ while(true){ if(Thread.currentThread().isInterrupted()){ System.out.println("Interruted!"); break; } try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("Interruted When Sleep"); //設(shè)置中斷狀態(tài),拋出異常后會(huì)清除中斷標(biāo)記位 Thread.currentThread().interrupt(); } Thread.yield(); } }
2.5 線程掛起
掛起(suspend)和繼續(xù)執(zhí)行(resume)線程
suspend()不會(huì)釋放鎖
如果加鎖發(fā)生在resume()之前 ,則死鎖發(fā)生
這兩個(gè)方法都是Deprecated方法,不推薦使用。
原因在于,suspend不釋放鎖,因此沒有線程可以訪問被它鎖住的臨界區(qū)資源,直到被其他線程resume。因?yàn)闊o法控制線程運(yùn)行的先后順序,如果其他線程的resume方法先被運(yùn)行,那則后運(yùn)行的suspend,將一直占有這把鎖,造成死鎖發(fā)生。
用以下代碼來模擬這個(gè)場景
package test; public class Test { static Object u = new Object(); static TestSuspendThread t1 = new TestSuspendThread("t1"); static TestSuspendThread t2 = new TestSuspendThread("t2"); public static class TestSuspendThread extends Thread { public TestSuspendThread(String name) { setName(name); } @Override public void run() { synchronized (u) { System.out.println("in " + getName()); Thread.currentThread().suspend(); } } } public static void main(String[] args) throws InterruptedException { t1.start(); Thread.sleep(100); t2.start(); t1.resume(); t2.resume(); t1.join(); t2.join(); } }
讓t1,t2同時(shí)爭奪一把鎖,爭奪到的線程suspend,然后再resume,按理來說,應(yīng)該某個(gè)線程爭奪后被resume釋放了鎖,然后另一個(gè)線程爭奪掉鎖,再被resume。
結(jié)果輸出是:
in t1
in t2
說明兩個(gè)線程都爭奪到了鎖,但是控制臺(tái)的紅燈還是亮著的,說明t1,t2一定有線程沒有執(zhí)行完。我們dump出堆來看看
發(fā)現(xiàn)t2一直被suspend。這樣就造成了死鎖。
2.6 join和yeild
yeild是個(gè)native靜態(tài)方法,這個(gè)方法是想把自己占有的cpu時(shí)間釋放掉,然后和其他線程一起競爭(注意yeild的線程還是有可能爭奪到cpu,注意與sleep區(qū)別)。在javadoc中也說明了,yeild是個(gè)基本不會(huì)用到的方法,一般在debug和test中使用。
join方法的意思是等待其他線程結(jié)束,就如suspend那節(jié)的代碼,想讓主線程等待t1,t2結(jié)束以后再結(jié)束。沒有結(jié)束的話,主線程就一直阻塞在那里。
package test; public class Test { public volatile static int i = 0; public static class AddThread extends Thread { @Override public void run() { for (i = 0; i < 10000000; i++) ; } } public static void main(String[] args) throws InterruptedException { AddThread at = new AddThread(); at.start(); at.join(); System.out.println(i); } }
如果把上述代碼的at.join去掉,則主線程會(huì)直接運(yùn)行結(jié)束,i的值會(huì)很小。如果有join,打印出的i的值一定是10000000。
那么join是怎么實(shí)現(xiàn)的呢?
join的本質(zhì)
while(isAlive())
{
wait(0);
}
join()方法也可以傳遞一個(gè)時(shí)間,意為有限期地等待,超過了這個(gè)時(shí)間就自動(dòng)喚醒。
這樣就有一個(gè)問題,誰來notify這個(gè)線程呢,在thread類中沒有地方調(diào)用了notify?
在javadoc中,找到了相關(guān)解釋。當(dāng)一個(gè)線程運(yùn)行完成終止后,將會(huì)調(diào)用notifyAll方法去喚醒等待在當(dāng)前線程實(shí)例上的所有線程,這個(gè)操作是jvm自己完成的。
所以javadoc中還給了我們一個(gè)建議,不要使用wait和notify/notifyall在線程實(shí)例上。因?yàn)閖vm會(huì)自己調(diào)用,有可能與你調(diào)用期望的結(jié)果不同。
3. 守護(hù)線程
在后臺(tái)默默地完成一些系統(tǒng)性的服務(wù),比如垃圾回收線程、JIT線程就可以理解為守護(hù)線程。
當(dāng)一個(gè)Java應(yīng)用內(nèi),所有非守護(hù)進(jìn)程都結(jié)束時(shí),Java虛擬機(jī)就會(huì)自然退出。
此前有寫過一篇python中如何實(shí)現(xiàn),查看這里。
而Java中變成守護(hù)進(jìn)程就相對(duì)簡單了。
Thread t=new DaemonT();
t.setDaemon(true);
t.start();
這樣就開啟了一個(gè)守護(hù)線程。
package test; public class Test { public static class DaemonThread extends Thread { @Override public void run() { for (int i = 0; i < 10000000; i++) { System.out.println("hi"); } } } public static void main(String[] args) throws InterruptedException { DaemonThread dt = new DaemonThread(); dt.start(); } }
當(dāng)線程dt不是一個(gè)守護(hù)線程時(shí),在運(yùn)行后,我們能看到控制臺(tái)輸出hi
當(dāng)在start之前加入
dt.setDaemon(true);
控制臺(tái)就直接退出了,并沒有輸出。
4. 線程優(yōu)先級(jí)
Thread類中有3個(gè)變量定義了線程優(yōu)先級(jí)。
public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; package test; public class Test { public static class High extends Thread { static int count = 0; @Override public void run() { while (true) { synchronized (Test.class) { count++; if (count > 10000000) { System.out.println("High"); break; } } } } } public static class Low extends Thread { static int count = 0; @Override public void run() { while (true) { synchronized (Test.class) { count++; if (count > 10000000) { System.out.println("Low"); break; } } } } } public static void main(String[] args) throws InterruptedException { High high = new High(); Low low = new Low(); high.setPriority(Thread.MAX_PRIORITY); low.setPriority(Thread.MIN_PRIORITY); low.start(); high.start(); } }
讓一個(gè)高優(yōu)先級(jí)的線程和低優(yōu)先級(jí)的線程同時(shí)爭奪一個(gè)鎖,看看哪個(gè)最先完成。
當(dāng)然并不一定是高優(yōu)先級(jí)一定先完成。再多次運(yùn)行后發(fā)現(xiàn),高優(yōu)先級(jí)完成的概率比較大,但是低優(yōu)先級(jí)還是有可能先完成的。
5. 基本的線程同步操作
synchronized 和 Object.wait() Obejct.notify()
這一節(jié)內(nèi)容詳情請(qǐng)看以前寫的一篇Blog
主要要注意的是
synchronized有三種加鎖方式:
指定加鎖對(duì)象:對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼前要獲得給定對(duì)象的鎖。
直接作用于實(shí)例方法:相當(dāng)于對(duì)當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖。
直接作用于靜態(tài)方法:相當(dāng)于對(duì)當(dāng)前類加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類的鎖。
作用于實(shí)例方法,則不要new兩個(gè)不同的實(shí)例
作用于靜態(tài)方法,只要類一樣就可以了,因?yàn)榧拥逆i是類.class,可以new兩個(gè)不同實(shí)例。
wait和notify的用法:
用什么鎖住,就用什么調(diào)用wait和notify
本文就不細(xì)說了。
相關(guān)文章
Windows Zookeeper安裝過程及啟動(dòng)圖解
這篇文章主要介紹了Windows Zookeeper安裝過程及啟動(dòng)圖解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12java使用wait()和notify()線程間通訊的實(shí)現(xiàn)
Java 線程通信是將多個(gè)獨(dú)立的線程個(gè)體進(jìn)行關(guān)聯(lián)處理,使得線程與線程之間能進(jìn)行相互通信,本文就介紹了java使用wait()和notify()線程間通訊的實(shí)現(xiàn),感興趣的可以了解一下2023-09-09MyBatis中的關(guān)聯(lián)關(guān)系配置與多表查詢的操作代碼
本文介紹了在MyBatis中配置和使用一對(duì)多和多對(duì)多關(guān)系的方法,通過合理的實(shí)體類設(shè)計(jì)、Mapper接口和XML文件的配置,我們可以方便地進(jìn)行多表查詢,并豐富了應(yīng)用程序的功能和靈活性,需要的朋友可以參考下2023-09-09spring框架cacheAnnotation緩存注釋聲明解析
這篇文章主要介紹了spring框架中cacheAnnotation注釋聲明緩存解析示例有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10Java Collections.EMPTY_LIST與Collections.emptyList()的區(qū)別
這篇文章主要介紹了Java Collections.EMPTY_LIST與Collections.emptyList()的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11springboot 多數(shù)據(jù)源的實(shí)現(xiàn)(最簡單的整合方式)
這篇文章主要介紹了springboot 多數(shù)據(jù)源的實(shí)現(xiàn)(最簡單的整合方式),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11