水中月,淺析Java的線(xiàn)程調(diào)度策略

作者:楊興強(qiáng)
原文來(lái)源:開(kāi)點(diǎn)工作室(ID:kaidiancs)

一.從一個(gè)例子開(kāi)始

看著Java線(xiàn)程執(zhí)行起來(lái)的那種任性和隨意,我們不免會(huì)問(wèn):是誰(shuí)在主導(dǎo)Java線(xiàn)程的執(zhí)行咏闪?它按照什么樣的策略來(lái)調(diào)度Java線(xiàn)程阁危?本文將帶著這樣的問(wèn)題,探討Java線(xiàn)程的調(diào)度機(jī)制。

程序的問(wèn)題還是先從代碼說(shuō)起吧,下面是一個(gè)廣泛引用的例子:
假設(shè)某航班有100張余票昌屉,現(xiàn)有10個(gè)窗口(線(xiàn)程)同時(shí)賣(mài)這100張票丸凭。下面程序?qū)崿F(xiàn)了10個(gè)線(xiàn)程并發(fā)執(zhí)行的過(guò)程福扬。
// sched 類(lèi)
// 創(chuàng)建多線(xiàn)程模擬多窗口售票腕铸,演示java線(xiàn)程的并發(fā)運(yùn)行狀況,說(shuō)明java和操作系統(tǒng)的線(xiàn)程調(diào)度策略

class sched implements Runnable {
finalintTICKET_MAXNUM = 500;
// 總票數(shù)
finalintTHREAD_NUM = 10;
// 線(xiàn)程(售票窗口)數(shù)量
publicintcount = 0;
// 已售出票數(shù)
privateintThreadNo[];
// 第i張票的線(xiàn)程的序號(hào)是TicketThreadNo[i]铛碑,-1表示未售出
privateintTicketNum[];
// 記錄每個(gè)線(xiàn)程售出的票數(shù),說(shuō)明每個(gè)線(xiàn)程占用的CPU時(shí)間
privateintThreadId[];
// ThreadId[i]存放第i個(gè)線(xiàn)程的Id號(hào)
privateintNewThreadNo;
// 將要?jiǎng)?chuàng)建的線(xiàn)程的序號(hào)狠裹,初值為0
double d;
// 工作變量,僅用于消耗線(xiàn)程的CPU時(shí)間

 public sched() {
      inti;
      ThreadNo = newint[TICKET_MAXNUM];
      for (i = 0; i < TICKET_MAXNUM; i++)
           ThreadNo[i] = -1;
      TicketNum = newint[THREAD_NUM];
      for (i = 0; i < THREAD_NUM; i++)
           TicketNum[i] = 0;
      NewThreadNo = 0;
      ThreadId = newint[THREAD_NUM];
 }

// sell()方法描述一次售票操作,synchronized 用于實(shí)現(xiàn)對(duì)共享變量count的互斥
 publicsynchronizedvoid sell() {
      if (count < TICKET_MAXNUM) {
           ThreadNo[count] = getNo((int)Thread.currentThread().getId());

//System.out.println(sold[count]+ "號(hào)線(xiàn)程售第" + count + "張票");
count++;
//delay();
}
}
// 從線(xiàn)程的id號(hào)得到線(xiàn)程的序號(hào)
privateint getNo(intid) {
int i;
for (i=0; i<THREAD_NUM; i++)
if (ThreadId[i] == id)
returni;
return -1;
}

