Java多線程基礎(chǔ)

線程與進(jìn)程

線程:進(jìn)程中負(fù)責(zé)程序執(zhí)行的執(zhí)行單元

線程本身依靠程序進(jìn)行運(yùn)行
線程是程序中的順序控制流暗赶,只能使用分配給程序的資源和環(huán)境

進(jìn)程:執(zhí)行中的程序

一個(gè)進(jìn)程至少包含一個(gè)線程

單線程:程序中只存在一個(gè)線程尾菇,實(shí)際上主方法就是一個(gè)主線程

多線程:在一個(gè)程序中運(yùn)行多個(gè)任務(wù)

目的是更好地使用CPU資源

線程的實(shí)現(xiàn)

繼承Thread類

java.lang包中定義, 繼承Thread類必須重寫run()方法

class MyThread extends Thread{
    private static int num = 0;

    public MyThread(){
        num++;
    }

    @Override
    public void run() {
        System.out.println("主動(dòng)創(chuàng)建的第"+num+"個(gè)線程");
    }
}

創(chuàng)建好了自己的線程類之后垫桂,就可以創(chuàng)建線程對象了胧弛,然后通過start()方法去啟動(dòng)線程妇斤。注意橙困,不是調(diào)用run()方法啟動(dòng)線程驾霜,run方法中只是定義需要執(zhí)行的任務(wù),如果調(diào)用run方法店枣,即相當(dāng)于在主線程中執(zhí)行run方法速警,跟普通的方法調(diào)用沒有任何區(qū)別叹誉,此時(shí)并不會(huì)創(chuàng)建一個(gè)新的線程來執(zhí)行定義的任務(wù)。

public class Test {
    public static void main(String[] args)  {
        MyThread thread = new MyThread();
        thread.start();
    }
}
class MyThread extends Thread{
    private static int num = 0;
    public MyThread(){
        num++;
    }
    @Override
    public void run() {
        System.out.println("主動(dòng)創(chuàng)建的第"+num+"個(gè)線程");
    }
}

在上面代碼中闷旧,通過調(diào)用start()方法长豁,就會(huì)創(chuàng)建一個(gè)新的線程了。為了分清start()方法調(diào)用和run()方法調(diào)用的區(qū)別忙灼,請看下面一個(gè)例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主線程ID:"+Thread.currentThread().getId());
        MyThread thread1 = new MyThread("thread1");
        thread1.start();
        MyThread thread2 = new MyThread("thread2");
        thread2.run();
    }
}

class MyThread extends Thread{
    private String name;

    public MyThread(String name){
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("name:"+name+" 子線程ID:"+Thread.currentThread().getId());
    }
}

運(yùn)行結(jié)果:


image.png

從輸出結(jié)果可以得出以下結(jié)論:

  • 1)thread1和thread2的線程ID不同匠襟,thread2和主線程ID相同,說明通過run方法調(diào)用并不會(huì)創(chuàng)建新的線程该园,而是在主線程中直接運(yùn)行run方法酸舍,跟普通的方法調(diào)用沒有任何區(qū)別

  • 2)雖然thread1的start方法調(diào)用在thread2的run方法前面調(diào)用里初,但是先輸出的是thread2的run方法調(diào)用的相關(guān)信息啃勉,說明新線程創(chuàng)建的過程不會(huì)阻塞主線程的后續(xù)執(zhí)行

實(shí)現(xiàn)Runnable接口

在Java中創(chuàng)建線程除了繼承Thread類之外双妨,還可以通過實(shí)現(xiàn)Runnable接口來實(shí)現(xiàn)類似的功能淮阐。實(shí)現(xiàn)Runnable接口必須重寫其run方法。
下面是一個(gè)例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主線程ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }

    @Override
    public void run() {
        System.out.println("子線程ID:"+Thread.currentThread().getId());
    }
}

