線程基礎,線程之間共享與協(xié)作
1.基礎概念
進程概念:進程是程序運行資源分配的最小單位
進程是操作系統(tǒng)進行資源分配的最小單位,其中資源包括:CPU、內存空間、磁盤IO 等,同一進程中的多條線程共享該進程中的全部系統(tǒng)資源,而進程和進程之間是相互獨立的。進程是具有一定獨立功能的程序關于某個數據集合上的一次運行活動,進程是系統(tǒng)進行資源分配和調度的一個獨立單位。進程是程序在計算機上的一次執(zhí)行活動。當你運行一個程序,你就啟動了一個進程。顯然,程序是死的、靜態(tài)的,進程是活的、動態(tài)的。進程可以分為系統(tǒng)進程和用戶進程。凡是用于完成操作系統(tǒng)的各種功能的進程就是系統(tǒng)進程,它們就是處于運行狀態(tài)下的操作系統(tǒng)本身,用戶進程就是所有由你啟動的進程。
線程概念:線程是CPU 調度的最小單位,必須依賴于進程而存在
線程是進程的一個實體,是CPU 調度和分派的基本單位,它是比進程更小的、能獨立運行的基本單位。線程自己基本上不擁有系統(tǒng)資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
2. CPU 核心數和線程數的關系
多核心:也指單芯片多處理器( Chip Multiprocessors,簡稱CMP),CMP 是由美國斯坦福大學提出的,其思想是將大規(guī)模并行處理器中的SMP(對稱多處理器)集成到同一芯片內,各個處理器并行執(zhí)行不同的進程。這種依靠多個CPU 同時并行地運行程序是實現超高速計算的一個重要方向,稱為并行處理
多線程: Simultaneous Multithreading.簡稱SMT.讓同一個處理器上的多個線程同步執(zhí)行并共享處理器的執(zhí)行資源。
核心數、線程數:目前主流CPU 都是多核的。增加核心數目就是為了增加線程數,因為操作系統(tǒng)是通過線程來執(zhí)行任務的,一般情況下它們是1:1 對應關系,也就是說四核CPU 一般擁有四個線程。但Intel 引入超線程技術后,使核心數與線程數形成1:2 的關系
3. CPU時間片輪轉機制(RR 調度)
? 時間片輪轉法(Round-Robin,RR)主要用于分時系統(tǒng)中的進程調度。為了實現輪轉調度,系統(tǒng)把所有就緒進程按先入先出的原則排成一個隊列。新來的進程加到就緒隊列末尾。每當執(zhí)行進程調度時,進程調度程序總是選出就緒隊列的隊首進程,讓它在 CPU 上運行一個時間片的時間。時間片是一個小的時間單位,通常為 10~100ms 數量級。當進程用完分給它的時間片后,系統(tǒng)的計時器發(fā)出時鐘中斷,調度程序便停止該進程的運行,把它放入就緒隊列的末尾;然后,把 CPU 分給就緒隊列的隊首進程,同樣也讓它運行一個時間片,如此往復。
?
3.1 進程調度
? 采用此算法的系統(tǒng),其程序就緒隊列往往按進程到達的時間來排序。進程調度程序總是選擇就緒隊列中的第一個進程,也就是說按照先來先服務原則調度,但一旦進程占用處理機則僅使用一個時間片。在使用先一個時間片后,進程還沒有完成其運行,它必須釋放出處理機給下一個就緒的進程,而被搶占的進程返回到就緒隊列的末尾重新排隊等待再次運行。
處理器同一個時間只能處理一個任務。處理器在處理多任務的時候,就要看請求的時間順序,如果時間一致,就要進行預測。挑到一個任務后,需要若干步驟才能做完,這些步驟中有些需要處理器參與,有些不需要(如磁盤控制器的存儲過程)。不需要處理器處理的時候,這部分時間就要分配給其他的進程。原來的進程就要處于等待的時間段上。經過周密分配時間,宏觀上就象是多個任務一起運行一樣,但微觀上是有先后的,就是時間片輪換。
3.2 實現思想
? 時間片輪轉算法的基本思想是,系統(tǒng)將所有的就緒進程按先來先服務算法的原則,排成一個隊列,每次調度時,系統(tǒng)把處理機分配給隊列首進程,并讓其執(zhí)行一個時間片。當執(zhí)行的時間片用完時,由一個計時器發(fā)出時鐘中斷請求,調度程序根據這個請求停止該進程的運行,將它送到就緒隊列的末尾,再把處理機分給就緒隊列中新的隊列首進程,同時讓它也執(zhí)行一個時間片。
3.3 時間片設置多少合適
? 從一個進程切換到另一個進程是需要定時間的,包括保存和裝入寄存器值及內存映像,更新各種表格和隊列等。假如進程切( processwitch),有時稱為上下文切換( context switch),需要5ms,再假設時間片設為20ms,則在做完20ms 有用的工作之后,CPU 將花費5ms 來進行進程切換。CPU 時間的20%被浪費在了管理開銷上了。
? 為了提高CPU 效率,我們可以將時間片設為500ms。這時浪費的時間只有0.1%。但考慮到在一個分時系統(tǒng)中,如果有10 個交互用戶幾乎同時按下回車鍵,將發(fā)生什么情況?假設所有其他進程都用足它們的時間片的話,最后一個不幸的進程不得不等待5s 才獲得運行機會。多數用戶無法忍受一條簡短命令要5 才能做出響應。
? 結論總結如下: 時間片設得太短會導致過多的進程切換,降低了CPU 效率:而設得太長又可能引起對短的交互請求的響應變差。將時間片設為100ms 通常是一個比較合理的折衷。
4. 并發(fā)和并行的區(qū)別
? **并發(fā): **指應用能夠交替執(zhí)行不同的任務,比如單CPU 核心下執(zhí)行多線程并非是同時執(zhí)行多個任務,如果你開兩個線程執(zhí)行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個任務,已達到"同時執(zhí)行效果",其實并不是的,只是計算機的速度太快,我們無法察覺到而已.
? 并行: 指應用能夠同時執(zhí)行不同的任務,例:吃飯的時候可以邊吃飯邊打電話,這兩件事情可以同時執(zhí)行
? **兩者區(qū)別: **一個是交替執(zhí)行,一個是同時執(zhí)行.
5. 多線程程序需要注意事項
5.1 線程之間的安全性
? 在同一個進程里面的多線程是資源共享的,也就是都可以訪問同一個內存地址當中的一個變量。例如:若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的:若有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
5.2 線程之間的死鎖
? 為了解決線程之間的安全性引入了Java 的鎖機制,而一不小心就會產生Java線程死鎖的多線程問題,因為不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。
5.3 線程太多了會將服務器資源耗盡形成死機當機
? 線程數太多有可能造成系統(tǒng)創(chuàng)建大量線程而導致消耗完系統(tǒng)內存以及CPU的“過渡切換”,造成系統(tǒng)的死機。
? 針對多線程程序可能耗盡資源的問題,在我們的程序中應該使用線程池來管理線程、使用數據庫連接池來管理數據庫連接,用對象池來管理對象,防止對象經常創(chuàng)建和回收導致內存抖動。
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() + "]");
}
}
運行結果:
thread id:[5]; thread name:[Attach Listener] //內存dump,線程dump,類信息統(tǒng)計,獲取系統(tǒng)屬性等
thread id:[4]; thread name:[Signal Dispatcher]//分發(fā)處理發(fā)送給JVM 信號的線程
thread id:[3]; thread name:[Finalizer] //調用對象finalize 方法的線程
thread id:[2]; thread name:[Reference Handler] //清除Reference 的線程
thread id:[1]; thread name:[main] //主程序,用戶程序入口
7. 線程的啟動和中止
7.1線程啟動
? 線程的啟動方式有兩種:1.繼承Thread類并且實現run方法的方式;2.實現Runnable接口的方式
7.1.1 繼承Thread類并且實現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 實現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 只是對任務(業(yè)務邏輯)的抽象。Thread 可以接受任意一個Runnable 的實例并執(zhí)行。
7.2 線程的中止
? 線程的中止有兩種可能,要么是run()方法執(zhí)行完畢,要么是程序人為中止執(zhí)行。
? 這里重點討論程序人為中止的手段。從Thread類中,可以發(fā)現中止線程執(zhí)行的方法有 suspend()、resume()和stop(),但是這些方法都是過時的,也是官方不建議使用的,因為使用以上三種方法來暴力停止線程執(zhí)行,可能會造成死鎖的問題。以suspend()為例,當調用了suspend()之后,線程不會釋放已經占有的資源(比如鎖),而是占有著資源進入睡眠狀態(tài),這樣容易引發(fā)死鎖問題。stop()和resume()原理是一樣的,可能會導致程序死鎖,嚴重會導致自己或者其他程序ANR。所以考慮到這種嚴重的副作用,官方不建議使用以上三種方法停止線程執(zhí)行任務。
? 那么如何比較優(yōu)雅地中止線程呢?
? 因為JDK中的線程是協(xié)作式的,而不是搶占式的,否則線程發(fā)起了中斷,線程可以不理會此中斷。所以配合使用interrupt()和isInterrupted()來中斷線程執(zhí)行。interrupt()方法只是將中斷標志位置位了,而不是強行中止線程,源碼如下:
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();
//和諧停止標志位
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í)行的任務,不會真正結束
userThread.interrupt();
}
7.3 我們可以自定義標志位來管理線程么?
? 不建議自定義一個取消標志位來中止線程的運行。因為run 方法里有阻塞調用時會無法很快檢測到取消標志,線程必須從阻塞調用返回后,才會檢查這個取消標志。這種情況下,使用中斷會更好,因為,
一、一般的阻塞方法,如sleep 等本身就支持中斷的檢查,
-
二、檢查中斷位的狀態(tài)和檢查取消標志位沒什么區(qū)別,用中斷位的狀態(tài)還可以避免聲明取消標志位,減少資源的消耗。
如果一個線程處于了阻塞狀態(tài)(如線程調用了thread.sleep、thread.join、thread.wait 等),則線程在檢查中斷標示時如果發(fā)現中斷標示為true,則會在這些阻塞方法調用處拋InterruptedException 異常,并且在拋出異常后會立即將線程的中斷標示位清除,即重新設置為false。
8. 關于線程的其他點點滴滴
8.1 run()和start()關系
? Thread 類是Java 里對線程概念的抽象,可以這樣理解:我們通過new Thread()其實只是new 出一個Thread 的實例,還沒有操作系統(tǒng)中真正的線程掛起鉤來。只有執(zhí)行了start()方法后,才實現了真正意義上的啟動線程。start()方法讓一個線程進入就緒隊列等待分配cpu,分到cpu 后才調用實現的run()方法,start()方法不能重復調用,如果重復調用會拋出異常。而run 方法是業(yè)務邏輯實現的地方,本質上和任意一個類的任意一個成員方法并沒有任何區(qū)別,可以重復執(zhí)行,也可以被單獨調用。
8.2 Thread中的其他方法
8.2.1 yield()方法
? 使當前線程讓出CPU 占有權,但讓出的時間是不可設定的。也不會釋放鎖資源。注意:并不是每個線程都需要這個鎖的,而且執(zhí)行yield( )的線程不一定就會持有鎖,我們完全可以在釋放鎖后再調用yield 方法。所有執(zhí)行yield()的線程有可能在進入到就緒狀態(tài)后會被操作系統(tǒng)再次選中馬上又被執(zhí)行。
8.2.2 join()方法
? (1) 把指定的線程加入到當前線程,可以將兩個交替執(zhí)行的線程合并為順序執(zhí)行。比如在線程B 中調用了線程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í)行的同步事件了,雖然這樣做就相當于不開線程,主要是為了演示合并成串行執(zhí)行。
? (2) 從另一個角度上講,join()可以讓某個子線程執(zhí)行完畢之后在執(zhí)行主線程的代碼。相當于“阻塞”主線程,等子線程執(zhí)行完成之后,在執(zhí)行主線程代碼。
public static void main(String[] args) throws InterruptedException {
ThreadJoinTest threadJoinTest1 = new ThreadJoinTest("A");
threadJoinTest1.start();
threadJoinTest1.join(); // 當join()執(zhí)行完之后才能執(zhí)行以下代碼。
System.out.println("我只能在join()執(zhí)行完成之后才能執(zhí)行!");
}
8.2.3 線程的生命周期以及基本狀態(tài)
線程生命周期
? 關于Java中線程的生命周期,首先看一下下面這張較為經典的圖:

線程的基本狀態(tài)
新建狀態(tài)(New):當線程對象對創(chuàng)建后,即進入了新建狀態(tài),如:Thread t = new MyThread();
就緒狀態(tài)(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態(tài)。處于就緒狀態(tài)的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執(zhí)行,并不是說執(zhí)行了t.start()此線程立即就會執(zhí)行;
運行狀態(tài)(Running):當CPU開始調度處于就緒狀態(tài)的線程時,此時線程才得以真正執(zhí)行,即進入到運行狀態(tài)。注:就 緒狀態(tài)是進入到運行狀態(tài)的唯一入口,也就是說,線程要想進入運行狀態(tài)執(zhí)行,首先必須處于就緒狀態(tài)中;
阻塞狀態(tài)(Blocked):處于運行狀態(tài)中的線程由于某種原因,暫時放棄對CPU的使用權,停止執(zhí)行,此時進入阻塞狀態(tài),直到其進入到就緒狀態(tài),才 有機會再次被CPU調用以進入到運行狀態(tài)。根據阻塞產生的原因不同,阻塞狀態(tài)又可以分為三種:
1.等待阻塞:運行狀態(tài)中的線程執(zhí)行wait()方法,使本線程進入到等待阻塞狀態(tài);
2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態(tài);
3.其他阻塞 -- 通過調用線程的sleep()或join()或發(fā)出了I/O請求時,線程會進入到阻塞狀態(tài)。當sleep()狀態(tài)超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態(tài)。
死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結束生命周期。
8.2.4 線程優(yōu)先級
? 在Java 線程中,通過一個整型成員變量priority 來控制優(yōu)先級,優(yōu)先級的范圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優(yōu)先級,默認優(yōu)先級是5,優(yōu)先級高的線程分配時間片的數量要多于優(yōu)先級低的線程。
? 設置線程優(yōu)先級時,針對頻繁阻塞(休眠或者I/O 操作)的線程需要設置較高優(yōu)先級,而偏重計算(需要較多CPU 時間或者偏運算)的線程則設置較低的優(yōu)先級,確保處理器不會被獨占。
8.2.5 守護線程
? Daemon(守護)線程是一種支持型線程,因為它主要被用作程序中后臺調度以及支持性工作。這意味著,當一個Java 虛擬機中存在Daemon 線程的時候,當主線程退出之后,守護線程也會跟著退出。比如垃圾回收線程就是Daemon 線程,但是在Java 虛擬機退出時Daemon 線程中的finally 塊并不一定會執(zhí)行。在構建Daemon 線程時,不能依靠finally 塊中的內容來確保執(zhí)行關閉或清理資源的邏輯。
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í)行完畢之后,守護線程一起結束
//如果不是守護線程,Main線程會等待該線程執(zhí)行結束后結束。
//如果是守護線程,子線程大于main的時間,main執(zhí)行完了就結束,不管子線程。
thread.setDaemon(true);
thread.start();
// 設置3000 6000觀察守護線程內打印情況可以看出守護線程的生命周期
Thread.sleep(3000);
}