 publicvoid run() {

//Thread.currentThread().setPriority(NewThreadNo+1);
ThreadId[NewThreadNo++] = (int)Thread.currentThread().getId();
while (count < TICKET_MAXNUM) // 只要有票汽烦,線(xiàn)程(窗口)就不停售票
sell();
}
// 僅用于消耗CPU時(shí)間涛菠,表示售票所用時(shí)間
privatevoid delay(){
d = 5000000;
while (d > 0)
d = d - 1;
}
// 累計(jì)并打印每個(gè)線(xiàn)程賣(mài)的票數(shù)
publicvoid accumulate() {
int i;
for (i=0; i<TICKET_MAXNUM; i++)
TicketNum[ThreadNo[i]]++;
for (i=0; i<THREAD_NUM; i++)
System.out.printf("%3d號(hào)線(xiàn)程賣(mài):%-4d張票\n", i, TicketNum[i]);
}
}
// 主程序
publicclass jsched {
publicstaticvoid main(String[] args) {
inti;
sched t = new sched();
for (i = 0; i < t.THREAD_NUM; i++) {
new Thread(t).start();
}
while (t.count < t.TICKET_MAXNUM) //等待票都賣(mài)完
Thread.yield();
t.accumulate();
}
}
上面例子中,主線(xiàn)程依次創(chuàng)建并啟動(dòng)10個(gè)線(xiàn)程,他們執(zhí)行相同的程序run(),其作用就是不停地調(diào)用sell()以售票撇吞。NewThreadNo表示新創(chuàng)建線(xiàn)程的序號(hào)俗冻,初值為0,每創(chuàng)建一個(gè)新線(xiàn)程牍颈,加1迄薄。售票時(shí)記錄每張票都是由哪個(gè)線(xiàn)程售出的,線(xiàn)程序號(hào)存于ThreadNo[]中煮岁。線(xiàn)程Id是每個(gè)線(xiàn)程在Java中的唯一標(biāo)識(shí)讥蔽,可以通過(guò)調(diào)用Thread.currentThread().getId()獲得當(dāng)前線(xiàn)程的Id,并由此知道正在執(zhí)行當(dāng)前程序的線(xiàn)程是哪一個(gè)線(xiàn)程画机。序號(hào)為i的線(xiàn)程的Id存在ThreadId[i]中冶伞,該數(shù)組記錄了線(xiàn)程序號(hào)和Id號(hào)之間的對(duì)應(yīng)關(guān)系。主線(xiàn)程最后統(tǒng)計(jì)各線(xiàn)程分別賣(mài)了多少?gòu)埰薄?/p>

在并發(fā)環(huán)境下步氏,線(xiàn)程之間的運(yùn)行次序總是呈現(xiàn)某種隨機(jī)性响禽,程序的運(yùn)行結(jié)果往往是不固定的。所以我們觀察程序運(yùn)行結(jié)果時(shí)荚醒,需要多次運(yùn)行芋类,才能總結(jié)出大概的運(yùn)行規(guī)律。當(dāng)然有時(shí)也許根本就沒(méi)有規(guī)律腌且。

多次運(yùn)行以上程序梗肝,較典型的結(jié)果如下:
0號(hào)線(xiàn)程賣(mài):100 張票
1號(hào)線(xiàn)程賣(mài):0 張票
2號(hào)線(xiàn)程賣(mài):0 張票
3號(hào)線(xiàn)程賣(mài):0 張票
4號(hào)線(xiàn)程賣(mài):0 張票
5號(hào)線(xiàn)程賣(mài):0 張票
6號(hào)線(xiàn)程賣(mài):0 張票
7號(hào)線(xiàn)程賣(mài):0 張票
8號(hào)線(xiàn)程賣(mài):0 張票
9號(hào)線(xiàn)程賣(mài):0 張票

票都被線(xiàn)程0賣(mài)了,其他線(xiàn)程好像沒(méi)干活铺董,系統(tǒng)采用的像是先來(lái)先服務(wù)的調(diào)度原則巫击。盡管10個(gè)線(xiàn)程是并發(fā)執(zhí)行的,但主線(xiàn)程是依次創(chuàng)建各個(gè)線(xiàn)程的精续。當(dāng)創(chuàng)建了線(xiàn)程0之后坝锰,主線(xiàn)程就和線(xiàn)程0并發(fā)執(zhí)行了。所以線(xiàn)程0一般情況下都先于其他線(xiàn)程執(zhí)行重付。但這不能說(shuō)明系統(tǒng)采用的是先來(lái)先服務(wù)策略顷级。由于賣(mài)票這活兒太簡(jiǎn)單,還沒(méi)等其他線(xiàn)程開(kāi)始執(zhí)行确垫,線(xiàn)程0就把票賣(mài)光了弓颈。這好辦帽芽,只要在sell()方法中加上延遲,把delay()函數(shù)調(diào)用前面的注釋去掉翔冀,即可人為增加賣(mài)票的工作量导街。修改后多次運(yùn)行程序,較典型的結(jié)果如下:

0號(hào)線(xiàn)程賣(mài):9 張票
1號(hào)線(xiàn)程賣(mài):8 張票
2號(hào)線(xiàn)程賣(mài):18 張票
3號(hào)線(xiàn)程賣(mài):17 張票
4號(hào)線(xiàn)程賣(mài):17 張票
5號(hào)線(xiàn)程賣(mài):7 張票
6號(hào)線(xiàn)程賣(mài):7 張票
7號(hào)線(xiàn)程賣(mài):5 張票
8號(hào)線(xiàn)程賣(mài):5 張票
9號(hào)線(xiàn)程賣(mài):7 張票

此時(shí)各線(xiàn)程都參與了賣(mài)票纤子,而且能看到他們交替執(zhí)行搬瑰。系統(tǒng)采用的又像是時(shí)間片輪轉(zhuǎn)的調(diào)度策略。