Runnable的中文意思是“任務(wù)”刁品,顧名思義泣特,通過實(shí)現(xiàn)Runnable接口,我們定義了一個(gè)子任務(wù)哑诊,然后將子任務(wù)交由Thread去執(zhí)行群扶。注意,這種方式必須將Runnable作為Thread類的參數(shù)镀裤,然后通過Thread的start方法來創(chuàng)建一個(gè)新線程來執(zhí)行該子任務(wù)竞阐。如果調(diào)用Runnable的run方法的話,是不會(huì)創(chuàng)建新線程的暑劝,這根普通的方法調(diào)用沒有任何區(qū)別骆莹。

事實(shí)上,查看Thread類的實(shí)現(xiàn)源代碼會(huì)發(fā)現(xiàn)Thread類是實(shí)現(xiàn)了Runnable接口的担猛。

在Java中幕垦,這2種方式都可以用來創(chuàng)建線程去執(zhí)行子任務(wù),具體選擇哪一種方式要看自己的需求傅联。直接繼承Thread類的話先改,可能比實(shí)現(xiàn)Runnable接口看起來更加簡潔,但是由于Java只允許單繼承蒸走,所以如果自定義類需要繼承其他類仇奶,則只能選擇實(shí)現(xiàn)Runnable接口。

使用ExecutorService比驻、Callable该溯、Future實(shí)現(xiàn)有返回結(jié)果的多線程

ExecutorService岛抄、Callable、Future這個(gè)對象實(shí)際上都是屬于Executor框架中的功能類狈茉。想要詳細(xì)了解Executor框架的可以訪問http://www.javaeye.com/topic/366591 夫椭,這里面對該框架做了很詳細(xì)的解釋。返回結(jié)果的線程是在JDK1.5中引入的新特征氯庆,確實(shí)很實(shí)用蹭秋,有了這種特征我就不需要再為了得到返回值而大費(fèi)周折了,而且即便實(shí)現(xiàn)了也可能漏洞百出点晴。

可返回值的任務(wù)必須實(shí)現(xiàn)Callable接口感凤,類似的悯周,無返回值的任務(wù)必須Runnable接口粒督。執(zhí)行Callable任務(wù)后,可以獲取一個(gè)Future的對象禽翼,在該對象上調(diào)用get就可以獲取到Callable任務(wù)返回的Object了屠橄,再結(jié)合線程池接口ExecutorService就可以實(shí)現(xiàn)傳說中有返回結(jié)果的多線程了。下面提供了一個(gè)完整的有返回結(jié)果的多線程測試?yán)尤虻玻贘DK1.5下驗(yàn)證過沒問題可以直接使用锐墙。代碼如下:

/**
* 有返回值的線程 
*/  
@SuppressWarnings("unchecked")  
public class Test {  
public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序開始運(yùn)行----");  
   Date date1 = new Date();  
  
   int taskSize = 5;  
   // 創(chuàng)建一個(gè)線程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 創(chuàng)建多個(gè)有返回值的任務(wù)  
   List<Future> list = new ArrayList<Future>();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 執(zhí)行任務(wù)并獲取Future對象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 關(guān)閉線程池  
   pool.shutdown();  
  
   // 獲取所有并發(fā)任務(wù)的運(yùn)行結(jié)果  
   for (Future f : list) {  
    // 從Future對象上獲取任務(wù)的返回值,并輸出到控制臺(tái)  
    System.out.println(">>>" + f.get().toString());  
   }  
  
   Date date2 = new Date();  
   System.out.println("----程序結(jié)束運(yùn)行----长酗,程序運(yùn)行時(shí)間【"  
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  
  
class MyCallable implements Callable<Object> {  
private String taskNum;  
  
MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  
  
public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任務(wù)啟動(dòng)");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任務(wù)終止");  
   return taskNum + "任務(wù)返回運(yùn)行結(jié)果,當(dāng)前任務(wù)時(shí)間【" + time + "毫秒】";  
}
}

