Java 高并發(fā)二:多線(xiàn)程基礎(chǔ)詳細(xì)介紹
本系列基于煉數(shù)成金課程,為了更好的學(xué)習(xí),做了系列的記錄。 本文主要介紹 1.什么是線(xiàn)程 2.線(xiàn)程的基本操作 3.守護(hù)線(xiàn)程 4.線(xiàn)程優(yōu)先級(jí) 5.基本的線(xiàn)程同步操作
1. 什么是線(xiàn)程
線(xiàn)程是進(jìn)程內(nèi)的執(zhí)行單元

某個(gè)進(jìn)程當(dāng)中都有若干個(gè)線(xiàn)程。
線(xiàn)程是進(jìn)程內(nèi)的執(zhí)行單元。
使用線(xiàn)程的原因是,進(jìn)程的切換是非常重量級(jí)的操作,非常消耗資源。如果使用多進(jìn)程,那么并發(fā)數(shù)相對(duì)來(lái)說(shuō)不會(huì)很高。而線(xiàn)程是更細(xì)小的調(diào)度單元,更加輕量級(jí),所以線(xiàn)程會(huì)較為廣泛的用于并發(fā)設(shè)計(jì)。
在Java當(dāng)中線(xiàn)程的概念和操作系統(tǒng)級(jí)別線(xiàn)程的概念是類(lèi)似的。事實(shí)上,Jvm將會(huì)把Java中的線(xiàn)程映射到操作系統(tǒng)的線(xiàn)程區(qū)。
2. 線(xiàn)程的基本操作
2.1 線(xiàn)程狀態(tài)圖

上圖是Java中線(xiàn)程的基本操作。
當(dāng)new出一個(gè)線(xiàn)程時(shí),其實(shí)線(xiàn)程并沒(méi)有工作。它只是生成了一個(gè)實(shí)體,當(dāng)你調(diào)用這個(gè)實(shí)例的start方法時(shí),線(xiàn)程才真正地被啟動(dòng)。啟動(dòng)后到Runnable狀態(tài),Runnable表示該線(xiàn)程的資源等等已經(jīng)被準(zhǔn)備好,已經(jīng)可以執(zhí)行了,但是并不表示一定在執(zhí)行狀態(tài),由于時(shí)間片輪轉(zhuǎn),該線(xiàn)程也可能此時(shí)并沒(méi)有在執(zhí)行。對(duì)于我們來(lái)說(shuō),該線(xiàn)程可以認(rèn)為已經(jīng)被執(zhí)行了,但是是否真實(shí)執(zhí)行,還得看物理cpu的調(diào)度。當(dāng)線(xiàn)程任務(wù)執(zhí)行結(jié)束后,線(xiàn)程就到了Terminated狀態(tài)。
有時(shí)候在線(xiàn)程的執(zhí)行當(dāng)中,不可避免的會(huì)申請(qǐng)某些鎖或某個(gè)對(duì)象的監(jiān)視器,當(dāng)無(wú)法獲取時(shí),這個(gè)線(xiàn)程會(huì)被阻塞住,會(huì)被掛起,到了Blocked狀態(tài)。如果這個(gè)線(xiàn)程調(diào)用了wait方法,它就處于一個(gè)Waiting狀態(tài)。進(jìn)入Waiting狀態(tài)的線(xiàn)程會(huì)等待其他線(xiàn)程給它notify,通知到之后由Waiting狀態(tài)又切換到Runnable狀態(tài)繼續(xù)執(zhí)行。當(dāng)然等待狀態(tài)有兩種,一種是無(wú)限期等待,直到被notify。一直則是有限期等待,比如等待10秒還是沒(méi)有被notify,則自動(dòng)切換到Runnable狀態(tài)。
2.2 新建線(xiàn)程
Thread thread = new Thread();
thread.start();
這樣就開(kāi)啟了一個(gè)線(xiàn)程。
有一點(diǎn)需要注意的是
Thread thread = new Thread();
thread.run();
直接調(diào)用run方法是無(wú)法開(kāi)啟一個(gè)新線(xiàn)程的。
start方法其實(shí)是在一個(gè)新的操作系統(tǒng)線(xiàn)程上面去調(diào)用run方法。換句話(huà)說(shuō),直接調(diào)用run方法而不是調(diào)用start方法的話(huà),它并不會(huì)開(kāi)啟新的線(xiàn)程,而是在調(diào)用run的當(dāng)前的線(xiàn)程當(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)用而已,并沒(méi)有達(dá)到多線(xiàn)程的作用)
run方法的實(shí)現(xiàn)有兩種方式
第一種方式,直接覆蓋run方法,就如剛剛代碼中所示,最方便的用一個(gè)匿名類(lèi)就可以實(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接口。
在張孝祥的視頻中,推薦第二種方式,稱(chēng)其更加面向?qū)ο蟆?/p>
2.3 終止線(xiàn)程
Thread.stop() 不推薦使用。它會(huì)釋放所有monitor
在源碼中已經(jīng)明確說(shuō)明stop方法被Deprecated,在Javadoc中也說(shuō)明了原因。
原因在于stop方法太過(guò)"暴力"了,無(wú)論線(xiàn)程執(zhí)行到哪里,它將會(huì)立即停止掉線(xiàn)程。