然而Java的Thread類(lèi)提供了設(shè)置線(xiàn)程優(yōu)先級(jí)的方法控硼,每個(gè)線(xiàn)程初始創(chuàng)建時(shí)會(huì)被賦予一個(gè)默認(rèn)的優(yōu)先級(jí)泽论。那么線(xiàn)程的優(yōu)先級(jí)又起到了什么作用呢?

現(xiàn)在我們?nèi)サ魊un()方法中對(duì)設(shè)置優(yōu)先級(jí)的方法setPriority()的注釋?zhuān)摲椒▽?0個(gè)線(xiàn)程的優(yōu)先級(jí)分別設(shè)為1至10卡乾,這是Java支持的10個(gè)不同級(jí)別的優(yōu)先級(jí)翼悴。多次運(yùn)行程序,每個(gè)線(xiàn)程賣(mài)出的票數(shù)都不一樣幔妨,較典型的運(yùn)行結(jié)果如下:
......
0號(hào)線(xiàn)程賣(mài):8 張票
1號(hào)線(xiàn)程賣(mài):2 張票
2號(hào)線(xiàn)程賣(mài):1 張票
3號(hào)線(xiàn)程賣(mài):10 張票
4號(hào)線(xiàn)程賣(mài):1 張票
5號(hào)線(xiàn)程賣(mài):0 張票
6號(hào)線(xiàn)程賣(mài):5 張票
7號(hào)線(xiàn)程賣(mài):73 張票
8號(hào)線(xiàn)程賣(mài):0 張票
9號(hào)線(xiàn)程賣(mài):0 張票

很明顯抄瓦,7號(hào)線(xiàn)程賣(mài)了最多的票,這是因?yàn)?號(hào)線(xiàn)程的優(yōu)先級(jí)較高陶冷。然而8、9號(hào)線(xiàn)程的優(yōu)先級(jí)更高毯辅,卻沒(méi)賣(mài)一張票埂伦。從運(yùn)行結(jié)果上看,優(yōu)先級(jí)起了一定的作用思恐,但并不絕對(duì)沾谜,有時(shí)甚至不被理會(huì)。

多次改變程序中線(xiàn)程的數(shù)量胀莹,票的數(shù)量基跑、賣(mài)票的延遲時(shí)間、線(xiàn)程的優(yōu)先級(jí)等各種參數(shù)描焰,我們?cè)噲D觀察線(xiàn)程的調(diào)度方法媳否,但仍然無(wú)法把握J(rèn)ava線(xiàn)程的調(diào)度策略。唯一可以確定的是:它像是采用了輪轉(zhuǎn)法荆秦,但不是完全的輪轉(zhuǎn)篱竭,因?yàn)榻?jīng)常有線(xiàn)程被輪空好幾次;它像是采用了優(yōu)先級(jí)的調(diào)度策略步绸,但又不是完全按優(yōu)先級(jí)的次序分配CPU掺逼,因?yàn)樽罡邇?yōu)先級(jí)的線(xiàn)程也常被忽略;它有時(shí)還像先來(lái)先服務(wù)的調(diào)度瓤介。

以上我們看到的Java線(xiàn)程的執(zhí)行過(guò)程僅僅是在Windows XP平臺(tái)上的運(yùn)行情況吕喘。事實(shí)上赘那,Java在不同的發(fā)展時(shí)期、不同的平臺(tái)上氯质,其線(xiàn)程調(diào)度的策略也是不同的募舟。要想概括Java的線(xiàn)程調(diào)度方法,不是一件容易的事病梢,還是讓我們沿著Java虛擬機(jī)實(shí)現(xiàn)的蹤跡來(lái)探尋和理解Java線(xiàn)程的調(diào)度方法吧胃珍。

二.早期的JVM線(xiàn)程調(diào)度策略

很多網(wǎng)站或課本上都是這樣介紹java線(xiàn)程的調(diào)度策略的[1]:

(1)JVM使用搶占的、基于優(yōu)先權(quán)的調(diào)度策略蜓陌;
(2)每個(gè)線(xiàn)程都有優(yōu)先級(jí)觅彰,JVM總是選擇最高優(yōu)先級(jí)的線(xiàn)程執(zhí)行;
(3)若果兩個(gè)線(xiàn)程具有相同的優(yōu)先級(jí)钮热,則采用FIFO的調(diào)度順序填抬。

在早期的java1.1中,JVM自己實(shí)現(xiàn)線(xiàn)程調(diào)度隧期,而不依賴(lài)于底層的平臺(tái)飒责。綠色線(xiàn)程(用戶(hù)級(jí)線(xiàn)程)[2]是JVM使用的唯一的線(xiàn)程模型(至少是在solaris平臺(tái)上),其線(xiàn)程調(diào)度采用的應(yīng)該就是上述這種調(diào)度策略仆潮。

