java從誕生之日起,就明智的選擇了內(nèi)置對多線程的支持。
幾個概念
在開始寫并發(fā)之前,先介紹幾個簡單的概念:
- 并發(fā)和并行: 并發(fā)指多個任務(wù)交替的執(zhí)行,并行指多個任務(wù)同時執(zhí)行
- 臨界區(qū):表示一種公共資源或者共享數(shù)據(jù),一次只能有一個線程訪問它
- JMM的特性: 原子性,可見性,有序性
程序、進(jìn)程、線程
- 程序:具有某些功能的代碼。
- 進(jìn)程:操作系統(tǒng)進(jìn)行資源分配和資源調(diào)度的基本單位。進(jìn)程是程序執(zhí)行的實(shí)體。
- 線程:輕量級的進(jìn)程,程序執(zhí)行的最小單位,線程中含有獨(dú)立的計數(shù)器,堆棧和局部變量等屬性,必須擁有一個父進(jìn)程,并且共享進(jìn)程中所擁有的全部資源。
進(jìn)程之間不能共享內(nèi)存,線程共享內(nèi)存非常容易。
線程的狀態(tài)
線程是有生命周期的,生命周期的狀態(tài)如下:
- NEW :新建
- READY:就緒
- RUNNABLE : 運(yùn)行
- BLOCKED :阻塞
- WAITING : 等待
- TIME_WAITING:超時等待
- TERMINATED : 終止
Java程序中的線程在自身的生命周期中,并不是固定地處于某個狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換。轉(zhuǎn)換圖如下:
新建狀態(tài)
- 使用new關(guān)鍵字創(chuàng)建線程之后,線程就處于新建狀態(tài),此時JVM為其分配了內(nèi)存,并初始化了成員變量。此時并沒有表現(xiàn)出線程的任何動態(tài)特征,程序也不會執(zhí)行線程執(zhí)行體。
就緒狀態(tài)
- 對象調(diào)用了start()方法后,該線程就處于就緒狀態(tài),JVM會為其創(chuàng)建方法調(diào)用棧和程序計數(shù)器,處于這個狀態(tài)的線程并沒有運(yùn)行,只是表示可以運(yùn)行了,何時運(yùn)行,取決于系統(tǒng)調(diào)度。
運(yùn)行狀態(tài)
- 就緒狀態(tài)的線程,獲得CPU之后,就處于運(yùn)行狀態(tài),當(dāng)當(dāng)前的時間片用完,或者運(yùn)行yield()/sleep()方法時,線程就必須放棄CPU,結(jié)束運(yùn)行狀態(tài)。
阻塞狀態(tài)/等待/超時等待
- 線程調(diào)用sleep()/join()/wait()方法,放棄CPU資源,線程被阻塞/等待
- 線程調(diào)用了一個IO方法,在該方法返回前,被阻塞
- 線程試圖獲取一個同步監(jiān)視器,但是失敗了,被阻塞
- 線程在等待某個通知
線程結(jié)束
- run()/call()方法執(zhí)行體執(zhí)行完成,線程正常結(jié)束
- 拋出Exception/Error
- 調(diào)用線程結(jié)束控制
上面提到了線程創(chuàng)建的三種方式,在代碼層級來看看線程的具體實(shí)現(xiàn):
線程的創(chuàng)建
繼承Thread類
public class ThreadTest extends Thread {
@Override
public void run() {
super.run();
System.out.println("我是線程執(zhí)行體 !");
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
threadTest.start();
}
}
實(shí)現(xiàn)Runnable接口
class RunnableTest implements Runnable{
@Override
public void run() {
System.out.println("runnable 執(zhí)行體");
}
public static void main(String[] args) {
Thread thread = new Thread(new RunnableTest());
thread.start();
}
}
實(shí)現(xiàn)Callable接口和FutureTask
class callableTest implements Callable{
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 3;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
callableTest callableTest = new callableTest();
FutureTask<Integer> futureTask = new FutureTask<Integer>(callableTest);
Thread thread = new Thread(futureTask , "有返回值的線程");
thread.start();
System.out.println(Thread.currentThread().getName());
System.out.println(futureTask.get());
}
}
上面展示了三種創(chuàng)建線程的三種方式,下面做一下簡要的分析:
- 實(shí)現(xiàn)接口的方式,還可以繼承其他的類
- 多個線程可以共享同一個target對象,適合多個線程來處理同一份資源。但是編程稍微復(fù)雜
- 繼承Thread類的方式不能在繼承其它的類,但是編寫簡單。
一般推薦使用實(shí)現(xiàn)接口的方式來創(chuàng)建多線程。
線程執(zhí)行的任務(wù)定義在了run()方法中,只有通過start()方法才能創(chuàng)建線程,這是為什么呢,通過源代碼來分析一下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runable是一個函數(shù)式接口,定義也比較簡單,只是定義了一個抽象的方法。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable也是一個函數(shù)式接口,并且支持泛型,并返回泛型的類型。
看一下Thread類的實(shí)現(xiàn),Thread類的代碼比較多,這里只闡述一些重要的點(diǎn):
先來看Thread類的定義
public class Thread implements Runnable
Thead類實(shí)現(xiàn)了Runable接口
構(gòu)造方法: Thread類提供了9個構(gòu)造方法,只闡述2個構(gòu)造方法,這兩個也是最常用的方法,如下。
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//init 方法重載
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
this.name = name.toCharArray();
//刪除了一些代碼,這些代碼會獲取一些父線程的屬性,并設(shè)置到當(dāng)前線程
....
this.target = target; //敲黑板,畫重點(diǎn)
}
上面的代碼是一個線程的創(chuàng)建過程,包括獲取一些父進(jìn)程的屬性,如設(shè)置線程的名字,設(shè)置是否是守護(hù)進(jìn)程,優(yōu)先級等。如果在創(chuàng)建線程的時候沒有指定線程的名字,那么線程的名字為:Thread-num的形式,就是通過上面的代碼來創(chuàng)建的,nextThreadNum()方法會返回一個數(shù)字,nextThreadNum()是一個被synchronized關(guān)鍵字修飾的方法,會將一個被static int類型修飾的整數(shù)進(jìn)行+1操作,并返回,這樣就保證了線程的名字不會重復(fù),當(dāng)然也可以自己指定線程的名字,在創(chuàng)建的時候傳入即可。
上面代碼target是一個私有的Runable變量,定義如下:
private Runnable target;
看一下Thread類的start()方法:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); //敲黑板,畫重點(diǎn),調(diào)用start0()方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
start()方法是一個同步方法,并且會調(diào)用start0()來運(yùn)行,start0()方法是一個被native修飾的本地方法,將委托給操作系統(tǒng)來運(yùn)行,并調(diào)用run()方法。
定義如下:
private native void start0();
調(diào)用start()方法后,線程就處于了就緒狀態(tài),系統(tǒng)調(diào)度后,變?yōu)檫\(yùn)行狀態(tài),并調(diào)用run()方法, 看一下run()方法的定義:
@Override
public void run() {
if (target != null) {
target.run();
}
}
Thread類實(shí)現(xiàn)了Runbale接口所以必須要實(shí)現(xiàn)run(),run()方法的實(shí)現(xiàn)比較簡單,首先判斷target對象是否為null,如果為null就什么也不做,如果不為null,則調(diào)用target對象的run()方法。
分析到這,應(yīng)該就能明白,有很多書上都說,無論是繼承Thread類,還是實(shí)現(xiàn)了Runable接口都必須要重寫run()方法,原因就在這。
繼承Thread類的對象,調(diào)用Thread類的無參構(gòu)造方法,此時target對象為空,但是它重寫了run()方法,它會調(diào)用自己的run方法,實(shí)現(xiàn)了Runnable接口的對象,需要借助Thread(Runable target ) 構(gòu)造方法運(yùn)行 , 此時target對象不為空,會調(diào)用target的run()方法。
關(guān)于Callable和FutureTask的實(shí)現(xiàn)方式,這里簡單說一下, FutureTask實(shí)現(xiàn)了 RunnableFuture接口, 而RunnableFuture接口繼承了Runable接口, 所以它能通過new Thread(Runnable target)構(gòu)造函數(shù)來運(yùn)行。會調(diào)用FutureTask的run()方法,在run()方法中調(diào)用了Callable對象的call()方法,獲得了其返回值,換句話說 FutureTask對象封裝了該Callable對象的返回值。通過FutureTask的get()方法來獲得子線程執(zhí)行結(jié)束后的返回值。
線程控制
- join線程:讓一個線程等待另一個線程執(zhí)行完成的方法 join() ,主線程創(chuàng)建了一個子線程,并且調(diào)用了join()方法,那么主線程只有等待子線程執(zhí)行完成后,才能向下運(yùn)行。如果調(diào)用了join(long millis)主線程會在等到millis時間后向下執(zhí)行。
- 守護(hù)線程(daemon):一個線程設(shè)置成為守護(hù)線程后,會隨著前臺線程的結(jié)束而結(jié)束。GC(垃圾回收)就是一個非常典型的守護(hù)進(jìn)程。
- 線程睡眠sleep():使線程進(jìn)入阻塞狀態(tài),不在運(yùn)行
- 線程讓步y(tǒng)ield():yield和sleep有點(diǎn)類似,不同是它進(jìn)入的不是阻塞狀態(tài),而是就緒狀態(tài)。