這周趕項(xiàng)目允耿,暫停了一下微博梳玫。結(jié)果今天看到簡(jiǎn)書app的圖標(biāo)竟然有負(fù)罪感!趁著周末右犹,更新一波提澎。。念链。
從本文開始盼忌,我們開始分析一個(gè)新的Java的知識(shí)點(diǎn)--多線程。要研究這個(gè)掂墓,首先我們要知道谦纱,什么是線程。
線程的定義
進(jìn)程是指一個(gè)內(nèi)存中運(yùn)行的應(yīng)用程序君编,每個(gè)進(jìn)程都有自己獨(dú)立的一塊內(nèi)存空間跨嘉,擁有自己的數(shù)據(jù)和代碼。
線程是指進(jìn)程中的一個(gè)任務(wù)吃嘿,一個(gè)進(jìn)程中可以運(yùn)行多個(gè)線程祠乃。線程是屬于某個(gè)進(jìn)程,進(jìn)程中的多個(gè)線程共享此進(jìn)程的內(nèi)存兑燥。
拿手機(jī)舉個(gè)例子亮瓷,我們拿著手機(jī)可以一邊聽歌一邊看微信,此時(shí)降瞳,微信和聽歌軟件就構(gòu)成了多進(jìn)程嘱支。即同一時(shí)刻,有不同的進(jìn)程在工作挣饥。而多線程就是指在同一個(gè)程序中除师,同時(shí)執(zhí)行多個(gè)任務(wù),通常扔枫,每個(gè)任務(wù)稱為一個(gè)線程汛聚。線程跟進(jìn)程的區(qū)別是,每個(gè)進(jìn)程都有自己獨(dú)立的數(shù)據(jù)空間茧吊,而同一類的線程共享數(shù)據(jù)贞岭。多進(jìn)程是為了提高CPU的使用率,而多線程是為了提高應(yīng)用程序的使用率搓侄。
線程的狀態(tài)
線程的狀態(tài)如下:
線程的狀態(tài)分為五種:
新建狀態(tài):當(dāng)使用某種方式創(chuàng)建一個(gè)線程對(duì)象后瞄桨,該線程就是新建狀態(tài)
就緒狀態(tài):當(dāng)已創(chuàng)建的線程的start()方法被調(diào)用后,就進(jìn)入就緒狀態(tài)讶踪。這種狀態(tài)也叫做"可執(zhí)行狀態(tài)"芯侥。在這種狀態(tài)下,該線程隨時(shí)等待被CPU調(diào)度執(zhí)行(注意乳讥,這個(gè)時(shí)候不是立即執(zhí)行柱查,而是等待CPU的調(diào)度)。
可運(yùn)行狀態(tài):在Java虛擬機(jī)中執(zhí)行的線程處于此狀態(tài)云石。即線程取得CPU的權(quán)限唉工,開始執(zhí)行。線程只能從就緒狀態(tài)進(jìn)入運(yùn)行狀態(tài)汹忠。(在任何給定時(shí)刻淋硝,一個(gè)可運(yùn)行的線程可能正在運(yùn)行也可能沒有運(yùn)行)
阻塞狀態(tài):當(dāng)線程因?yàn)槟撤N關(guān)系,無法獲得CPU的使用權(quán)限時(shí)宽菜,就處于這種狀態(tài)谣膳。比如調(diào)用的wait()方法(等待)、有同步鎖(阻塞)及調(diào)用了sleep()方法(計(jì)時(shí)等待)等铅乡。
死亡狀態(tài):以退出的線程處于此狀態(tài)继谚。退出可能是線程執(zhí)行完畢,或者是發(fā)生了異常等阵幸。
當(dāng)一個(gè)線程開始運(yùn)行的時(shí)候花履,它并不是始終運(yùn)行的。因?yàn)镴ava中多線程是搶占式調(diào)度挚赊,即多個(gè)線程搶占時(shí)間片來執(zhí)行任務(wù)臭挽。當(dāng)一個(gè)線程的時(shí)間片用完后,它就會(huì)被系統(tǒng)剝奪其運(yùn)行權(quán)限咬腕,并與其他線程共同爭(zhēng)奪下一個(gè)時(shí)間片的使用權(quán)欢峰。
線程的創(chuàng)建
創(chuàng)建線程的方式有:繼承Thread類、實(shí)現(xiàn)Runnable接口及通過Callable和Future新建一個(gè)線程涨共。
繼承Thread類
創(chuàng)建的步驟為:
1纽帖、繼承Thread類
2、重寫run方法
3举反、實(shí)例化我們寫的Thread類的子類懊直,并調(diào)用start方法啟動(dòng)線程
具體代碼如下:
//繼承Thread類
public class MyThread extends Thread{
//重寫run方法(線程的執(zhí)行部分)
@Override
public void run() {
//自己的代碼邏輯
........
}
}
public class Main{
public static void main(String [] args){
//實(shí)例化一個(gè)MyThread類的子類
MyThread myThread = new MyThread();
//調(diào)用start方法啟動(dòng)線程
myThread.start();
}
}
實(shí)現(xiàn)Runnable接口
創(chuàng)建步驟為:
1、定義一個(gè)類火鼻,實(shí)現(xiàn)Runnable接口室囊,重寫run方法雕崩;
2、創(chuàng)建一個(gè)Runnable的實(shí)現(xiàn)類的實(shí)例融撞,并以此為參數(shù)創(chuàng)建一個(gè)Thread類
3盼铁、調(diào)用Thread類的start方法,啟動(dòng)線程
還可以創(chuàng)建一個(gè)實(shí)現(xiàn)Runnable接口的匿名類尝偎,或者創(chuàng)建一個(gè)實(shí)現(xiàn)Runnable接口的Java Lambda表達(dá)式(JDK8之后)饶火。
具體代碼如下:
//定義一個(gè)類,實(shí)現(xiàn)Runnable接口
public class MyRunnable implements Runnable {
//重寫run方法
@Override
public void run() {
//自己的代碼邏輯
.......
}
}
public class Main{
public static void main(String [] args){
//實(shí)例化一個(gè)Runnable實(shí)現(xiàn)類的實(shí)例
MyRunnable myRunnable = new MyRunnable();
//以myRunnable為參數(shù)實(shí)例化一個(gè)Thread類
Thread thread = new Thread(myRunnable);
//調(diào)用start方法啟動(dòng)線程
thread();
/**
*--------分割線------
*/
//創(chuàng)建Runnable的匿名實(shí)現(xiàn)
Runnable myRunnable =new Runnable(){
public void run(){
//自己的代碼邏輯
.......
}
}
//Runnable的Lambda實(shí)現(xiàn)
Runnable runnable =() -> { //自己的代碼邏輯};
}
注意:
1致扯、使用"myThread.run();"時(shí)肤寝,run()方法并非是由剛創(chuàng)建的新線程執(zhí)行,而是被創(chuàng)建新線程的當(dāng)前線程所執(zhí)行了抖僵。想要讓創(chuàng)建的新線程執(zhí)行run()方法鲤看,必須調(diào)用新線程的start()方法。且start()方法不可以多次調(diào)用耍群。
2刨摩、調(diào)用start()方法后,并不是讓線程立刻執(zhí)行世吨,而是將線程變?yōu)榭蓤?zhí)行狀態(tài)澡刹,等待CPU的調(diào)度。
問題來了耘婚,當(dāng)用此方式創(chuàng)建線程后罢浇,線程執(zhí)行的run()方法是Runnable接口中的還是Thread類中的?
我們看一下Thread類的定義及其run()方法:
public class Thread implements Runnable {
private Runnable target;
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
......
this.target = target;
......
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
可以看到沐祷,在run()方法中嚷闭,會(huì)判斷target是否為空,不為空則執(zhí)行Runnable的run()方法赖临,否則此方法不執(zhí)行任何操作并返回胞锰。也是因?yàn)槿绱耍琂ava提示Thread的子類要重寫此方法兢榨。
實(shí)現(xiàn)Runnable接口比繼承Thread類的優(yōu)勢(shì):
1嗅榕、避免了Java中的單繼承限制
2、適合多個(gè)相同的程序代碼的線程去處理同一個(gè)共享資源
3吵聪、代碼可以被多個(gè)線程共享且代碼和數(shù)據(jù)獨(dú)立凌那,增加了程序的健壯性
通過Callable和FutureTask
創(chuàng)建步驟為:
1、創(chuàng)建一個(gè)Callable接口的實(shí)現(xiàn)類吟逝,并實(shí)現(xiàn)call()方法
2帽蝶、使用FutureTask類包裝Callable實(shí)現(xiàn)類的對(duì)象,封裝了Callable的call()方法的返回值
3块攒、以FutureTask對(duì)象為Thread的參數(shù)創(chuàng)建線程励稳,并啟動(dòng)線程
4佃乘、調(diào)用FutureTask對(duì)象的get()方法,獲取線程執(zhí)行結(jié)束后的返回值
代碼如下:
//創(chuàng)建一個(gè)Callable接口的實(shí)現(xiàn)類驹尼,并實(shí)現(xiàn)call()方法
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//自己的代碼邏輯
return value;
}
}
public class Main{
public static void main(String [] args){
//使用FutureTask類包裝Callable實(shí)現(xiàn)類的對(duì)象
MyThread myThread = new MyThread();
FutureTask futureTask = new FutureTask<Integer>(myThread);
//以FutureTask對(duì)象為Thread的參數(shù)創(chuàng)建線程
Thread thread = new Thread(futureTask);
thread.start();
//獲取值
try {
//get()方法會(huì)阻塞趣避,直到子線程執(zhí)行結(jié)束才返回
int x = (int) futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
其中,Callable的類型參數(shù)為返回值的類型扶欣,F(xiàn)uture保存異步計(jì)算的結(jié)果。
在計(jì)算過程中千扶,可以使用isDone方法判斷Future任務(wù)是否結(jié)束(包括正常結(jié)束或者中途退出)料祠,返回true表示完成,返回false表示未完成澎羞∷枵溃可以用cancal方法取消計(jì)算。
一般推薦使用實(shí)現(xiàn)Runable或Callable接口的方式來創(chuàng)建多線程妆绞。因?yàn)檫@樣既可以繼承其他類顺呕,而且多線程可以共享一個(gè)target,即多線程可以共同處理同一份資源括饶。
線程的優(yōu)先級(jí)及守護(hù)線程
線程優(yōu)先級(jí)
每個(gè)線程都有優(yōu)先級(jí)株茶,且默認(rèn)情況下赫编,線程繼承其父類的優(yōu)先級(jí)点弯。我們可以使用setPriority方法為線程設(shè)置優(yōu)先級(jí)「鸺遥可以將線程的優(yōu)先級(jí)設(shè)置在MIN_PRIORITY(1)和MAX_PRIORITY(10)之間技羔。NORM_PRIORITY表示線程優(yōu)先級(jí)為5僵闯,為默認(rèn)優(yōu)先級(jí)。數(shù)字越大藤滥,表示優(yōu)先級(jí)越高鳖粟。高優(yōu)先級(jí)的線程被CPU調(diào)用的概率大于低優(yōu)先級(jí)的線程。不過要注意的是線程優(yōu)先級(jí)無法保證線程的執(zhí)行順序拙绊,它是依賴于平臺(tái)的向图。比如在Linux下,線程優(yōu)先級(jí)僅適用于Java6之后标沪,在這之前線程優(yōu)先級(jí)沒有作用张漂。
守護(hù)線程
在Java中,線程可以分為用戶線程和守護(hù)線程谨娜,可以使用Thread類的setDaemon方法將一個(gè)線程設(shè)置為守護(hù)線程航攒。守護(hù)線程在后臺(tái)運(yùn)行,當(dāng)JVM中沒有其他非守護(hù)線程時(shí)趴梢,守護(hù)線程會(huì)和JVM一起結(jié)束漠畜。守護(hù)線程的作用是為其他線程提供服務(wù)币他,比如我們熟悉的GC就是這樣。要注意的是憔狞,不要用守護(hù)線程訪問文件或數(shù)據(jù)庫等資源蝴悉。因?yàn)槭刈o(hù)線程可能在任何時(shí)候發(fā)生中斷,而這個(gè)時(shí)候瘾敢,我們對(duì)資源文件的讀寫有可能還沒有完成拍冠。
有時(shí)候主線程都結(jié)束了,守護(hù)線程還在執(zhí)行簇抵,這是因?yàn)榫€程結(jié)束是需要時(shí)間的庆杜。
Thread類的部分方法
start()方法
start方法為將線程由新建狀態(tài)轉(zhuǎn)變?yōu)榭蛇\(yùn)行狀態(tài),其源碼如下:
//表示Java線程狀態(tài)的工具碟摆,初始化為線程未啟動(dòng)
private volatile int threadStatus = 0;
//線程是否運(yùn)行的標(biāo)志
boolean started = false;
//此線程的線程組
private ThreadGroup group;
public synchronized void start() {
//判斷線程是否未啟動(dòng)或者已經(jīng)運(yùn)行晃财,滿足一項(xiàng)則拋出異常
if (threadStatus != 0 || started)
throw new IllegalThreadStateException();
//添加次線程到其線程組
group.add(this);
//將運(yùn)行狀態(tài)置為false
started = false;
try {
//調(diào)用本地方法啟動(dòng)線程
nativeCreate(this, stackSize, daemon);
//將線程的運(yùn)行狀態(tài)置為true
started = true;
} finally {
try {
//如果啟動(dòng)失敗,從其線程組中移除此線程
//此線程組的狀態(tài)將回滾典蜕,就像從未嘗試啟動(dòng)線程一樣断盛。該線程再次被視為線程組的未啟動(dòng)成員,允許隨后嘗試啟動(dòng)該線程愉舔。
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
可以看到钢猛,當(dāng)一個(gè)線程是未啟動(dòng)或者已運(yùn)行時(shí),調(diào)用start方法將拋出異常轩缤。當(dāng)線程啟動(dòng)失敗后厢洞,會(huì)將其從線程組中移除,以讓其有機(jī)會(huì)重新啟動(dòng)典奉。
sleep()方法
使當(dāng)前正在執(zhí)行的線程休眠(暫時(shí)停止執(zhí)行)躺翻,該線程不會(huì)失去任何監(jiān)視器的所有權(quán)。源碼如下:
private static final int NANOS_PER_MILLI = 1000000;
public static void sleep(long millis) throws InterruptedException {
Thread.sleep(millis, 0);
}
//實(shí)際調(diào)用的方法
public static void sleep(long millis, int nanos)
throws InterruptedException {
//判斷傳入的毫秒和納秒是否有錯(cuò)誤
if (millis < 0) {
throw new IllegalArgumentException("millis < 0: " + millis);
}
if (nanos < 0) {
throw new IllegalArgumentException("nanos < 0: " + nanos);
}
if (nanos > 999999) {
throw new IllegalArgumentException("nanos > 999999: " + nanos);
}
//零睡眠
if (millis == 0 && nanos == 0) {
//如果線程為中斷狀態(tài)卫玖,則拋出異常并返回
if (Thread.interrupted()) {
throw new InterruptedException();
}
return;
}
//返回運(yùn)行的Java虛擬機(jī)的高分辨率時(shí)間源的當(dāng)前值公你,以納秒計(jì)。
long start = System.nanoTime();
//將傳入的時(shí)間轉(zhuǎn)為納秒級(jí)
long duration = (millis * NANOS_PER_MILLI) + nanos;
//獲取鎖
Object lock = currentThread().lock;
//等待可能會(huì)提前返回假瞬,所以循環(huán)直到睡眠時(shí)間結(jié)束陕靠。
synchronized (lock) {
while (true) {
//調(diào)用本地方法
sleep(lock, millis, nanos);
long now = System.nanoTime();
long elapsed = now - start;
if (elapsed >= duration) {
break;
}
duration -= elapsed;
start = now;
millis = duration / NANOS_PER_MILLI;
nanos = (int) (duration % NANOS_PER_MILLI);
}
}
}
由上面的源碼可以看出,我們最終調(diào)用的是sleep(long millis, int nanos)方法脱茉。在sleep方法中剪芥,是通過循環(huán)不斷判斷當(dāng)前時(shí)間跟起始時(shí)間的差值,直到這個(gè)值大于等于我們傳入的休眠時(shí)間琴许,則線程可以繼續(xù)工作税肪。在此期間,當(dāng)前線程為阻塞狀態(tài)。
yield()方法
線程執(zhí)行此方法的作用是暫停當(dāng)前正在執(zhí)行的線程益兄,使其他具有相同優(yōu)先級(jí)的線程獲得運(yùn)行的機(jī)會(huì)锻梳。但是在實(shí)際中,我們不能保證其功能可以完全實(shí)現(xiàn)净捅,因?yàn)閥ield是將線程從運(yùn)行狀態(tài)變?yōu)榭蛇\(yùn)行狀態(tài)疑枯,在這種情況下,當(dāng)前線程可能會(huì)被CPU再次選中蛔六。此方法為本地方法荆永,jdk中源碼如下:
public static native void yield();
join()方法
此方法為讓一個(gè)線程加入到另一個(gè)線程的后面,在前面的線程沒有結(jié)束的時(shí)候国章,后面的線程不被執(zhí)行具钥。調(diào)用此方法會(huì)導(dǎo)致線程棧發(fā)生變化,當(dāng)然捉腥,這些變化都是瞬時(shí)的氓拼。
//負(fù)責(zé)此線程的join / sleep / park操作的同步對(duì)象
private final Object lock = new Object();
public final void join() throws InterruptedException {
//調(diào)用join(long millis)方法
join(0);
}
public final void join(long millis, int nanos)
throws InterruptedException {
synchronized(lock) {
//判斷傳入的參數(shù)是否正確
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
//根據(jù)條件判斷millis是否要加1
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
//調(diào)用join(long millis)方法
join(millis);
}
}
//真正被調(diào)用的方法
public final void join(long millis) throws InterruptedException {
synchronized(lock) {
//返回當(dāng)前時(shí)間
long base = System.currentTimeMillis();
long now = 0;
//判斷傳入的參數(shù)是否正確
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//以下就是根據(jù)isAlive(線程是否存活)你画,調(diào)用wait方法的循環(huán)
if (millis == 0) {
while (isAlive()) {
lock.wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
lock.wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
}
wait()方法的作用:導(dǎo)致當(dāng)前線程等待抵碟,直到另一個(gè)線程調(diào)用此對(duì)象的notify()方法或notifyAll()方法或指定的等待時(shí)間已經(jīng)過去。
由源碼可知坏匪,我們可以自己設(shè)置等待時(shí)間拟逮。但是如果我們不設(shè)置等待時(shí)間或者設(shè)置的等待時(shí)間為0,則線程會(huì)永遠(yuǎn)等待适滓。直到被其join的線程結(jié)束后敦迄,會(huì)調(diào)用this.notifyAll方法,使其結(jié)束等待凭迹。
未捕獲異常處理器
UncaughtExceptionHandler:是在Java Thread類中定義的罚屋,當(dāng)Thread由于未捕獲的異常而突然終止時(shí)調(diào)用的處理程序接口。這個(gè)接口只有一個(gè)方法:
//當(dāng)給定線程由于給定的未捕獲異常而終止時(shí)調(diào)用的方法嗅绸。Java虛擬機(jī)將忽略此方法拋出的任何異常
void uncaughtException(Thread t, Throwable e);
當(dāng)一個(gè)線程由于未捕獲的異常而即將終止時(shí)脾猛,Java虛擬機(jī)將使用getUncaughtExceptionHandler向線程查詢其UncaughtExceptionHandler并將調(diào)用處理程序的uncaughtException方法,將線程和異常作為參數(shù)傳遞鱼鸠。如果某個(gè)線程沒有顯式設(shè)置其UncaughtExceptionHandler猛拴,則其ThreadGroup對(duì)象將充當(dāng)其UncaughtExceptionHandler。如果ThreadGroup對(duì)象沒有處理異常的特殊要求蚀狰,它可以將調(diào)用轉(zhuǎn)發(fā)到getDefaultUncaughtExceptionHandler默認(rèn)的未捕獲異常處理程序愉昆。我們可以用setUncaughtExceptionHandler方法為任何線程設(shè)置一個(gè)處理器。也可以用Thread類的靜態(tài)方法setDefaultUncaughtExceptionHandler為所有線程設(shè)置一個(gè)默認(rèn)的處理器麻蹋。我們可以通過實(shí)現(xiàn)Thread.UncaughtExceptionHandler接口并重寫其uncaughtException方法來自定義一個(gè)未捕獲異常處理器跛溉。
小結(jié)
本文主要是簡(jiǎn)單的介紹一下線程的相關(guān)概念,使大家對(duì)線程有基本的了解。本文中涉及到的鎖及介紹的相關(guān)方法倒谷,會(huì)在后期的分析中一一講解蛛蒙。