綠色線(xiàn)程的執(zhí)行時(shí)間由線(xiàn)程本身來(lái)控制宏蛉,線(xiàn)程自身工作告一段落后,要主動(dòng)告知系統(tǒng)切換到另一個(gè)線(xiàn)程上性置。其特點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單拾并,不需要專(zhuān)門(mén)的硬件支持,切換操作對(duì)線(xiàn)程自身來(lái)說(shuō)是預(yù)先可知的鹏浅。

因?yàn)榫G色線(xiàn)程庫(kù)是用戶(hù)級(jí)的嗅义,并且Solaris一次只能處理一個(gè)綠色線(xiàn)程,即Java運(yùn)行時(shí)采用多對(duì)一的線(xiàn)程模型隐砸,所以會(huì)導(dǎo)致如下問(wèn)題:(1)Java應(yīng)用程序不能與Solaris環(huán)境中的多線(xiàn)程技術(shù)互操作之碗,就是說(shuō)Solaris管理不了Java線(xiàn)程;(2)Java線(xiàn)程不能在多處理機(jī)上并行執(zhí)行季希;(3)Java 應(yīng)用不能享用操作系統(tǒng)提供的并發(fā)性褪那。由于綠色線(xiàn)程的這些限制,在java1.2之后的版本中放棄了該模型式塌,而采用本地線(xiàn)程(Native threads武通,是指使用操作系統(tǒng)本地的線(xiàn)程庫(kù)建立和管理的線(xiàn)程),即將Java線(xiàn)程連接到本地線(xiàn)程上珊搀,主要由底層平臺(tái)實(shí)現(xiàn)線(xiàn)程的調(diào)度[3]冶忱。

三.依托底層平臺(tái)的Java線(xiàn)程調(diào)度策略

Java語(yǔ)言規(guī)范和Java虛擬機(jī)規(guī)范是Java的重要文檔,可惜的是他們都沒(méi)有說(shuō)明Java線(xiàn)程的調(diào)度問(wèn)題境析∏羟梗或許從Java的角度看派诬,線(xiàn)程并不是Java最基本的內(nèi)容。畢竟Thread類(lèi)也僅僅是Java一個(gè)特定的類(lèi)而已链沼。

終于在Java SE 8 API規(guī)范的Thread類(lèi)說(shuō)明中算是找到了線(xiàn)程調(diào)度的有關(guān)描述:每個(gè)線(xiàn)程有一個(gè)優(yōu)先級(jí)(從1級(jí)到10級(jí))默赂,較高優(yōu)先級(jí)的線(xiàn)程比低優(yōu)先級(jí)線(xiàn)程先執(zhí)行[4]。程序員可以通過(guò)Thread.setPriority(int)設(shè)置線(xiàn)程的優(yōu)先級(jí)括勺,默認(rèn)的優(yōu)先級(jí)是NORM_PRIORITY缆八。Java SE 還聲明JVM可以任何方式實(shí)現(xiàn)線(xiàn)程的優(yōu)先級(jí),甚至忽略它的存在疾捍。這可以看做Java SE提供給程序員的關(guān)于線(xiàn)程調(diào)度的策略奈辰,恐怕就這些了。為什么后來(lái)Java關(guān)于線(xiàn)程調(diào)度說(shuō)得這么少乱豆?是因?yàn)樗幌朐俟苓@事兒了奖恰,再說(shuō)就是多嘴。

我們是通過(guò)Java創(chuàng)建的線(xiàn)程宛裕,線(xiàn)程調(diào)度的事兒Java是脫不開(kāi)的瑟啃。那Java又是如何將線(xiàn)程調(diào)度交給底層的操作系統(tǒng)去做呢?下面我們將跟隨JVM虛擬機(jī)底層平臺(tái)上的實(shí)現(xiàn)揩尸,說(shuō)明Java線(xiàn)程的調(diào)度策略蛹屿。

  1. Solaris平臺(tái)上的JVM線(xiàn)程調(diào)度策略先說(shuō)Solaris本身的線(xiàn)程調(diào)度。在第9版之前岩榆,Solaris 采用M:N的線(xiàn)程模型蜡峰,即M個(gè)本地線(xiàn)程映射到N個(gè)內(nèi)核級(jí)線(xiàn)程(LWP,LWP和內(nèi)核級(jí)線(xiàn)程是一一對(duì)應(yīng)的)上朗恳。當(dāng)本地線(xiàn)程連接到一個(gè)LWP上時(shí),它才有機(jī)會(huì)獲得CPU使用權(quán)载绿。雖然Solaris提供了改變LWP的優(yōu)先權(quán)的系統(tǒng)調(diào)用粥诫,但是由于本地線(xiàn)程與LWP的連接是動(dòng)態(tài)的、不固定的崭庸。一個(gè)本地線(xiàn)程過(guò)一會(huì)兒可能會(huì)連接到另一個(gè)LWP上怀浆。因而Solaris沒(méi)有可靠的方法改變本地線(xiàn)程的優(yōu)先權(quán)。