代碼說明:
上述代碼中Executors類溪北,提供了一系列工廠方法用于創(chuàng)先線程池,返回的線程池都實(shí)現(xiàn)了ExecutorService接口夺脾。

public static ExecutorService newFixedThreadPool(int nThreads)
創(chuàng)建固定數(shù)目線程的線程池之拨。

public static ExecutorService newCachedThreadPool()
創(chuàng)建一個(gè)可緩存的線程池,調(diào)用execute 將重用以前構(gòu)造的線程(如果線程可用)咧叭。如果現(xiàn)有線程沒有可用的蚀乔,則創(chuàng)建一個(gè)新線程并添加到池中。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程菲茬。

public static ExecutorService newSingleThreadExecutor()
創(chuàng)建一個(gè)單線程化的Executor吉挣。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
創(chuàng)建一個(gè)支持定時(shí)及周期性的任務(wù)執(zhí)行的線程池,多數(shù)情況下可用來替代Timer類婉弹。

ExecutoreService提供了submit()方法睬魂,傳遞一個(gè)Callable,或Runnable镀赌,返回Future氯哮。如果Executor后臺(tái)線程池還沒有完成Callable的計(jì)算,這調(diào)用返回Future對象的get()方法佩脊,會(huì)阻塞直到計(jì)算完成蛙粘。

線程的狀態(tài)

在正式學(xué)習(xí)Thread類中的具體方法之前垫卤,我們先來了解一下線程有哪些狀態(tài),這個(gè)將會(huì)有助于后面對Thread類中的方法的理解出牧。

  • 創(chuàng)建(new)狀態(tài): 準(zhǔn)備好了一個(gè)多線程的對象

  • 就緒(runnable)狀態(tài): 調(diào)用了start()方法, 等待CPU進(jìn)行調(diào)度

  • 運(yùn)行(running)狀態(tài): 執(zhí)行run()方法

  • 阻塞(blocked)狀態(tài): 暫時(shí)停止執(zhí)行, 可能將資源交給其它線程使用

  • 終止(dead)狀態(tài): 線程銷毀

當(dāng)需要新起一個(gè)線程來執(zhí)行某個(gè)子任務(wù)時(shí)穴肘,就創(chuàng)建了一個(gè)線程。但是線程創(chuàng)建之后舔痕,不會(huì)立即進(jìn)入就緒狀態(tài)评抚,因?yàn)榫€程的運(yùn)行需要一些條件(比如內(nèi)存資源,在前面的JVM內(nèi)存區(qū)域劃分一篇博文中知道程序計(jì)數(shù)器伯复、Java棧慨代、本地方法棧都是線程私有的,所以需要為線程分配一定的內(nèi)存空間)啸如,只有線程運(yùn)行需要的所有條件滿足了侍匙,才進(jìn)入就緒狀態(tài)。

當(dāng)線程進(jìn)入就緒狀態(tài)后叮雳,不代表立刻就能獲取CPU執(zhí)行時(shí)間想暗,也許此時(shí)CPU正在執(zhí)行其他的事情,因此它要等待帘不。當(dāng)?shù)玫紺PU執(zhí)行時(shí)間之后说莫,線程便真正進(jìn)入運(yùn)行狀態(tài)。

線程在運(yùn)行狀態(tài)過程中寞焙,可能有多個(gè)原因?qū)е庐?dāng)前線程不繼續(xù)運(yùn)行下去储狭,比如用戶主動(dòng)讓線程睡眠(睡眠一定的時(shí)間之后再重新執(zhí)行)、用戶主動(dòng)讓線程等待捣郊,或者被同步塊給阻塞辽狈,此時(shí)就對應(yīng)著多個(gè)狀態(tài):time waiting(睡眠或等待一定的事件)、waiting(等待被喚醒)模她、blocked(阻塞)稻艰。

當(dāng)由于突然中斷或者子任務(wù)執(zhí)行完畢,線程就會(huì)被消亡侈净。

下面這副圖描述了線程從創(chuàng)建到消亡之間的狀態(tài):