當(dāng)寫(xiě)線(xiàn)程得到鎖以后開(kāi)始寫(xiě)入數(shù)據(jù),寫(xiě)完id = 1,在準(zhǔn)備將name = 1時(shí)被stop,釋放鎖。讀線(xiàn)程獲得鎖進(jìn)行讀操作,讀到的id為1,而name還是0,導(dǎo)致了數(shù)據(jù)不一致。
最重要的是這種錯(cuò)誤不會(huì)拋出異常,將很難被發(fā)現(xiàn)。
2.4 線(xiàn)程中斷
線(xiàn)程中斷有3種方法
public void Thread.interrupt() // 中斷線(xiàn)程
public boolean Thread.isInterrupted() // 判斷是否被中斷
public static boolean Thread.interrupted() // 判斷是否被中斷,并清除當(dāng)前中斷狀態(tài)
什么是線(xiàn)程中斷呢?
如果不了解Java的中斷機(jī)制,這樣的一種解釋極容易造成誤解,認(rèn)為調(diào)用了線(xiàn)程的interrupt方法就一定會(huì)中斷線(xiàn)程。
其實(shí),Java的中斷是一種協(xié)作機(jī)制。也就是說(shuō)調(diào)用線(xiàn)程對(duì)象的interrupt方法并不一定就中斷了正在運(yùn)行的線(xiàn)程,它只是要求線(xiàn)程自己在合適的時(shí)機(jī)中斷自己。每個(gè)線(xiàn)程都有一個(gè)boolean的中斷狀態(tài)(不一定就是對(duì)象的屬性,事實(shí)上,該狀態(tài)也確實(shí)不是Thread的字段),interrupt方法僅僅只是將該狀態(tài)置為true。對(duì)于非阻塞中的線(xiàn)程, 只是改變了中斷狀態(tài), 即Thread.isInterrupted()將返回true,并不會(huì)使程序停止;
public void run(){//線(xiàn)程t1
while(true){
Thread.yield();
}
}
t1.interrupt();
這樣使線(xiàn)程t1中斷,是不會(huì)有效果的,只是更改了中斷狀態(tài)位。
如果希望非常優(yōu)雅地終止這個(gè)線(xiàn)程,就該這樣做
public void run(){
while(true)
{
if(Thread.currentThread().isInterrupted())
{
System.out.println("Interruted!");
break;
}
Thread.yield();
}
}
使用中斷,就對(duì)數(shù)據(jù)一致性有了一定的保證。
對(duì)于可取消的阻塞狀態(tài)中的線(xiàn)程, 比如等待在這些函數(shù)上的線(xiàn)程, Thread.sleep(), Object.wait(), Thread.join(), 這個(gè)線(xiàn)程收到中斷信號(hào)后, 會(huì)拋出InterruptedException, 同時(shí)會(huì)把中斷狀態(tài)置回為false.
對(duì)于取消阻塞狀態(tài)中的線(xiàn)程,可以這樣抒寫(xiě)代碼:
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 線(xiàn)程掛起
掛起(suspend)和繼續(xù)執(zhí)行(resume)線(xiàn)程
suspend()不會(huì)釋放鎖
如果加鎖發(fā)生在resume()之前 ,則死鎖發(fā)生
這兩個(gè)方法都是Deprecated方法,不推薦使用。
原因在于,suspend不釋放鎖,因此沒(méi)有線(xiàn)程可以訪(fǎng)問(wèn)被它鎖住的臨界區(qū)資源,直到被其他線(xiàn)程resume。因?yàn)闊o(wú)法控制線(xiàn)程運(yùn)行的先后順序,如果其他線(xiàn)程的resume方法先被運(yùn)行,那則后運(yùn)行的suspend,將一直占有這把鎖,造成死鎖發(fā)生。
用以下代碼來(lái)模擬這個(gè)場(chǎng)景
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í)爭(zhēng)奪一把鎖,爭(zhēng)奪到的線(xiàn)程suspend,然后再resume,按理來(lái)說(shuō),應(yīng)該某個(gè)線(xiàn)程爭(zhēng)奪后被resume釋放了鎖,然后另一個(gè)線(xiàn)程爭(zhēng)奪掉鎖,再被resume。
結(jié)果輸出是:
in t1
in t2
說(shuō)明兩個(gè)線(xiàn)程都爭(zhēng)奪到了鎖,但是控制臺(tái)的紅燈還是亮著的,說(shuō)明t1,t2一定有線(xiàn)程沒(méi)有執(zhí)行完。我們dump出堆來(lái)看看