再說(shuō)Java線(xiàn)程怕享。既然Java底層的運(yùn)行平臺(tái)提供了強(qiáng)大的線(xiàn)程管理能力执赡,Java就沒(méi)有理由再自己進(jìn)行線(xiàn)程的管理和調(diào)度了。于是JVM放棄了綠色線(xiàn)程的實(shí)現(xiàn)機(jī)制函筋,將每個(gè)Java線(xiàn)程一對(duì)一映射到Solaris平臺(tái)上的一個(gè)本地線(xiàn)程上沙合,并將線(xiàn)程調(diào)度交由本地線(xiàn)程的調(diào)度程序。由于Java線(xiàn)程是與本地線(xiàn)程是一對(duì)一地綁在一起的跌帐,所以改變Java線(xiàn)程的優(yōu)先權(quán)也不會(huì)有可靠地運(yùn)行結(jié)果首懈。

線(xiàn)程映射.jpg

盡管如此绊率,Solaris早期版本還是盡量實(shí)現(xiàn)了基本的用戶(hù)模式下的搶占。系統(tǒng)維護(hù)這一條戒律:就緒隊(duì)列上任何線(xiàn)程的優(yōu)先級(jí)必須小于等于正在運(yùn)行的線(xiàn)程究履,否則滤否,優(yōu)先級(jí)最低的正在運(yùn)行的線(xiàn)程將被剝奪運(yùn)行的機(jī)會(huì),即將其對(duì)應(yīng)的LWP讓給優(yōu)先級(jí)高的本地線(xiàn)程最仑。在如下三種情況下會(huì)發(fā)生線(xiàn)程的搶占:

(1)當(dāng)正在運(yùn)行的本地線(xiàn)程降低了其優(yōu)先級(jí)藐俺,使其小于就緒隊(duì)列中的某個(gè)線(xiàn)程的優(yōu)先級(jí)
(2)當(dāng)正在運(yùn)行的本地線(xiàn)程增加了就緒隊(duì)列中某個(gè)線(xiàn)程的優(yōu)先級(jí),使其高于正在運(yùn)行的線(xiàn)程的優(yōu)先級(jí)
(3)當(dāng)就緒隊(duì)列中新加入了一個(gè)優(yōu)先級(jí)高于正在運(yùn)行的線(xiàn)程的優(yōu)先級(jí)泥彤,例如欲芹,某個(gè)高優(yōu)先級(jí)的線(xiàn)程被喚醒。

Java線(xiàn)程的喚醒全景、優(yōu)先級(jí)設(shè)置是由JVM實(shí)現(xiàn)的耀石,但線(xiàn)程的調(diào)度(與LWP的連接)則是由本地線(xiàn)程庫(kù)完成。操作系統(tǒng)(Solaris)可以依據(jù)自己的原則改變LWP的優(yōu)先級(jí)爸黄,例如滞伟,通過(guò)動(dòng)態(tài)優(yōu)先級(jí)實(shí)現(xiàn)分時(shí),這是線(xiàn)程庫(kù)和JVM都無(wú)法干預(yù)的炕贵。

Solaris 9之后梆奈,使用了1:1的線(xiàn)程模型,即本地線(xiàn)程與LWP一對(duì)一地綁在一起称开,本地線(xiàn)程庫(kù)也失去了直接干預(yù)線(xiàn)程調(diào)度的機(jī)會(huì)(指為本地線(xiàn)程選擇連接LWP)亩钟。Java線(xiàn)程也就通過(guò)本地線(xiàn)程與LWP終生地一對(duì)一地綁在一起。這樣可以通過(guò)改變本地線(xiàn)程或Java線(xiàn)程的優(yōu)先級(jí)來(lái)影響LWP的優(yōu)先級(jí)鳖轰,從而影響系統(tǒng)的CPU調(diào)度清酥。但具體的CPU分配策略還是Solaris做出的,JVM僅起輔助的作用蕴侣。

  1. Windows平臺(tái)上的Java線(xiàn)程調(diào)度策略