image.png

在有些教程上將blocked尊勿、waiting、time waiting統(tǒng)稱為阻塞狀態(tài)畜侦,這個(gè)也是可以的元扔,只不過這里我想將線程的狀態(tài)和Java中的方法調(diào)用聯(lián)系起來,所以將waiting和time waiting兩個(gè)狀態(tài)分離出來旋膳。

注:sleep和wait的區(qū)別:

  • sleepThread類的方法,waitObject類中定義的方法.

  • Thread.sleep不會(huì)導(dǎo)致鎖行為的改變, 如果當(dāng)前線程是擁有鎖的, 那么Thread.sleep不會(huì)讓線程釋放鎖.

  • Thread.sleepObject.wait都會(huì)暫停當(dāng)前的線程. OS會(huì)將執(zhí)行時(shí)間分配給其它線程. 區(qū)別是, 調(diào)用wait后, 需要?jiǎng)e的線程執(zhí)行notify/notifyAll才能夠重新獲得CPU執(zhí)行時(shí)間.

上下文切換

對于單核CPU來說(對于多核CPU澎语,此處就理解為一個(gè)核),CPU在一個(gè)時(shí)刻只能運(yùn)行一個(gè)線程,當(dāng)在運(yùn)行一個(gè)線程的過程中轉(zhuǎn)去運(yùn)行另外一個(gè)線程擅羞,這個(gè)叫做線程上下文切換(對于進(jìn)程也是類似)尸变。

由于可能當(dāng)前線程的任務(wù)并沒有執(zhí)行完畢,所以在切換時(shí)需要保存線程的運(yùn)行狀態(tài)减俏,以便下次重新切換回來時(shí)能夠繼續(xù)切換之前的狀態(tài)運(yùn)行召烂。舉個(gè)簡單的例子:比如一個(gè)線程A正在讀取一個(gè)文件的內(nèi)容,正讀到文件的一半娃承,此時(shí)需要暫停線程A奏夫,轉(zhuǎn)去執(zhí)行線程B,當(dāng)再次切換回來執(zhí)行線程A的時(shí)候历筝,我們不希望線程A又從文件的開頭來讀取酗昼。

因此需要記錄線程A的運(yùn)行狀態(tài),那么會(huì)記錄哪些數(shù)據(jù)呢梳猪?因?yàn)橄麓位謴?fù)時(shí)需要知道在這之前當(dāng)前線程已經(jīng)執(zhí)行到哪條指令了麻削,所以需要記錄程序計(jì)數(shù)器的值,另外比如說線程正在進(jìn)行某個(gè)計(jì)算的時(shí)候被掛起了舔示,那么下次繼續(xù)執(zhí)行的時(shí)候需要知道之前掛起時(shí)變量的值時(shí)多少碟婆,因此需要記錄CPU寄存器的狀態(tài)电抚。所以一般來說惕稻,線程上下文切換過程中會(huì)記錄程序計(jì)數(shù)器、CPU寄存器狀態(tài)等數(shù)據(jù)蝙叛。

說簡單點(diǎn)的:對于線程的上下文切換實(shí)際上就是 存儲(chǔ)和恢復(fù)CPU狀態(tài)的過程俺祠,它使得線程執(zhí)行能夠從中斷點(diǎn)恢復(fù)執(zhí)行。

雖然多線程可以使得任務(wù)執(zhí)行的效率得到提升借帘,但是由于在線程切換時(shí)同樣會(huì)帶來一定的開銷代價(jià)蜘渣,并且多個(gè)線程會(huì)導(dǎo)致系統(tǒng)資源占用的增加,所以在進(jìn)行多線程編程時(shí)要注意這些因素肺然。

