首先理解三個(gè)概念。程序是一段靜態(tài)的代碼,應(yīng)用執(zhí)行的藍(lán)本抢呆。進(jìn)程是針對(duì)操作系統(tǒng)而言的一個(gè)概念,一個(gè)系統(tǒng)可以有多個(gè)進(jìn)程脏答。線(xiàn)程是相對(duì)于進(jìn)程而言的一個(gè)概念,一個(gè)進(jìn)程可以有多個(gè)線(xiàn)程亩鬼,每個(gè)線(xiàn)程都有自己獨(dú)立的執(zhí)行流程殖告,放到內(nèi)存中分析,就是每個(gè)線(xiàn)程都有一個(gè)方法調(diào)用棧雳锋。同一個(gè)進(jìn)程內(nèi)的各個(gè)線(xiàn)程相互獨(dú)立黄绩,但是會(huì)互相爭(zhēng)奪資源,比如內(nèi)存對(duì)象玷过,文件或者是數(shù)據(jù)庫(kù)中的記錄和表爽丹。共享資源必然就會(huì)產(chǎn)生爭(zhēng)用資源的問(wèn)題,所以多線(xiàn)程問(wèn)題冶匹,最需要解決的就是安全性(同一張票只能賣(mài)給一個(gè)人)和并發(fā)性(同時(shí)給多個(gè)人提供服務(wù)习劫,但每個(gè)人都感覺(jué)系統(tǒng)在只為它提供服務(wù))的問(wèn)題了咆瘟。
Java 號(hào)稱(chēng)一次編寫(xiě)到處運(yùn)行嚼隘?
對(duì)于線(xiàn)程管理, JVM 有自己的優(yōu)先級(jí)別和調(diào)度機(jī)制袒餐,但是因?yàn)?JVM 本質(zhì)上也只是操作系統(tǒng)中的一個(gè)應(yīng)用進(jìn)程飞蛹,它也是受限于操作系統(tǒng)谤狡。畢竟操作系統(tǒng)才是老大嘛,是它主管的硬件資源卧檐,所以這里存在兩種不同的線(xiàn)程管理規(guī)定墓懂,導(dǎo)致同樣的程序在不同的操作系統(tǒng)和硬件中有不同的執(zhí)行情況(Java 會(huì)采用隨機(jī)調(diào)度而操作系統(tǒng)會(huì)采用先來(lái)先服務(wù)、時(shí)間片輪轉(zhuǎn)的調(diào)度算法)霉囚。一次編寫(xiě)捕仔、到處運(yùn)行在線(xiàn)程這塊成了被打臉了。怎么解決呢盈罐?加鎖什么的榜跌,明天會(huì)講
Java 線(xiàn)程的優(yōu)先級(jí)別
java 的優(yōu)先級(jí)別有 [1,10] 十種盅粪,但是通常只會(huì)用到 1 钓葫、5 和 10 這三個(gè)級(jí)別。這一點(diǎn)可以從 java.lang.Thread 類(lèi)的僅有的三個(gè)靜態(tài)字段中看出來(lái)票顾。主線(xiàn)程默認(rèn)優(yōu)先級(jí)是 5 級(jí)础浮,它創(chuàng)建的線(xiàn)程優(yōu)先級(jí)默認(rèn)也是 5 級(jí),這從字段的命名中就可以看出來(lái)了奠骄,默認(rèn)就是 5 級(jí)嘛豆同。源碼如下:
這里有一個(gè) final 修飾的變量,只能叫做單值變量含鳞,而不能稱(chēng)作常量诱告,常量位于方法區(qū)的常量數(shù)據(jù)區(qū),而這里明顯就位于靜態(tài)數(shù)據(jù)區(qū)民晒,如果未添加 static 關(guān)鍵字精居,那它應(yīng)該是在堆區(qū)內(nèi)的對(duì)應(yīng)對(duì)象內(nèi)存空間中。
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
關(guān)于 Thread 類(lèi)
線(xiàn)程的默認(rèn)命名方式為"Thread-"+線(xiàn)程序號(hào)
潜必,線(xiàn)程序號(hào)從 0 開(kāi)始靴姿,所以命名為Thread-0 , Thread-1
等磁滚,也可以通過(guò)public static Thread currentThread()
方法得到當(dāng)前線(xiàn)程對(duì)象的引用佛吓,再調(diào)用public final void setName(String name)
方法指定線(xiàn)程名字。
Thread 類(lèi)的常見(jiàn)方法
其中常用的睡眠方法也是一個(gè)靜態(tài)方法public static void sleep(long millis)throws InterruptedException
垂攘。
Thread 類(lèi)還有一個(gè)讓步方法public static void yield()
维雇,會(huì)把當(dāng)前執(zhí)行機(jī)會(huì)讓給同優(yōu)先級(jí)的其他線(xiàn)程執(zhí)行。
Thread 類(lèi)有一個(gè) final 修飾的方法public final boolean isAlive()
晒他,用來(lái)判斷該線(xiàn)程是否還活著吱型。
public void interrupt()
,線(xiàn)程對(duì)象調(diào)用此方法陨仅,會(huì)立即出現(xiàn)中斷異常(InterruptedException)津滞。虛擬機(jī)強(qiáng)行使得此線(xiàn)程從阻塞態(tài)轉(zhuǎn)成運(yùn)行態(tài)铝侵,然后執(zhí)行 catch 代碼段。
主線(xiàn)程死亡后触徐,會(huì)立即強(qiáng)行喚醒處于阻塞狀態(tài)的正在睡眠的線(xiàn)程咪鲜,進(jìn)入到就緒態(tài),然后再轉(zhuǎn)成運(yùn)行態(tài)撞鹉,然后 JVM 會(huì)在 sleep() 方法中//產(chǎn)生一個(gè) 中斷異常 拋出來(lái)疟丙。這個(gè)中斷方法要配合 sleep() 方法使用,已經(jīng)得到驗(yàn)證鸟雏,如果沒(méi)有 sleep 方法隆敢,即使調(diào)用了 interrupt() 方法仍然沒(méi)有任何效果,這里本來(lái)就是利用會(huì)拋出這個(gè)異常//來(lái)進(jìn)行邏輯處理啊崔慧。
@Deprecated
public final void stop()
TODO:這兩個(gè)方法需要進(jìn)一步去整理
線(xiàn)程的生命周期
主線(xiàn)程的入口方法是 main() 方法拂蝎,主方法結(jié)束,主線(xiàn)程結(jié)束惶室。其他線(xiàn)程的入口方法就是 run() 方法温自,run() 方法結(jié)束,該線(xiàn)程就結(jié)束了皇钞。有了多線(xiàn)程后悼泌,主線(xiàn)程結(jié)束,其他線(xiàn)程仍然會(huì)繼續(xù)執(zhí)行夹界,直到所有線(xiàn)程結(jié)束馆里,程序才結(jié)束。main() 方法是 JVM 調(diào)用的可柿,run() 方法當(dāng)然也是由 JVM 調(diào)用的鸠踪。
如果將某個(gè)線(xiàn)程采用public final void setDaemon(boolean on)
方法設(shè)置為后臺(tái)線(xiàn)程的話(huà),當(dāng)所有前臺(tái)線(xiàn)程都結(jié)束了复斥,那么后臺(tái)線(xiàn)程無(wú)論是否執(zhí)行完都立即會(huì)結(jié)束营密。當(dāng)然如果這個(gè)時(shí)候,輪到它的時(shí)間片還在運(yùn)轉(zhuǎn)的話(huà)目锭,這個(gè)時(shí)間片內(nèi)還是會(huì)繼續(xù)執(zhí)行的评汰。
線(xiàn)程的生命周期有五個(gè)狀態(tài):
使用 new 新建線(xiàn)程對(duì)象的時(shí)候,線(xiàn)程處于新建狀態(tài)痢虹,僅僅在堆區(qū)開(kāi)辟了一個(gè)線(xiàn)程對(duì)象空間而已被去,線(xiàn)程對(duì)象和其他對(duì)象在內(nèi)存中沒(méi)有任何區(qū)別啊。
線(xiàn)程對(duì)象調(diào)用 .start() 方法的時(shí)候奖唯,就處于就緒狀態(tài)惨缆,會(huì)分配好所有資源.。也就是創(chuàng)建對(duì)應(yīng)的方法調(diào)用棧和分配程序計(jì)數(shù)器。
再由 JVM 調(diào)用的時(shí)候踪央,線(xiàn)程就處于運(yùn)行狀態(tài),會(huì)占用處理機(jī)瓢阴。
當(dāng)調(diào)用了睡眠方法或者正在等待 IO 操作就會(huì)進(jìn)入阻塞狀態(tài)畅蹂。
JVM 也是有能力強(qiáng)行把運(yùn)行狀態(tài)的線(xiàn)程直接轉(zhuǎn)為就緒狀態(tài)的,比如時(shí)間片到了荣恐。當(dāng)線(xiàn)程結(jié)束后液斜,會(huì)釋放掉所有資源,只等著 GC 進(jìn)行垃圾回收叠穆。
創(chuàng)建線(xiàn)程的方式
方式一:首先定義一個(gè)類(lèi)繼承 Thread 類(lèi)少漆,重寫(xiě) run() 方法。然后在主方法或者其他方法中調(diào)用線(xiàn)程類(lèi)對(duì)象的 start() 方法硼被,讓線(xiàn)程處于就緒狀態(tài)示损,分配好所需資源。
方式二:定義一個(gè)類(lèi)實(shí)現(xiàn) java.lang.Runnable 接口嚷硫,并實(shí)現(xiàn) run() 方法检访。將這類(lèi)對(duì)象作為參數(shù)傳遞給 Thread 類(lèi)的構(gòu)造函數(shù),Thread 類(lèi) new 出來(lái)的才是線(xiàn)程對(duì)象仔掸,實(shí)現(xiàn)了 java.lang.Runnable 接口的類(lèi)對(duì)象并不是線(xiàn)程脆贵,它只是給線(xiàn)程提供了一個(gè)入口方法。
方式三:通過(guò)匿名內(nèi)部類(lèi)的方式起暮,使用 Thread 類(lèi)的構(gòu)造方法public Thread(Runnable target)
傳遞一個(gè)匿名內(nèi)部類(lèi)對(duì)象卖氨。
線(xiàn)程同步鎖
一個(gè)進(jìn)程中有多個(gè)線(xiàn)程,這多個(gè)線(xiàn)程之間是互相共享進(jìn)程的資源的负懦,確切點(diǎn)的說(shuō)就是共享堆區(qū)的對(duì)象筒捺。所以,這里的共享資源問(wèn)題引出來(lái)了安全性的問(wèn)題纸厉,為了保證安全性就不得不添加 synchronized 同步鎖焙矛,以保證原子操作能夠正確執(zhí)行,不至于因?yàn)榫€(xiàn)程調(diào)度的不確定性而受到干擾残腌。
同步鎖可以分為對(duì)象鎖村斟、方法鎖和類(lèi)鎖。
對(duì)象鎖
Object obj = new Object();
synchronized(obj){
//synchronized 代碼塊內(nèi)部的代碼就一定會(huì)是以個(gè)原子操作
}
如上述代碼所示:首先明確一點(diǎn)抛猫,java 中每一個(gè)對(duì)象都有一個(gè)鎖蟆盹。JVM 在遇到 sychronized 關(guān)鍵字的時(shí)候,首先會(huì)檢查該對(duì)象有沒(méi)有被鎖住闺金。如果沒(méi)上鎖逾滥,會(huì)先給對(duì)象上鎖,再來(lái)執(zhí)行 sychronized 代碼塊的內(nèi)容,執(zhí)行完后寨昙,再來(lái)釋放鎖讥巡。如果對(duì)象鎖未被釋放期間,其他線(xiàn)程想要執(zhí)行 sychronized 代碼快的內(nèi)容舔哪,是不被允許的欢顷。只能在鎖池中等待,該線(xiàn)程就從運(yùn)行狀態(tài)捉蚤,轉(zhuǎn)為阻塞狀態(tài)抬驴。
方法鎖
public synchronized void sell(){
//對(duì)應(yīng)的原子操作
}
這里講的是實(shí)例方法鎖,實(shí)例方法鎖其實(shí)本質(zhì)上就是對(duì)象鎖缆巧,它鎖的是調(diào)用這個(gè)方法的對(duì)象布持,也就是 this 。更確切的說(shuō)陕悬,一個(gè)對(duì)象的任意一個(gè)方法被鎖住了的情況下题暖,也就是這個(gè)對(duì)象被鎖住了,那么這個(gè)類(lèi)的其他方法也就無(wú)法被調(diào)用了捉超。
類(lèi)鎖
也就是靜態(tài)方法加鎖就是類(lèi)鎖芙委,鎖的是這個(gè)類(lèi)的反射對(duì)象。這和實(shí)例方法同理狂秦,也就是說(shuō)灌侣,同一時(shí)刻只能有一個(gè)靜態(tài)方法被執(zhí)行。
線(xiàn)程之間的協(xié)作
因?yàn)橛脩?hù)想要同時(shí)使用多款應(yīng)用程序裂问,所以操作系統(tǒng)需要支持多進(jìn)程侧啼。而且在使用單個(gè)應(yīng)用程序的時(shí)候,還想要同時(shí)做多件事情堪簿,這就有了線(xiàn)程的產(chǎn)生痊乾。線(xiàn)程之間有時(shí)可能需要共享資源,因?yàn)榫€(xiàn)程調(diào)度的不確定性椭更,所以需要使用 sychronized 加同步鎖來(lái)保證各個(gè)線(xiàn)程彼此的獨(dú)立性哪审。線(xiàn)程有時(shí)候還需要互相協(xié)作,以完成某件事情虑瀑。線(xiàn)程之間協(xié)作的最典型的一個(gè)應(yīng)用實(shí)例就是生產(chǎn)者和消費(fèi)者問(wèn)題了湿滓。
首先要弄明白以下內(nèi)容:Java 中每一個(gè)內(nèi)存對(duì)象都有一個(gè)專(zhuān)門(mén)用來(lái)存放線(xiàn)程對(duì)象的鎖池和等待池。java.lang.Object 類(lèi)中的public final void wait() throws InterruptedException
方法和public final void notify()
方法都是專(zhuān)門(mén)針對(duì)線(xiàn)程的協(xié)作而設(shè)計(jì)的方法舌狗。
但是為什么它會(huì)定義在 Object 類(lèi)中去呢叽奥?按道理應(yīng)該設(shè)計(jì)在 Thread 類(lèi)中的啊痛侍!這樣設(shè)計(jì)的目的就是可以在任何添加了 sychronized 同步鎖的代碼塊中調(diào)用任意對(duì)象的 wait() 和 notify() 方法朝氓,而不用考慮這個(gè)對(duì)象是否是繼承了 Thread 類(lèi)或者實(shí)現(xiàn)了 Runnable 接口。而且這兩個(gè)方法必須在添加了同步鎖的代碼塊中使用,如果沒(méi)有赵哲,編譯階段會(huì)通過(guò)待德,但是在運(yùn)行階段會(huì)拋出 IllegalMonitorStateException 異常。為什么要設(shè)計(jì)這樣報(bào)出異常呢枫夺?因?yàn)橹挥性谕芥i代碼塊中才有存在的意義啊将宪,所以 JVM 就干脆給一個(gè)異常錯(cuò)誤。
當(dāng)對(duì)象 t1 調(diào)用 wait() 方法時(shí)筷屡,其所在的線(xiàn)程對(duì)象涧偷,會(huì)進(jìn)入這個(gè)對(duì)象 t1 的等待池中簸喂,此時(shí)處于阻塞狀態(tài)毙死,同時(shí)會(huì)釋放掉對(duì)象 t1 的鎖。此時(shí)這個(gè)線(xiàn)程對(duì)象在對(duì)象 t1 的等待池中只能等待其它線(xiàn)程調(diào)用該對(duì)象 t1 的 notify() 或者 notifyAll() 方法才能夠使得它從等待池中釋放喻鳄。對(duì)象的鎖池能夠自動(dòng)開(kāi)啟扼倘,等待池的線(xiàn)程對(duì)象只能被其他的線(xiàn)程調(diào)用 notify() 方法,才能夠開(kāi)啟鎖池除呵。
其中 Object 類(lèi)的 wait() 方法有三個(gè)重載的方法public final void wait() throws InterruptedException
再菊,表示等待無(wú)限長(zhǎng)時(shí)間和public final void wait(long timeout) throws InterruptedException
,表示等待指定長(zhǎng)時(shí)間颜曾,以及public final void wait(long timeout, int nanos) throws InterruptedException
等待指定長(zhǎng)時(shí)間纠拔。
其中 Object 類(lèi)還有兩個(gè)重載方法public final void notify()
和public final void notifyAll()
。第一個(gè)方法只能釋放對(duì)象 t1 等待池中的隨機(jī)的一個(gè)線(xiàn)程對(duì)象泛豪,第二個(gè)方法則可以釋放掉所有的等待池對(duì)象稠诲。它們?nèi)紡膶?duì)象 t1 的等待池中進(jìn)入到對(duì)象 t1 的鎖池中去,還會(huì)自動(dòng)給對(duì)象 t1 加鎖诡曙。
public class Test1 {
public static void main(String[] args) {
Tree q = new Tree();
Producer p = new Producer(q);
Consumer c = new Consumer(q);
p.start();
c.start();
}
}
class Producer extends Thread {
Tree q;
Producer(Tree q) {
this.q = q;
}
public void run() {
for (int i = 0; i < 10; i++) {
q.put(i);
System.out.println("Producer put " + i);
}
}
}
class Consumer extends Thread {
Tree q;
Consumer(Tree q) {
this.q = q;
}
public void run() {
while (true) {
System.out.println("Consumer get " + q.get());
}
}
}
/* 樹(shù)類(lèi) **/
class Tree {
int hole;// 樹(shù)洞
boolean bFull = false;
// 放情報(bào)
public synchronized void put(int i) {
if (!this.bFull)// 如果為空
{
this.hole = i;// 把情報(bào)放入樹(shù)洞
this.bFull = true;// 設(shè)為非空
/*
* 從該對(duì)象的等待隊(duì)列中釋放消費(fèi)者線(xiàn)程進(jìn)入該對(duì)象鎖池 使該線(xiàn)程將再次成為可運(yùn)行的線(xiàn)程
*/
this.notify();
}
try {// 生產(chǎn)者線(xiàn)程進(jìn)入this對(duì)象的等待隊(duì)列開(kāi)啟鎖池
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// 取情報(bào)
public synchronized int get() {
if (!this.bFull)// 如果為空
{
try {
this.wait();// 消費(fèi)者線(xiàn)程進(jìn)入this對(duì)象的等待隊(duì)列開(kāi)啟鎖池
} catch (Exception e) {
e.printStackTrace();
}
}
bFull = false;// 設(shè)置為空
/*
* 從該對(duì)象的等待隊(duì)列中釋放生產(chǎn)者線(xiàn)程進(jìn)入該對(duì)象鎖池 使該線(xiàn)程將再次成為可運(yùn)行的線(xiàn)程
*/
this.notify();
int value = hole;
return value;
}
}
以上生產(chǎn)者消費(fèi)者的案例中臀叙,分析過(guò)程如下:
內(nèi)存圖如下:
假設(shè)先執(zhí)行的是消費(fèi)者線(xiàn)程,消費(fèi)者線(xiàn)程執(zhí)行對(duì)象 q 的 get() 方法時(shí)价卤,先判斷對(duì)象 q 是否加鎖了劝萤,加鎖就進(jìn)入鎖池中等待,如果是未加鎖就加鎖慎璧,然后判斷樹(shù)洞是否為空床嫌,空則進(jìn)入到等待池中等待,釋放對(duì)象鎖胸私。如果不空則取出樹(shù)洞中的內(nèi)容既鞠,并釋放對(duì)象等待池中的線(xiàn)程對(duì)象。
消費(fèi)者線(xiàn)程調(diào)用 get() 方法盖文,加對(duì)象鎖嘱蛋,判斷樹(shù)洞為空,調(diào)用 wait() 方法進(jìn)入等待池中,釋放對(duì)象鎖洒敏。
生產(chǎn)者調(diào)用 put() 方法龄恋,加對(duì)象鎖,判斷樹(shù)洞為空凶伙,就將情報(bào)放入樹(shù)洞中郭毕,再調(diào)用 notify() 方法釋放處于等待池中的線(xiàn)程對(duì)象,隨即進(jìn)入到對(duì)象鎖池中函荣,然后生產(chǎn)者線(xiàn)程再次調(diào)用 wait() 方法显押,進(jìn)入到等待池中,釋放對(duì)象鎖傻挂。
消費(fèi)者線(xiàn)程對(duì)象此時(shí)處于運(yùn)行狀態(tài)乘碑,開(kāi)始加對(duì)象鎖,拿到樹(shù)洞中的情報(bào)金拒,后調(diào)用 notify() 方法從等待池中釋放生產(chǎn)者線(xiàn)程對(duì)象兽肤,生產(chǎn)者對(duì)象進(jìn)入鎖池中,返回拿到的情報(bào)绪抛,對(duì)象鎖自動(dòng)開(kāi)啟资铡。