在Windows下焰轻,Java線(xiàn)程一對(duì)一地綁定到Win32線(xiàn)程(相當(dāng)于Solaris的native線(xiàn)程)上。當(dāng)然Win32線(xiàn)程也是一對(duì)一地綁定到內(nèi)核級(jí)線(xiàn)程上昆雀,所以Java線(xiàn)程的調(diào)度實(shí)際上是內(nèi)核完成的辱志。Java虛擬機(jī)可以做的是通過(guò)將Java線(xiàn)程的優(yōu)先級(jí)映射到Win32線(xiàn)程的優(yōu)先級(jí)上,從而影響系統(tǒng)的線(xiàn)程調(diào)度決策狞膘。

Windows內(nèi)核使用了32級(jí)優(yōu)先權(quán)模式來(lái)決定線(xiàn)程的調(diào)度順序揩懒。優(yōu)先權(quán)被分為兩類(lèi):可變類(lèi)優(yōu)先權(quán)包含了1-15級(jí),不可變類(lèi)優(yōu)先權(quán)(實(shí)時(shí)類(lèi))包含了16-31級(jí)挽封。調(diào)度程序?yàn)槊恳粋€(gè)優(yōu)先級(jí)建一個(gè)調(diào)度隊(duì)列已球,從高優(yōu)先級(jí)到低優(yōu)先級(jí)隊(duì)列逐個(gè)查找,直到找到一個(gè)可運(yùn)行的線(xiàn)程。

Win32將進(jìn)程(process)分為如下6個(gè)優(yōu)先級(jí)類(lèi):
REALTIME_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
ABOVE_NORMAL_PRIORITY_CLASS
NORMAL_PRIORITY_CLASS
BELOW_NORMAL_PRIORITY_CLASS
IDLE_PRIORITY_CLASS
為區(qū)分進(jìn)程內(nèi)線(xiàn)程的優(yōu)先級(jí)和悦,每個(gè)優(yōu)先級(jí)類(lèi)又包含6個(gè)相對(duì)優(yōu)先級(jí):
TIME_CRITIAL
HEGHEST
ABOVE_NORNAL
NORMAL
BELOW_NORMAL
LOWEST
IDLE
這樣每個(gè)Win32線(xiàn)程屬于某個(gè)優(yōu)先級(jí)類(lèi)(由該線(xiàn)程所屬的進(jìn)程決定)退疫,并具有進(jìn)程內(nèi)的某個(gè)相對(duì)優(yōu)先級(jí),其對(duì)應(yīng)的內(nèi)核級(jí)線(xiàn)程的優(yōu)先級(jí)如下表所示:

表1.JPG

當(dāng)把Java 線(xiàn)程綁定到Win32線(xiàn)程時(shí)鸽素,需要將Java線(xiàn)程的優(yōu)先級(jí)映射到Win32線(xiàn)程上褒繁。Java 6在Windows的實(shí)現(xiàn)中將Java線(xiàn)程的優(yōu)先級(jí)按下表所示映射到Win32線(xiàn)程的相對(duì)優(yōu)先級(jí)上。

表2.JPG

當(dāng)JVM將線(xiàn)程的優(yōu)先級(jí)映射到Win32線(xiàn)程的優(yōu)先級(jí)上之后馍忽,線(xiàn)程調(diào)度的工作就是Win32和Windows內(nèi)核的事兒了棒坏。

Windows采用基于優(yōu)先級(jí)的、搶占的線(xiàn)程調(diào)度算法遭笋。調(diào)度程序保證總是讓具有最高優(yōu)先級(jí)的線(xiàn)程運(yùn)行坝冕。一個(gè)線(xiàn)程僅在如下四種情況下才會(huì)放棄CPU:(1)被一個(gè)更高優(yōu)先級(jí)的線(xiàn)程搶占;(2)結(jié)束瓦呼;(3)時(shí)間片到喂窟;(4)執(zhí)行導(dǎo)致阻塞的系統(tǒng)調(diào)用。當(dāng)線(xiàn)程的時(shí)間片用完后央串,降低其優(yōu)先級(jí)磨澡;當(dāng)線(xiàn)程從阻塞變?yōu)榫途w時(shí),增加線(xiàn)程的優(yōu)先級(jí)质和;當(dāng)線(xiàn)程很長(zhǎng)時(shí)間沒(méi)有機(jī)會(huì)運(yùn)行時(shí)稳摄,系統(tǒng)也會(huì)提升線(xiàn)程的優(yōu)先級(jí)。Windows區(qū)分前臺(tái)和后臺(tái)進(jìn)程饲宿,前臺(tái)進(jìn)程往往獲得更長(zhǎng)的時(shí)間片厦酬。以上這些措施體現(xiàn)了Windows基于動(dòng)態(tài)優(yōu)先級(jí)、分時(shí)和搶占的CPU調(diào)度策略瘫想。調(diào)度策略很復(fù)雜仗阅,考慮了線(xiàn)程執(zhí)行過(guò)程的各個(gè)方面,再加上系統(tǒng)運(yùn)行環(huán)境的變化国夜,我們很難通過(guò)線(xiàn)程運(yùn)行過(guò)程的觀察理清調(diào)度算法的全貌减噪。在本文開(kāi)頭的例子說(shuō)明了這一點(diǎn)。