在Android平臺(tái)里蔫缸,UI線程與其子線程之分工是很明確的:

  • 子線程負(fù)責(zé)執(zhí)行費(fèi)時(shí)的工作。
  • 主線程負(fù)責(zé)UI的操作或事件际起;子線程不可以插手有關(guān)UI的事拾碌。
    在同一進(jìn)程里的主、子線程之間可以透過MessageQueue來互相溝通街望。當(dāng)一個(gè)新進(jìn)程新誕生時(shí)校翔,會(huì)同時(shí)誕生一個(gè)主線程,也替主線程建立一個(gè)MessageQueue灾前,以及負(fù)責(zé)管理MessageQueue的Looper類別之對象防症。子線程可以透過Handler對象而將Message對象丟(Post)到主線程的MessageQueue里,而主線程的Looper對象就會(huì)接到這個(gè)Message對象,并依據(jù)其內(nèi)容而呼叫適當(dāng)?shù)暮瘮?shù)來處理蔫敲。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饲嗽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子奈嘿,更是在濱河造成了極大的恐慌喝噪,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件指么,死亡現(xiàn)場離奇詭異酝惧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)伯诬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門晚唇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盗似,你說我怎么就攤上這事哩陕。” “怎么了赫舒?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵悍及,是天一觀的道長。 經(jīng)常有香客問我接癌,道長心赶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任缺猛,我火速辦了婚禮缨叫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荔燎。我一直安慰自己耻姥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布有咨。 她就那樣靜靜地躺著琐簇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪座享。 梳的紋絲不亂的頭發(fā)上婉商,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機(jī)與錄音征讲,去河邊找鬼据某。 笑死,一個(gè)胖子當(dāng)著我的面吹牛诗箍,可吹牛的內(nèi)容都是我干的癣籽。 我是一名探鬼主播挽唉,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼筷狼!你這毒婦竟也來了瓶籽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤埂材,失蹤者是張志新(化名)和其女友劉穎塑顺,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俏险,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡严拒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了竖独。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裤唠。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖莹痢,靈堂內(nèi)的尸體忽然破棺而出种蘸,到底是詐尸還是另有隱情,我是刑警寧澤竞膳,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布航瞭,位于F島的核電站,受9級特大地震影響坦辟,放射性物質(zhì)發(fā)生泄漏刊侯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一长窄、第九天 我趴在偏房一處隱蔽的房頂上張望滔吠。 院中可真熱鬧,春花似錦挠日、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至椅贱,卻和暖如春懂算,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背庇麦。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工计技, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人山橄。 一個(gè)月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓垮媒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子睡雇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容

  • 前言 多線程并發(fā)編程是Java編程中重要的一塊內(nèi)容萌衬,也是面試重點(diǎn)覆蓋區(qū)域,所以學(xué)好多線程并發(fā)編程對我們來說極其重要...
    嘟爺MD閱讀 7,309評論 21 272
  • 寫在前面的話: 這篇博客是我從這里“轉(zhuǎn)載”的它抱,為什么轉(zhuǎn)載兩個(gè)字加“”呢秕豫?因?yàn)檫@絕不是簡單的復(fù)制粘貼,我花了五六個(gè)小...
    SmartSean閱讀 4,717評論 12 45
  • 為什么使用多線程 可以最大限度地利用CPU的空閑時(shí)間來處理其它任務(wù)观蓄。異步處理不同的任務(wù)混移,提高任務(wù)處理效率。 線程的...
    零度沸騰_yjz閱讀 367評論 0 4
  • 線程的簡介 幾乎每種操作系統(tǒng)都支持進(jìn)程的概念侮穿。進(jìn)程就是在某種程度上相互隔離的沫屡、獨(dú)立運(yùn)行的程序。線程化是允許多個(gè)活動(dòng)...
    小人物灌籃閱讀 676評論 2 4
  • 小南是個(gè)嬌小而漂亮的女人撮珠。18歲那年高考后沮脖,與本區(qū)的男孩談了一次轟轟烈烈的戀愛。朋友的不看好芯急,父母的不愿意勺届,當(dāng)大學(xué)...
    陳偉利閱讀 464評論 0 0