一.線程概念
在java中,并發(fā)編程是一個相當(dāng)重要的話題,通過使用線程我們可以發(fā)揮多處理器的強大計算能力,可以構(gòu)建多任務(wù)的應(yīng)用從而提升體驗,而并發(fā)編程的基礎(chǔ)就是線程。
jdk從1.0時代就引入了線程相關(guān)的類(Thread,Runnable等),在jdk1.5中又引入了java.util.concurrent包,提供了更多便捷的并發(fā)操作。
今天我們就來走近線程這位java家族的老朋友,了解他的日常。
提到線程,就不能不提進程,在操作系統(tǒng)運行過程中,進程和線程都是為了便于進行多任務(wù)處理而存在的,其中進程有獨立的內(nèi)存空間以及相關(guān)資源(如文件句柄等),而線程是在進程內(nèi)部更小的工作單元,同一個進程中的線程共享資源,有一個類比可以用來解釋進程和線程:
我們的計算機就像是一座工廠,時刻都在運行,而工廠中又有很多車間。

由于工廠的電力有限,一段時間內(nèi),只能供應(yīng)一個車間運行,當(dāng)一個車間運行的時候,其他車間就必須停工。

進程就好比是工廠里的車間。
一個車間里,有很多工人,車間里的空間和資源(比如空調(diào)、洗手間等)是車間里的工人共享的。

每個工人就相當(dāng)于進程中的線程。
簡單總結(jié)一下:進程是CPU資源分配的最小單元,線程是CPU調(diào)度的最小單元。
二.線程生命周期
我們今天的主角是線程,下面來看一下線程的生命周期。
如果查看Thread類的源碼,我們就會發(fā)現(xiàn),Thread有如下幾種狀態(tài):
public enum State {
NEW, // 新創(chuàng)建
RUNNABLE, // 可以被調(diào)度
BLOCKED, // 被阻塞
WAITING, // 等待
TIMED_WAITING, // 限時等待
TERMINATED; // 結(jié)束
}
各個狀態(tài)間的流轉(zhuǎn)關(guān)系如下圖所示:

下面我們通過一段代碼來感受一下線程的狀態(tài)流轉(zhuǎn):
public class ThreadLifeCycle {
// 定義一個線程
static class MyThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start to work:" + Thread.currentThread().getState());
// 線程執(zhí)行過程中需要調(diào)用一個同步的方法
work();
}
}
// 同步方法,將造成線程阻塞
public static synchronized void work() {
try {
// 線程進入后等待2秒鐘
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThread job1 = new MyThread();
MyThread job2 = new MyThread();
Thread thread1 = new Thread(job1, "thread1");
Thread thread2 = new Thread(job1, "thread2");
System.out.println("thread1 befor start:" + thread1.getState());
System.out.println("thread2 befor start:" + thread2.getState());
thread1.start();
thread2.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 after wait 100ms:" + thread1.getState());
System.out.println("thread2 after wait 100ms:" + thread2.getState());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 after 5000ms:" + thread1.getState());
System.out.println("thread2 after 5000ms:" + thread2.getState());
}
}
運行結(jié)果如下:
thread1 befor start:NEW
thread2 befor start:NEW
thread1 start to work:RUNNABLE
thread2 start to work:RUNNABLE
thread1 after wait 100ms:TIMED_WAITING
thread2 after wait 100ms:BLOCKED
thread1 after 5000ms:TERMINATED
thread2 after 5000ms:TERMINATED
可以看到:
- 在線程剛剛創(chuàng)建,調(diào)用start方法以前,兩個線程的狀態(tài)均為NEW;
- 在調(diào)用線程的start方法后,線程的狀態(tài)變?yōu)镽UNNABLE
- 在主線程等待100ms后,線程1處于TIMED_WAITING狀態(tài),這是由于線程1先開始執(zhí)行,并獲得了進入work方法的鎖;而線程2由于無法獲得鎖,只能處于BLOCKED狀態(tài)
- 5秒鐘后,兩個線程都已經(jīng)執(zhí)行完成了,處于TERMINATED狀態(tài)
三.線程的調(diào)度
由于java是平臺無關(guān)的,而線程調(diào)度這種事情都是操作系統(tǒng)級別的,因此具體的線程調(diào)度策略取決于具體的操作系統(tǒng)。
當(dāng)前,主流的操作系統(tǒng)均使用搶占式的,基于線程優(yōu)先級的調(diào)度策略。這里有兩個關(guān)鍵詞:搶占式、優(yōu)先級。
搶占式:所有的線程爭奪CPU使用權(quán),CPU按照一定算法來給所有線程分配時間片,一個線程執(zhí)行完自己的時間片后需要讓出CPU執(zhí)行權(quán)。
優(yōu)先級:所有的線程均有優(yōu)先級,優(yōu)先級高的線程將優(yōu)先被執(zhí)行。
我們在創(chuàng)建線程時,可以通過setPriority方法給線程設(shè)置優(yōu)先級。設(shè)置完優(yōu)先級后,jvm在執(zhí)行時,將把這個優(yōu)先級設(shè)置為操作系統(tǒng)的線程優(yōu)先級,供cpu調(diào)度。
Thread類中定義了有關(guān)優(yōu)先級的常量:
// 最低優(yōu)先級
public final static int MIN_PRIORITY = 1;
// 默認優(yōu)先級
public final static int NORM_PRIORITY = 5;
// 最高優(yōu)先級
public final static int MAX_PRIORITY = 10;
下面我們通過程序來感受一下線程的優(yōu)先級:
本人的電腦是4核,win7系統(tǒng)。為了能使線程出現(xiàn)等待和調(diào)度,我們下面將啟動8個工作線程。
public class ThreadPriority {
static class MyThread extends Thread {
private volatile boolean running = true;
private volatile Random random = new Random();
public MyThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
int count = 0;
// 不停的執(zhí)行正弦預(yù)算,直到接到結(jié)束指令
while (running) {
Math.sin(random.nextDouble());
count++;
}
System.out.println(threadName + " run " + count + " times");
}
public void shutDown() {
running = false;
}
}
public static void main(String[] args) {
// 給主線程設(shè)置為最高優(yōu)先級,以使其他線程啟動后,主線程能夠執(zhí)行后續(xù)操所
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
List<MyThread> list = new ArrayList<MyThread>();
for (int i = 0; i < 8; i++) {
MyThread thread = new MyThread( "thread" + i);
thread.setPriority(5);
list.add(thread);
}
for (int i = 0; i < 8; i++) {
list.get(i).start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 8; i++) {
list.get(i).shutDown();
}
}
}
第一次,我們將所有線程的優(yōu)先級都設(shè)置為5,執(zhí)行結(jié)果如下:
thread3 run 63641301 times
thread5 run 73277080 times
thread4 run 65114449 times
thread2 run 65464405 times
thread0 run 66025262 times
thread7 run 63567566 times
thread6 run 97058244 times
thread1 run 72544080 times
然后,我們修改一下線程的優(yōu)先級:
……
for (int i = 0; i < 8; i++) {
MyThread thread = new MyThread( "thread" + i);
// 不同的線程優(yōu)先級不同
thread.setPriority(i + 1);
list.add(thread);
}
……
執(zhí)行結(jié)果如下:
thread6 run 154709310 times
thread7 run 145893865 times
thread4 run 147268692 times
thread5 run 108303568 times
thread2 run 19743170 times
thread0 run 16955990 times
thread3 run 15611271 times
thread1 run 0 times
可以發(fā)現(xiàn),4,5,6,7四個線程執(zhí)行的次數(shù)比0,1,2,3要多一個數(shù)量級,甚至線程1沒有被分到時間片。另外也可以看出,cpu并不是完全嚴(yán)格的按照優(yōu)先級來進行調(diào)度,指定優(yōu)先級只能給cpu一個參考,低優(yōu)先級的未必得不到執(zhí)行,但是大體還是按照高優(yōu)先級先被執(zhí)行的原則的。
四.線程間的協(xié)作
在了解了線程的調(diào)度機制后,我們知道,一個線程在什么時候被調(diào)度是不確定的,即便是優(yōu)先級較高的線程,也只是原則上比優(yōu)先級低的線程優(yōu)先執(zhí)行而已,優(yōu)先多少也是不確定的。
但是有時,我們需要針對不同的線程設(shè)定一些執(zhí)行的規(guī)則,例如一個線程執(zhí)行完成后再執(zhí)行另一個線程等等,因此就需要建立線程間的協(xié)作機制。
1.wait、notify、notifyAll
這三個方法是Object類中的基礎(chǔ)方法,不知道大家有沒有疑問,反正我看到這的時候有一個疑惑:這三個方法都是用于線程間協(xié)作的,為什么不定義在Thread類中,而要定義在Object類中呢?
讓我們先來了解一下這三個方法的使用,稍后再來回答這個問題。
直接上代碼:
public class ThreadCooperation {
public synchronized void testWait() {
System.out.println(Thread.currentThread().getName() +" start");
try {
// 使進入該方法的線程處于等待狀態(tài)
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +" end");
}
static class MyThread implements Runnable {
private ThreadCooperation cooperation;
public MyThread(ThreadCooperation cooperation) {
this.cooperation = cooperation;
}
@Override
public void run() {
cooperation.testWait();
}
}
public static void main(String[] args) {
ThreadCooperation cooperation = new ThreadCooperation();
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(cooperation), "thread" + i).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after 1000ms,notify");
synchronized (cooperation) {
cooperation.notify();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after another 1000ms,notify all");
synchronized (cooperation) {
cooperation.notifyAll();
}
}
}
在上面代碼中,我們創(chuàng)建了3個線程,每個線程執(zhí)行時會進入等待狀態(tài),然后等待主線程喚醒他們。
執(zhí)行結(jié)果如下:
thread0 start
thread2 start
thread1 start
after 1000ms,notify
thread0 end
after another 1000ms,notify all
thread1 end
thread2 end
在調(diào)用notify方法時,thread0被喚醒,在調(diào)用notifyAll方法時,剩余的兩個線程被喚醒。除此之外,我們也看到,當(dāng)一個線程處于wait時,該線程會釋放鎖,以便其他線程獲取,否則,再輸出"thread0 start"后,就不會輸出"thread2 start"和"thread1 start"了。
下面我們來回答本節(jié)提出的問題:為什么wait、notify、notifyAll方法被定義在Object類中,而不是Thread類?
因為等待,是指線程在某一個資源(對象)上等待,如我們上面的程序,線程可以在任何對象上等待;喚醒也是類似,線程在對象上等待,自然應(yīng)該在同一個對象上被喚醒。因此這三個方法被定義在Object類。
2. sleep、yield、join
這三個方法都是被定義在Thread類中的。顯然,他們都是指線程的某種行為。
(1)sleep方法是讓當(dāng)前線程休眠一段時間,即讓出cpu讓其他線程執(zhí)行,但是它并不會釋放鎖。通過代碼來看一下:
public class ThreadCooperation {
public synchronized void testSleep() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class MyThread implements Runnable {
private ThreadCooperation cooperation;
public MyThread(ThreadCooperation cooperation) {
this.cooperation = cooperation;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" start");
cooperation.testSleep();
System.out.println(Thread.currentThread().getName() +" end");
}
}
public static void main(String[] args) {
ThreadCooperation cooperation = new ThreadCooperation();
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(cooperation), "thread" + i).start();
}
}
}
上面代碼中,我們?nèi)匀粍?chuàng)建3個線程,每個線程中都需要調(diào)用testSleep方法,一旦某個線程進入testSleep方法后,就會進入休眠狀態(tài),其他線程可以執(zhí)行,但是由于第一個線程正在休眠,而且沒有釋放鎖,所以其他線程只能等待。
執(zhí)行結(jié)果如下:
thread1 start
thread0 start
thread2 start
thread1 end
thread2 end
thread0 end
可以看到,程序會先輸出:
thread1 start
thread0 start
thread2 start
然后每隔一秒再輸出后面的結(jié)果。說明在一個線程休眠時,其他線程得到了執(zhí)行,但是被阻塞在了testSleep方法上。
(2)yield方法使當(dāng)前線程變?yōu)榈却龍?zhí)行狀態(tài),讓出CPU以便其他線程執(zhí)行,但是不能保證當(dāng)前線程被立刻暫停,也不能保證暫定多久,甚至如果該線程的優(yōu)先級高,可能剛剛被暫停,又重新獲得執(zhí)行。所以yield方法的行為是不甚明確的,不可靠的。
同樣來看一段代碼:
public class ThreadCooperation {
public synchronized void testYield() {
System.out.println(Thread.currentThread().getName() +" testYield start");
Thread.yield();
System.out.println(Thread.currentThread().getName() +" testYield end");
}
static class MyThread implements Runnable {
private ThreadCooperation cooperation;
public MyThread(ThreadCooperation cooperation) {
this.cooperation = cooperation;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" start");
cooperation.testYield();
System.out.println(Thread.currentThread().getName() +" end");
}
}
public static void main(String[] args) {
ThreadCooperation cooperation = new ThreadCooperation();
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(cooperation), "thread" + i).start();
}
}
}
某一次執(zhí)行的結(jié)果如下:
thread0 start
thread1 start
thread0 testYield start
thread2 start
thread0 testYield end
thread0 end
thread2 testYield start
thread2 testYield end
thread2 end
thread1 testYield start
thread1 testYield end
thread1 end
可以看到在輸出“thread0 testYield start”之后,輸出了“thread2 start”,說明thread0讓出了cpu。但讓出cpu并不意味著會讓出鎖,其他線程雖然可以執(zhí)行,但是需要等待先進入testYield方法的線程執(zhí)行完才能進入。
(3)join方法使父線程等待子線程執(zhí)行完成后才能執(zhí)行。jdk源碼中對jon方法的注釋是:“Waits for this thread to die”,也就是需要等待執(zhí)行join方法的線程執(zhí)行完。
看一下下面的代碼:
public class ThreadCooperation {
static class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" start");
System.out.println(Thread.currentThread().getName() +" end");
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(), "thread" + i).start();
}
System.out.println("main thread run");
}
}
某一次執(zhí)行結(jié)果如下:
main thread run
thread0 start
thread1 start
thread1 end
thread0 end
thread2 start
thread2 end
可以看到主線程先執(zhí)行了。如果我們想讓子線程先執(zhí)行完,再繼續(xù)執(zhí)行主線程,就可以使用join方法了:
……
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<Thread>();
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new MyThread(), "thread" + i);
threadList.add(thread);
thread.start();
}
try {
// 主線程需要等待thread1執(zhí)行完
threadList.get(1).join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread run");
}
典型的執(zhí)行結(jié)果如下:
thread0 start
thread2 start
thread2 end
thread1 start
thread1 end
main thread run
thread0 end
從中我們可以看出與不使用join方法的區(qū)別。
下面總結(jié)一下wait、notify、sleep、yield、join這幾個方法:
- wait、notify方法定義在Object類中,sleep、yield、join方法定義在Thread類中。
- sleep、yield方法是靜態(tài)方法,而join方法是實例方法。
- wait、sleep、yield三個方法都可以時當(dāng)前線程讓出cpu,但其中wait方法必須在同步塊中調(diào)用,而其他兩個不用;wait方法針對某個對象,使線程在該對象上等待;而其他兩個方法都是針對線程的;wait方法會讓當(dāng)前線程釋放鎖,而其他兩個方法不會。wait方法和sleep方法都可以指定等待的時間,而yield不可以,調(diào)用yield后其行為是不可控的。
參考資料:
- 進程與線程的一個簡單解釋
- 淺析Java的線程調(diào)度策略
- Java多線程優(yōu)先級的一些測試
- 為什么wait,notify和notifyall定義在Object中
- Java 并發(fā)編程:線程間的協(xié)作(wait/notify/sleep/yield/join)
- Java中Wait、Sleep和Yield方法的區(qū)別
本我已遷移至我的博客:http://ipenge.com/37241.html