由于Java線(xiàn)程到Windows內(nèi)核線(xiàn)程一對(duì)一的綁定方式支竹,所以我們看到的Java線(xiàn)程的運(yùn)行過(guò)程實(shí)際上反映的是Windows的調(diào)度策略。

請(qǐng)注意鸠按,盡管Windows采用了基于優(yōu)先級(jí)的調(diào)度策略礼搁,但不會(huì)出現(xiàn)饑餓現(xiàn)象。其采取的主要措施是:優(yōu)先級(jí)再高的的線(xiàn)程也會(huì)在運(yùn)行一個(gè)時(shí)間片之后放棄CPU目尖,并且降低其優(yōu)先級(jí)馒吴,從而保證了低優(yōu)先級(jí)線(xiàn)程也有機(jī)會(huì)運(yùn)行。

  1. Linux中Java線(xiàn)程調(diào)度

同Windows一樣,在Linux上Java線(xiàn)程一對(duì)一地映射到內(nèi)核級(jí)線(xiàn)程上饮戳。不過(guò)Linux中是不區(qū)分進(jìn)程和線(xiàn)程的豪治,同一個(gè)進(jìn)程中的線(xiàn)程可以看作是共享程度較高的一組進(jìn)程。Linux也是通過(guò)優(yōu)先級(jí)來(lái)實(shí)現(xiàn)CPU分配的扯罐,應(yīng)用程序可以通過(guò)調(diào)整nice值(謙讓值)來(lái)設(shè)置進(jìn)程的優(yōu)先級(jí)负拟。nice值反映了線(xiàn)程的謙讓程度,該值越高說(shuō)明這個(gè)線(xiàn)程越有意愿把CPU讓給別的線(xiàn)程歹河,nice的值可以由線(xiàn)程自己設(shè)定掩浙。所以JVM需要實(shí)現(xiàn)Java線(xiàn)程的優(yōu)先級(jí)到nice的映射,即從區(qū)間[1秸歧,10]到[19厨姚, -20]的映射。把自己線(xiàn)程的nice值設(shè)置高了键菱,說(shuō)明你的人品很謙讓?zhuān)?dāng)然使用CPU的機(jī)會(huì)就會(huì)少一點(diǎn)谬墙。

linux調(diào)度器實(shí)現(xiàn)了一個(gè)搶占的、基于優(yōu)先級(jí)的調(diào)度算法经备,支持兩種類(lèi)型的進(jìn)程的調(diào)度:實(shí)時(shí)進(jìn)程的優(yōu)先級(jí)范圍為[0拭抬,99],普通進(jìn)程的優(yōu)先級(jí)范圍為[100弄喘,140]玖喘。

表3.jpg

進(jìn)程的優(yōu)先權(quán)越高,所獲得的時(shí)間片就越大蘑志。每個(gè)就緒進(jìn)程都有一個(gè)時(shí)間片累奈。內(nèi)核將就緒進(jìn)程分為活動(dòng)的(active)和過(guò)期的(expired)兩類(lèi):只要進(jìn)程的時(shí)間片沒(méi)有耗盡,就一直有資格運(yùn)行急但,稱(chēng)為活動(dòng)的澎媒;當(dāng)進(jìn)程的時(shí)間片耗盡后,就沒(méi)有資格運(yùn)行了波桩,稱(chēng)為過(guò)期的戒努。調(diào)度程序總是在活動(dòng)的進(jìn)程中選擇優(yōu)先級(jí)最高的進(jìn)程執(zhí)行,直到所有的活動(dòng)進(jìn)程都耗盡了他們的時(shí)間片镐躲。當(dāng)所有的活動(dòng)進(jìn)程都變成過(guò)期的之后储玫,調(diào)度程序再將所有過(guò)期的進(jìn)程置為活動(dòng)的,并為他們分配相應(yīng)的時(shí)間片萤皂,重新進(jìn)行新一輪的調(diào)度撒穷。所以Linux的線(xiàn)程調(diào)度也不會(huì)出現(xiàn)饑餓現(xiàn)象。

在Linux上裆熙,同Windows的情況類(lèi)似端礼,Java線(xiàn)程的調(diào)度最終轉(zhuǎn)化為了操作系統(tǒng)中的進(jìn)程調(diào)度禽笑。

四.總結(jié)