發(fā)現(xiàn)t2一直被suspend。這樣就造成了死鎖。
2.6 join和yeild
yeild是個(gè)native靜態(tài)方法,這個(gè)方法是想把自己占有的cpu時(shí)間釋放掉,然后和其他線(xiàn)程一起競(jìng)爭(zhēng)(注意yeild的線(xiàn)程還是有可能爭(zhēng)奪到cpu,注意與sleep區(qū)別)。在javadoc中也說(shuō)明了,yeild是個(gè)基本不會(huì)用到的方法,一般在debug和test中使用。
join方法的意思是等待其他線(xiàn)程結(jié)束,就如suspend那節(jié)的代碼,想讓主線(xiàn)程等待t1,t2結(jié)束以后再結(jié)束。沒(méi)有結(jié)束的話(huà),主線(xiàn)程就一直阻塞在那里。
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去掉,則主線(xiàn)程會(huì)直接運(yùn)行結(jié)束,i的值會(huì)很小。如果有join,打印出的i的值一定是10000000。
那么join是怎么實(shí)現(xiàn)的呢?
join的本質(zhì)
while(isAlive())
{
wait(0);
}
join()方法也可以傳遞一個(gè)時(shí)間,意為有限期地等待,超過(guò)了這個(gè)時(shí)間就自動(dòng)喚醒。
這樣就有一個(gè)問(wèn)題,誰(shuí)來(lái)notify這個(gè)線(xiàn)程呢,在thread類(lèi)中沒(méi)有地方調(diào)用了notify?
在javadoc中,找到了相關(guān)解釋。當(dāng)一個(gè)線(xiàn)程運(yùn)行完成終止后,將會(huì)調(diào)用notifyAll方法去喚醒等待在當(dāng)前線(xiàn)程實(shí)例上的所有線(xiàn)程,這個(gè)操作是jvm自己完成的。
所以javadoc中還給了我們一個(gè)建議,不要使用wait和notify/notifyall在線(xiàn)程實(shí)例上。因?yàn)閖vm會(huì)自己調(diào)用,有可能與你調(diào)用期望的結(jié)果不同。
3. 守護(hù)線(xiàn)程
在后臺(tái)默默地完成一些系統(tǒng)性的服務(wù),比如垃圾回收線(xiàn)程、JIT線(xiàn)程就可以理解為守護(hù)線(xiàn)程。
當(dāng)一個(gè)Java應(yīng)用內(nèi),所有非守護(hù)進(jìn)程都結(jié)束時(shí),Java虛擬機(jī)就會(huì)自然退出。
此前有寫(xiě)過(guò)一篇python中如何實(shí)現(xiàn),查看這里。
而Java中變成守護(hù)進(jìn)程就相對(duì)簡(jiǎn)單了。
Thread t=new DaemonT();
t.setDaemon(true);
t.start();
這樣就開(kāi)啟了一個(gè)守護(hù)線(xiàn)程。
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)線(xiàn)程dt不是一個(gè)守護(hù)線(xiàn)程時(shí),在運(yùn)行后,我們能看到控制臺(tái)輸出hi
當(dāng)在start之前加入
dt.setDaemon(true);
控制臺(tái)就直接退出了,并沒(méi)有輸出。
4. 線(xiàn)程優(yōu)先級(jí)
Thread類(lèi)中有3個(gè)變量定義了線(xiàn)程優(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í)的線(xiàn)程和低優(yōu)先級(jí)的線(xiàn)程同時(shí)爭(zhēng)奪一個(gè)鎖,看看哪個(gè)最先完成。
當(dāng)然并不一定是高優(yōu)先級(jí)一定先完成。再多次運(yùn)行后發(fā)現(xiàn),高優(yōu)先級(jí)完成的概率比較大,但是低優(yōu)先級(jí)還是有可能先完成的。
5. 基本的線(xiàn)程同步操作
synchronized 和 Object.wait() Obejct.notify()
這一節(jié)內(nèi)容詳情請(qǐng)看以前寫(xiě)的一篇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)前類(lèi)加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類(lèi)的鎖。
作用于實(shí)例方法,則不要new兩個(gè)不同的實(shí)例
作用于靜態(tài)方法,只要類(lèi)一樣就可以了,因?yàn)榧拥逆i是類(lèi).class,可以new兩個(gè)不同實(shí)例。
wait和notify的用法:
用什么鎖住,就用什么調(diào)用wait和notify
本文就不細(xì)說(shuō)了。
相關(guān)文章
Java中token的存儲(chǔ)和獲取實(shí)例代碼
關(guān)于java獲取微信Token驗(yàn)證的問(wèn)題相信很多人都遇見(jiàn)過(guò),尤其是對(duì)剛接觸微信開(kāi)發(fā)的人來(lái)說(shuō)確實(shí)有點(diǎn)棘手,下面這篇文章主要給大家介紹了關(guān)于Java中token存儲(chǔ)和獲取的相關(guān)資料,需要的朋友可以參考下2022-08-08
分布式調(diào)度器之Spring Task 的使用詳解
SpringTask是Spring框架中用于任務(wù)調(diào)度的組件,通過(guò)簡(jiǎn)單的注解就能實(shí)現(xiàn)定時(shí)任務(wù)的創(chuàng)建和調(diào)度,可以通過(guò)配置線(xiàn)程池來(lái)實(shí)現(xiàn),本文給大家介紹分布式調(diào)度器之Spring Task 的使用,感興趣的朋友跟隨小編一起看看吧2024-10-10
SpringBoot MongoDB 索引沖突分析及解決方法
這篇文章主要介紹了SpringBoot MongoDB 索引沖突分析及解決方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11
淺談為什么重寫(xiě)equals()就要重寫(xiě)hashCode()
困擾我很久的問(wèn)題,一直不明白為什么重寫(xiě)equals()方法的時(shí)候要重寫(xiě)hashCode()方法,這次總算弄明白了,作此分享,感興趣的可以了解一下2021-10-10
springboot-mongodb的多數(shù)據(jù)源配置的方法步驟
這篇文章主要介紹了springboot-mongodb的多數(shù)據(jù)源配置的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

