線程基礎(chǔ)捞稿,線程之間共享與協(xié)作
1.基礎(chǔ)概念
進程概念:進程是程序運行資源分配的最小單位
進程是操作系統(tǒng)進行資源分配的最小單位,其中資源包括:CPU晴竞、內(nèi)存空間寒矿、磁盤IO 等,同一進程中的多條線程共享該進程中的全部系統(tǒng)資源,而進程和進程之間是相互獨立的衅澈。進程是具有一定獨立功能的程序關(guān)于某個數(shù)據(jù)集合上的一次運行活動,進程是系統(tǒng)進行資源分配和調(diào)度的一個獨立單位嗅榕。進程是程序在計算機上的一次執(zhí)行活動室奏。當(dāng)你運行一個程序,你就啟動了一個進程火焰。顯然,程序是死的、靜態(tài)的,進程是活的胧沫、動態(tài)的昌简。進程可以分為系統(tǒng)進程和用戶進程占业。凡是用于完成操作系統(tǒng)的各種功能的進程就是系統(tǒng)進程,它們就是處于運行狀態(tài)下的操作系統(tǒng)本身,用戶進程就是所有由你啟動的進程。
線程概念:線程是CPU 調(diào)度的最小單位,必須依賴于進程而存在
線程是進程的一個實體,是CPU 調(diào)度和分派的基本單位,它是比進程更小的江场、能獨立運行的基本單位纺酸。線程自己基本上不擁有系統(tǒng)資源,只擁有一點在運行中必不可少的資源(如程序計數(shù)器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
2. CPU 核心數(shù)和線程數(shù)的關(guān)系
多核心:也指單芯片多處理器( Chip Multiprocessors,簡稱CMP),CMP 是由美國斯坦福大學(xué)提出的,其思想是將大規(guī)模并行處理器中的SMP(對稱多處理器)集成到同一芯片內(nèi),各個處理器并行執(zhí)行不同的進程址否。這種依靠多個CPU 同時并行地運行程序是實現(xiàn)超高速計算的一個重要方向,稱為并行處理
多線程: Simultaneous Multithreading.簡稱SMT.讓同一個處理器上的多個線程同步執(zhí)行并共享處理器的執(zhí)行資源餐蔬。
核心數(shù)、線程數(shù):目前主流CPU 都是多核的佑附。增加核心數(shù)目就是為了增加線程數(shù),因為操作系統(tǒng)是通過線程來執(zhí)行任務(wù)的,一般情況下它們是1:1 對應(yīng)關(guān)系,也就是說四核CPU 一般擁有四個線程樊诺。但Intel 引入超線程技術(shù)后,使核心數(shù)與線程數(shù)形成1:2 的關(guān)系
3. CPU時間片輪轉(zhuǎn)機制(RR 調(diào)度)
? 時間片輪轉(zhuǎn)法(Round-Robin,RR)主要用于分時系統(tǒng)中的進程調(diào)度音同。為了實現(xiàn)輪轉(zhuǎn)調(diào)度词爬,系統(tǒng)把所有就緒進程按先入先出的原則排成一個隊列。新來的進程加到就緒隊列末尾权均。每當(dāng)執(zhí)行進程調(diào)度時顿膨,進程調(diào)度程序總是選出就緒隊列的隊首進程,讓它在 CPU 上運行一個時間片的時間叽赊。時間片是一個小的時間單位恋沃,通常為 10~100ms 數(shù)量級。當(dāng)進程用完分給它的時間片后必指,系統(tǒng)的計時器發(fā)出時鐘中斷囊咏,調(diào)度程序便停止該進程的運行,把它放入就緒隊列的末尾塔橡;然后梅割,把 CPU 分給就緒隊列的隊首進程,同樣也讓它運行一個時間片葛家,如此往復(fù)户辞。
?
3.1 進程調(diào)度
? 采用此算法的系統(tǒng),其程序就緒隊列往往按進程到達的時間來排序癞谒。進程調(diào)度程序總是選擇就緒隊列中的第一個進程咆课,也就是說按照先來先服務(wù)原則調(diào)度,但一旦進程占用處理機則僅使用一個時間片扯俱。在使用先一個時間片后,進程還沒有完成其運行喇澡,它必須釋放出處理機給下一個就緒的進程迅栅,而被搶占的進程返回到就緒隊列的末尾重新排隊等待再次運行。
處理器同一個時間只能處理一個任務(wù)晴玖。處理器在處理多任務(wù)的時候读存,就要看請求的時間順序为流,如果時間一致,就要進行預(yù)測让簿。挑到一個任務(wù)后敬察,需要若干步驟才能做完,這些步驟中有些需要處理器參與尔当,有些不需要(如磁盤控制器的存儲過程)莲祸。不需要處理器處理的時候,這部分時間就要分配給其他的進程椭迎。原來的進程就要處于等待的時間段上锐帜。經(jīng)過周密分配時間,宏觀上就象是多個任務(wù)一起運行一樣畜号,但微觀上是有先后的缴阎,就是時間片輪換。
3.2 實現(xiàn)思想
? 時間片輪轉(zhuǎn)算法的基本思想是简软,系統(tǒng)將所有的就緒進程按先來先服務(wù)算法的原則蛮拔,排成一個隊列,每次調(diào)度時痹升,系統(tǒng)把處理機分配給隊列首進程建炫,并讓其執(zhí)行一個時間片。當(dāng)執(zhí)行的時間片用完時视卢,由一個計時器發(fā)出時鐘中斷請求踱卵,調(diào)度程序根據(jù)這個請求停止該進程的運行,將它送到就緒隊列的末尾据过,再把處理機分給就緒隊列中新的隊列首進程惋砂,同時讓它也執(zhí)行一個時間片。
3.3 時間片設(shè)置多少合適
? 從一個進程切換到另一個進程是需要定時間的,包括保存和裝入寄存器值及內(nèi)存映像,更新各種表格和隊列等绳锅。假如進程切( processwitch),有時稱為上下文切換( context switch),需要5ms,再假設(shè)時間片設(shè)為20ms,則在做完20ms 有用的工作之后,CPU 將花費5ms 來進行進程切換西饵。CPU 時間的20%被浪費在了管理開銷上了。
? 為了提高CPU 效率,我們可以將時間片設(shè)為500ms鳞芙。這時浪費的時間只有0.1%眷柔。但考慮到在一個分時系統(tǒng)中,如果有10 個交互用戶幾乎同時按下回車鍵,將發(fā)生什么情況?假設(shè)所有其他進程都用足它們的時間片的話,最后一個不幸的進程不得不等待5s 才獲得運行機會。多數(shù)用戶無法忍受一條簡短命令要5 才能做出響應(yīng)原朝。
? 結(jié)論總結(jié)如下: 時間片設(shè)得太短會導(dǎo)致過多的進程切換,降低了CPU 效率:而設(shè)得太長又可能引起對短的交互請求的響應(yīng)變差驯嘱。將時間片設(shè)為100ms 通常是一個比較合理的折衷。
4. 并發(fā)和并行的區(qū)別
? **并發(fā): **指應(yīng)用能夠交替執(zhí)行不同的任務(wù),比如單CPU 核心下執(zhí)行多線程并非是同時執(zhí)行多個任務(wù),如果你開兩個線程執(zhí)行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個任務(wù),已達到"同時執(zhí)行效果",其實并不是的,只是計算機的速度太快,我們無法察覺到而已.
? 并行: 指應(yīng)用能夠同時執(zhí)行不同的任務(wù),例:吃飯的時候可以邊吃飯邊打電話,這兩件事情可以同時執(zhí)行
? **兩者區(qū)別: **一個是交替執(zhí)行,一個是同時執(zhí)行.
5. 多線程程序需要注意事項
5.1 線程之間的安全性
? 在同一個進程里面的多線程是資源共享的,也就是都可以訪問同一個內(nèi)存地址當(dāng)中的一個變量喳坠。例如:若每個線程中對全局變量鞠评、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的:若有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
5.2 線程之間的死鎖
? 為了解決線程之間的安全性引入了Java 的鎖機制,而一不小心就會產(chǎn)生Java線程死鎖的多線程問題,因為不同的線程都在等待那些根本不可能被釋放的鎖,從而導(dǎo)致所有的工作都無法完成壕鹉。
5.3 線程太多了會將服務(wù)器資源耗盡形成死機當(dāng)機
? 線程數(shù)太多有可能造成系統(tǒng)創(chuàng)建大量線程而導(dǎo)致消耗完系統(tǒng)內(nèi)存以及CPU的“過渡切換”,造成系統(tǒng)的死機剃幌。
? 針對多線程程序可能耗盡資源的問題聋涨,在我們的程序中應(yīng)該使用線程池來管理線程、使用數(shù)據(jù)庫連接池來管理數(shù)據(jù)庫連接负乡,用對象池來管理對象牍白,防止對象經(jīng)常創(chuàng)建和回收導(dǎo)致內(nèi)存抖動。
6. Java程序與生俱來就是多線程程序
? 寫一個最簡單的demo抖棘,看看java虛擬機會為這個demo開辟多少個線程
public static void main(String[] args) {
// Java虛擬機線程管理接口
ThreadMXBean tBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = tBean.dumpAllThreads(false, false);
for (ThreadInfo info:threadInfos) {
System.out.println("thread id:[" + info.getThreadId()
+ "]; thread name:[" + info.getThreadName() + "]");
}
}
運行結(jié)果:
thread id:[5]; thread name:[Attach Listener] //內(nèi)存dump茂腥,線程dump,類信息統(tǒng)計钉答,獲取系統(tǒng)屬性等
thread id:[4]; thread name:[Signal Dispatcher]//分發(fā)處理發(fā)送給JVM 信號的線程
thread id:[3]; thread name:[Finalizer] //調(diào)用對象finalize 方法的線程
thread id:[2]; thread name:[Reference Handler] //清除Reference 的線程
thread id:[1]; thread name:[main] //主程序础芍,用戶程序入口
7. 線程的啟動和中止
7.1線程啟動
? 線程的啟動方式有兩種:1.繼承Thread類并且實現(xiàn)run方法的方式;2.實現(xiàn)Runnable接口的方式
7.1.1 繼承Thread類并且實現(xiàn)run方法的方式
//摘自java.lang.Thread
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of <code>Thread</code>. This subclass should override the <code>run</code> method of class <code>Thread</code>. An instance of the subclass can then be allocated and started.
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
@Override
public void run() {
// compute primes larger than minPrime
}
}
PrimeThread p = new PrimeThread(143);
p.start();
7.1.2 實現(xiàn)Runnable的方法
The other way to create a thread is to declare a class implements the <code>run</code> method. An instance of the class can then be allocated, passed as an argument when creating <code>Thread</code>, and started.
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
}
}
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
7.1.3 Thread 和Runnable 的區(qū)別
? Thread 是Java 里對線程的唯一抽象数尿,Runnable 只是對任務(wù)(業(yè)務(wù)邏輯)的抽象仑性。Thread 可以接受任意一個Runnable 的實例并執(zhí)行。
7.2 線程的中止
? 線程的中止有兩種可能右蹦,要么是run()方法執(zhí)行完畢诊杆,要么是程序人為中止執(zhí)行。
? 這里重點討論程序人為中止的手段何陆。從Thread類中晨汹,可以發(fā)現(xiàn)中止線程執(zhí)行的方法有 suspend()、resume()和stop()贷盲,但是這些方法都是過時的淘这,也是官方不建議使用的,因為使用以上三種方法來暴力停止線程執(zhí)行巩剖,可能會造成死鎖的問題铝穷。以suspend()為例,當(dāng)調(diào)用了suspend()之后佳魔,線程不會釋放已經(jīng)占有的資源(比如鎖)曙聂,而是占有著資源進入睡眠狀態(tài),這樣容易引發(fā)死鎖問題鞠鲜。stop()和resume()原理是一樣的宁脊,可能會導(dǎo)致程序死鎖,嚴(yán)重會導(dǎo)致自己或者其他程序ANR贤姆。所以考慮到這種嚴(yán)重的副作用榆苞,官方不建議使用以上三種方法停止線程執(zhí)行任務(wù)。
? 那么如何比較優(yōu)雅地中止線程呢霞捡?
? 因為JDK中的線程是協(xié)作式的坐漏,而不是搶占式的,否則線程發(fā)起了中斷,線程可以不理會此中斷仙畦。所以配合使用interrupt()和isInterrupted()來中斷線程執(zhí)行。interrupt()方法只是將中斷標(biāo)志位置位了音婶,而不是強行中止線程慨畸,源碼如下:
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
? 那么正確的使用方法如下:
public static class UserThread extends Thread{
@Override
public void run() {
super.run();
String name = Thread.currentThread().getName();
//和諧停止標(biāo)志位
while (!isInterrupted()){
System.out.println(name + "running ! isInterrupt stats = " + isInterrupted());
}
System.out.println(name + " exit ! isInterrupt stats = " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UserThread userThread = new UserThread();
userThread.start();
Thread.sleep(3);
//通知要中斷,但是如果還有在執(zhí)行的任務(wù)衣式,不會真正結(jié)束
userThread.interrupt();
}
7.3 我們可以自定義標(biāo)志位來管理線程么寸士?
? 不建議自定義一個取消標(biāo)志位來中止線程的運行。因為run 方法里有阻塞調(diào)用時會無法很快檢測到取消標(biāo)志碴卧,線程必須從阻塞調(diào)用返回后弱卡,才會檢查這個取消標(biāo)志。這種情況下住册,使用中斷會更好婶博,因為,
一荧飞、一般的阻塞方法凡人,如sleep 等本身就支持中斷的檢查,
-
二叹阔、檢查中斷位的狀態(tài)和檢查取消標(biāo)志位沒什么區(qū)別挠轴,用中斷位的狀態(tài)還可以避免聲明取消標(biāo)志位,減少資源的消耗耳幢。
如果一個線程處于了阻塞狀態(tài)(如線程調(diào)用了thread.sleep岸晦、thread.join、thread.wait 等)睛藻,則線程在檢查中斷標(biāo)示時如果發(fā)現(xiàn)中斷標(biāo)示為true启上,則會在這些阻塞方法調(diào)用處拋InterruptedException 異常,并且在拋出異常后會立即將線程的中斷標(biāo)示位清除修档,即重新設(shè)置為false碧绞。
8. 關(guān)于線程的其他點點滴滴
8.1 run()和start()關(guān)系
? Thread 類是Java 里對線程概念的抽象,可以這樣理解:我們通過new Thread()其實只是new 出一個Thread 的實例吱窝,還沒有操作系統(tǒng)中真正的線程掛起鉤來讥邻。只有執(zhí)行了start()方法后,才實現(xiàn)了真正意義上的啟動線程院峡。start()方法讓一個線程進入就緒隊列等待分配cpu兴使,分到cpu 后才調(diào)用實現(xiàn)的run()方法,start()方法不能重復(fù)調(diào)用照激,如果重復(fù)調(diào)用會拋出異常发魄。而run 方法是業(yè)務(wù)邏輯實現(xiàn)的地方,本質(zhì)上和任意一個類的任意一個成員方法并沒有任何區(qū)別,可以重復(fù)執(zhí)行励幼,也可以被單獨調(diào)用汰寓。
8.2 Thread中的其他方法
8.2.1 yield()方法
? 使當(dāng)前線程讓出CPU 占有權(quán),但讓出的時間是不可設(shè)定的苹粟。也不會釋放鎖資源有滑。注意:并不是每個線程都需要這個鎖的,而且執(zhí)行yield( )的線程不一定就會持有鎖嵌削,我們完全可以在釋放鎖后再調(diào)用yield 方法毛好。所有執(zhí)行yield()的線程有可能在進入到就緒狀態(tài)后會被操作系統(tǒng)再次選中馬上又被執(zhí)行。
8.2.2 join()方法
? (1) 把指定的線程加入到當(dāng)前線程苛秕,可以將兩個交替執(zhí)行的線程合并為順序執(zhí)行肌访。比如在線程B 中調(diào)用了線程A 的Join()方法,直到線程A 執(zhí)行完畢后艇劫,才會繼續(xù)執(zhí)行線程B吼驶。
public static void main(String[] args) throws InterruptedException {
ThreadJoinTest threadJoinTest1 = new ThreadJoinTest("A");
ThreadJoinTest threadJoinTest2 = new ThreadJoinTest("B");
ThreadJoinTest threadJoinTest3 = new ThreadJoinTest("C");
threadJoinTest1.start();
threadJoinTest1.join();
threadJoinTest2.start();
threadJoinTest2.join();
threadJoinTest3.start();
threadJoinTest3.join();
}
以上事例把異步執(zhí)行的事情,變成都在主線程執(zhí)行的同步事件了港准,雖然這樣做就相當(dāng)于不開線程旨剥,主要是為了演示合并成串行執(zhí)行。
? (2) 從另一個角度上講浅缸,join()可以讓某個子線程執(zhí)行完畢之后在執(zhí)行主線程的代碼轨帜。相當(dāng)于“阻塞”主線程,等子線程執(zhí)行完成之后衩椒,在執(zhí)行主線程代碼蚌父。
public static void main(String[] args) throws InterruptedException {
ThreadJoinTest threadJoinTest1 = new ThreadJoinTest("A");
threadJoinTest1.start();
threadJoinTest1.join(); // 當(dāng)join()執(zhí)行完之后才能執(zhí)行以下代碼。
System.out.println("我只能在join()執(zhí)行完成之后才能執(zhí)行毛萌!");
}
8.2.3 線程的生命周期以及基本狀態(tài)
線程生命周期
? 關(guān)于Java中線程的生命周期苟弛,首先看一下下面這張較為經(jīng)典的圖:
線程的基本狀態(tài)
新建狀態(tài)(New):當(dāng)線程對象對創(chuàng)建后,即進入了新建狀態(tài)阁将,如:Thread t = new MyThread();
就緒狀態(tài)(Runnable):當(dāng)調(diào)用線程對象的start()方法(t.start();)膏秫,線程即進入就緒狀態(tài)。處于就緒狀態(tài)的線程做盅,只是說明此線程已經(jīng)做好了準(zhǔn)備缤削,隨時等待CPU調(diào)度執(zhí)行,并不是說執(zhí)行了t.start()此線程立即就會執(zhí)行吹榴;
運行狀態(tài)(Running):當(dāng)CPU開始調(diào)度處于就緒狀態(tài)的線程時亭敢,此時線程才得以真正執(zhí)行,即進入到運行狀態(tài)图筹。注:就 緒狀態(tài)是進入到運行狀態(tài)的唯一入口帅刀,也就是說让腹,線程要想進入運行狀態(tài)執(zhí)行,首先必須處于就緒狀態(tài)中扣溺;
阻塞狀態(tài)(Blocked):處于運行狀態(tài)中的線程由于某種原因骇窍,暫時放棄對CPU的使用權(quán),停止執(zhí)行锥余,此時進入阻塞狀態(tài)像鸡,直到其進入到就緒狀態(tài),才 有機會再次被CPU調(diào)用以進入到運行狀態(tài)哈恰。根據(jù)阻塞產(chǎn)生的原因不同,阻塞狀態(tài)又可以分為三種:
1.等待阻塞:運行狀態(tài)中的線程執(zhí)行wait()方法志群,使本線程進入到等待阻塞狀態(tài)着绷;
2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態(tài)锌云;
3.其他阻塞 -- 通過調(diào)用線程的sleep()或join()或發(fā)出了I/O請求時荠医,線程會進入到阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時桑涎、join()等待線程終止或者超時彬向、或者I/O處理完畢時,線程重新轉(zhuǎn)入就緒狀態(tài)攻冷。
死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法娃胆,該線程結(jié)束生命周期。
8.2.4 線程優(yōu)先級
? 在Java 線程中等曼,通過一個整型成員變量priority 來控制優(yōu)先級里烦,優(yōu)先級的范圍從1~10,在線程構(gòu)建的時候可以通過setPriority(int)方法來修改優(yōu)先級禁谦,默認(rèn)優(yōu)先級是5胁黑,優(yōu)先級高的線程分配時間片的數(shù)量要多于優(yōu)先級低的線程。
? 設(shè)置線程優(yōu)先級時州泊,針對頻繁阻塞(休眠或者I/O 操作)的線程需要設(shè)置較高優(yōu)先級丧蘸,而偏重計算(需要較多CPU 時間或者偏運算)的線程則設(shè)置較低的優(yōu)先級,確保處理器不會被獨占遥皂。
8.2.5 守護線程
? Daemon(守護)線程是一種支持型線程力喷,因為它主要被用作程序中后臺調(diào)度以及支持性工作。這意味著渴肉,當(dāng)一個Java 虛擬機中存在Daemon 線程的時候冗懦,當(dāng)主線程退出之后,守護線程也會跟著退出仇祭。比如垃圾回收線程就是Daemon 線程披蕉,但是在Java 虛擬機退出時Daemon 線程中的finally 塊并不一定會執(zhí)行。在構(gòu)建Daemon 線程時,不能依靠finally 塊中的內(nèi)容來確保執(zhí)行關(guān)閉或清理資源的邏輯没讲。
public static void main(String[] args) throws InterruptedException {
final Thread thread = new Thread(){
@Override
public void run() {
super.run();
for (int i = 0; i < 5; i++) {
System.out.println(getName() + "----->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//如果該線程是守護線程眯娱,那么main線程執(zhí)行完畢之后,守護線程一起結(jié)束
//如果不是守護線程爬凑,Main線程會等待該線程執(zhí)行結(jié)束后結(jié)束徙缴。
//如果是守護線程,子線程大于main的時間嘁信,main執(zhí)行完了就結(jié)束于样,不管子線程。
thread.setDaemon(true);
thread.start();
// 設(shè)置3000 6000觀察守護線程內(nèi)打印情況可以看出守護線程的生命周期
Thread.sleep(3000);
}