從以上Java在不同平臺(tái)上的實(shí)現(xiàn)來(lái)看,只有在底層平臺(tái)不支持線(xiàn)程時(shí)蛤奥,JVM才會(huì)自己實(shí)現(xiàn)線(xiàn)程的管理和調(diào)度佳镜,此時(shí)Java線(xiàn)程以綠色線(xiàn)程的方式運(yùn)行。由于目前流行的操作系統(tǒng)都支持線(xiàn)程凡桥,所以JVM就沒(méi)必要管線(xiàn)程調(diào)度的事情了蟀伸。應(yīng)用程序通過(guò)setPriority()方法設(shè)置的線(xiàn)程優(yōu)先級(jí),將映射到內(nèi)核級(jí)線(xiàn)程的優(yōu)先級(jí)唬血,影響內(nèi)核的線(xiàn)程調(diào)度望蜡。

目前的Java的官方文檔中幾乎不再介紹有關(guān)Java線(xiàn)程的調(diào)度算法問(wèn)題,因?yàn)檫@確實(shí)不是Java的事兒了拷恨。盡管程序中還可以調(diào)用setPriority()脖律,提請(qǐng)JVM注意線(xiàn)程的優(yōu)先級(jí),但你千萬(wàn)不要把這事兒太當(dāng)真腕侄。Java中所謂的線(xiàn)程調(diào)度僅是底層平臺(tái)線(xiàn)程調(diào)度的一個(gè)影子而已小泉。

由于Java是跨平臺(tái)的,因此要求Java的程序設(shè)計(jì)不能對(duì)Java線(xiàn)程的調(diào)度方法有任何假設(shè)冕杠,即程序運(yùn)行的正確性不能依賴(lài)于線(xiàn)程調(diào)度的方法微姊。所以說(shuō)程序員最好不要過(guò)分關(guān)心底層平臺(tái)是如何實(shí)現(xiàn)線(xiàn)程調(diào)度的,呵呵分预!只要知道他們是并發(fā)運(yùn)行的就可以了兢交,甚至不必在意線(xiàn)程的優(yōu)先級(jí),因?yàn)閮?yōu)先級(jí)也不靠譜笼痹。正如Joshua Bloch在他的書(shū)《Effective Java》中給出的第72條忠告:任何依賴(lài)線(xiàn)程調(diào)度器來(lái)達(dá)到正確性或性能要求的程序配喳,很有可能都是不可移植的[5]。當(dāng)然凳干,世界上沒(méi)有絕對(duì)的事情晴裹。

如果程序員一定要規(guī)范線(xiàn)程的執(zhí)行順序,應(yīng)該使用線(xiàn)程的同步操作wait(), notify()等顯式實(shí)現(xiàn)線(xiàn)程之間的同步關(guān)系救赐,才能保證程序的正確性涧团。

參考文獻(xiàn):

[1] http://lass.cs.umass.edu/~shenoy/courses/fall01/labs/talab2.html[2] https://en.wikipedia.org/wiki/Green_threads[3] http://www.sco.com/developers/java/j2sdk122-001/ReleaseNotes.html#THREADS[4] http://docs.oracle.com/javase/8/docs/api/index.html[5] Joshua Bloch,Effective java

作者系山東大學(xué)計(jì)算機(jī)學(xué)院教授

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末经磅,一起剝皮案震驚了整個(gè)濱河市泌绣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌预厌,老刑警劉巖阿迈,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異配乓,居然都是意外死亡仿滔,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)犹芹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)崎页,“玉大人,你說(shuō)我怎么就攤上這事腰埂§梗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵屿笼,是天一觀的道長(zhǎng)牺荠。 經(jīng)常有香客問(wèn)我嗽测,道長(zhǎng)酝润,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任义郑,我火速辦了婚禮肝断,結(jié)果婚禮上杈曲,老公的妹妹穿的比我還像新娘。我一直安慰自己胸懈,他們只是感情好担扑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著趣钱,像睡著了一般涌献。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上首有,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天燕垃,我揣著相機(jī)與錄音,去河邊找鬼绞灼。 笑死利术,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的低矮。 我是一名探鬼主播印叁,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼军掂!你這毒婦竟也來(lái)了轮蜕?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蝗锥,失蹤者是張志新(化名)和其女友劉穎跃洛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體终议,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汇竭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年葱蝗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片细燎。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡两曼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出玻驻,到底是詐尸還是另有隱情悼凑,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布璧瞬,位于F島的核電站户辫,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏嗤锉。R本人自食惡果不足惜渔欢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瘟忱。 院中可真熱鬧膘茎,春花似錦、人聲如沸酷誓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)盐数。三九已至棒拂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間玫氢,已是汗流浹背帚屉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留漾峡,地道東北人攻旦。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像生逸,于是被迫代替她去往敵國(guó)和親牢屋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容