一文帶你讀懂Java中的線程?很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來(lái)學(xué)習(xí)下,希望你能有所收獲。
成都創(chuàng)新互聯(lián)是工信部頒發(fā)資質(zhì)IDC服務(wù)器商,為用戶提供優(yōu)質(zhì)的成都棕樹(shù)電信機(jī)房服務(wù)
在之前的章節(jié)中,我們都是假設(shè)程序中只有一條執(zhí)行流,程序從main方法的第一條語(yǔ)句逐條執(zhí)行直到結(jié)束。從本節(jié)開(kāi)始,我們討論并發(fā),在程序中創(chuàng)建線程來(lái)啟動(dòng)多條執(zhí)行流,并發(fā)和線程是一個(gè)復(fù)雜的話題,本節(jié),我們先來(lái)討論Java中線程的一些基本概念。
創(chuàng)建線程
線程表示一條單獨(dú)的執(zhí)行流,它有自己的程序執(zhí)行計(jì)數(shù)器,有自己的棧。下面,我們通過(guò)創(chuàng)建線程來(lái)對(duì)線程建立一個(gè)直觀感受,在Java中創(chuàng)建線程有兩種方式,一種是繼承Thread,另外一種是實(shí)現(xiàn)Runnable接口,我們先來(lái)看第一種。
繼承Thread
Java中java.lang.Thread這個(gè)類表示線程,一個(gè)類可以繼承Thread并重寫其run方法來(lái)實(shí)現(xiàn)一個(gè)線程,如下所示:
public class HelloThread extends Thread { @Override public void run() { System.out.println("hello"); } }
HelloThread這個(gè)類繼承了Thread,并重寫了run方法。run方法的方法簽名是固定的,public,沒(méi)有參數(shù),沒(méi)有返回值,不能拋出受檢異常。run方法類似于單線程程序中的main方法,線程從run方法的第一條語(yǔ)句開(kāi)始執(zhí)行直到結(jié)束。
定義了這個(gè)類不代表代碼就會(huì)開(kāi)始執(zhí)行,線程需要被啟動(dòng),啟動(dòng)需要先創(chuàng)建一個(gè)HelloThread對(duì)象,然后調(diào)用Thread的start方法,如下所示:
public static void main(String[] args) { Thread thread = new HelloThread(); thread.start(); }
我們?cè)趍ain方法中創(chuàng)建了一個(gè)線程對(duì)象,并調(diào)用了其start方法,調(diào)用start方法后,HelloThread的run方法就會(huì)開(kāi)始執(zhí)行,屏幕輸出:
hello
為什么調(diào)用的是start,執(zhí)行的卻是run方法呢?start表示啟動(dòng)該線程,使其成為一條單獨(dú)的執(zhí)行流,背后,操作系統(tǒng)會(huì)分配線程相關(guān)的資源,每個(gè)線程會(huì)有單獨(dú)的程序執(zhí)行計(jì)數(shù)器和棧,操作系統(tǒng)會(huì)把這個(gè)線程作為一個(gè)獨(dú)立的個(gè)體進(jìn)行調(diào)度,分配時(shí)間片讓它執(zhí)行,執(zhí)行的起點(diǎn)就是run方法。
如果不調(diào)用start,而直接調(diào)用run方法呢?屏幕的輸出并不會(huì)發(fā)生變化,但并不會(huì)啟動(dòng)一條單獨(dú)的執(zhí)行流,run方法的代碼依然是在main線程中執(zhí)行的,run方法只是main方法調(diào)用的一個(gè)普通方法。
怎么確認(rèn)代碼是在哪個(gè)線程中執(zhí)行的呢?Thread有一個(gè)靜態(tài)方法currentThread,返回當(dāng)前執(zhí)行的線程對(duì)象:
public static native Thread currentThread();
每個(gè)Thread都有一個(gè)id和name:
public long getId() public final String getName()
這樣,我們就可以判斷代碼是在哪個(gè)線程中執(zhí)行的,我們?cè)贖elloThead的run方法中加一些代碼:
@Override public void run() { System.out.println("thread name: "+ Thread.currentThread().getName()); System.out.println("hello"); }
如果在main方法中通過(guò)start方法啟動(dòng)線程,程序輸出為:
thread name: Thread-0 hello
如果在main方法中直接調(diào)用run方法,程序輸出為:
thread name: main hello
調(diào)用start后,就有了兩條執(zhí)行流,新的一條執(zhí)行run方法,舊的一條繼續(xù)執(zhí)行main方法,兩條執(zhí)行流并發(fā)執(zhí)行,操作系統(tǒng)負(fù)責(zé)調(diào)度,在單CPU的機(jī)器上,同一時(shí)刻只能有一個(gè)線程在執(zhí)行,在多CPU的機(jī)器上,同一時(shí)刻可以有多個(gè)線程同時(shí)執(zhí)行,但操作系統(tǒng)給我們屏蔽了這種差異,給程序員的感覺(jué)就是多個(gè)線程并發(fā)執(zhí)行,但哪條語(yǔ)句先執(zhí)行哪條后執(zhí)行是不一定的。當(dāng)所有線程都執(zhí)行完畢的時(shí)候,程序退出。
實(shí)現(xiàn)Runnable接口
通過(guò)繼承Thread來(lái)實(shí)現(xiàn)線程雖然比較簡(jiǎn)單,但我們知道,Java中只支持單繼承,每個(gè)類最多只能有一個(gè)父類,如果類已經(jīng)有父類了,就不能再繼承Thread,這時(shí),可以通過(guò)實(shí)現(xiàn)java.lang.Runnable接口來(lái)實(shí)現(xiàn)線程。
Runnable接口的定義很簡(jiǎn)單,只有一個(gè)run方法,如下所示:
public interface Runnable { public abstract void run(); }
一個(gè)類可以實(shí)現(xiàn)該接口,并實(shí)現(xiàn)run方法,如下所示:
public class HelloRunnable implements Runnable { @Override public void run() { System.out.println("hello"); } }
僅僅實(shí)現(xiàn)Runnable是不夠的,要啟動(dòng)線程,還是要?jiǎng)?chuàng)建一個(gè)Thread對(duì)象,但傳遞一個(gè)Runnable對(duì)象,如下所示:
public static void main(String[] args) { Thread helloThread = new Thread(new HelloRunnable()); helloThread.start(); }
無(wú)論是通過(guò)繼承Thead還是實(shí)現(xiàn)Runnable接口來(lái)實(shí)現(xiàn)線程,啟動(dòng)線程都是調(diào)用Thread對(duì)象的start方法。
線程的基本屬性和方法
id和name
前面我們提到,每個(gè)線程都有一個(gè)id和name,id是一個(gè)遞增的整數(shù),每創(chuàng)建一個(gè)線程就加一,name的默認(rèn)值是"Thread-"后跟一個(gè)編號(hào),name可以在Thread的構(gòu)造方法中進(jìn)行指定,也可以通過(guò)setName方法進(jìn)行設(shè)置,給Thread設(shè)置一個(gè)友好的名字,可以方便調(diào)試。
優(yōu)先級(jí)
線程有一個(gè)優(yōu)先級(jí)的概念,在Java中,優(yōu)先級(jí)從1到10,默認(rèn)為5,相關(guān)方法是:
public final void setPriority(int newPriority) public final int getPriority()
這個(gè)優(yōu)先級(jí)會(huì)被映射到操作系統(tǒng)中線程的優(yōu)先級(jí),不過(guò),因?yàn)椴僮飨到y(tǒng)各不相同,不一定都是10個(gè)優(yōu)先級(jí),Java中不同的優(yōu)先級(jí)可能會(huì)被映射到操作系統(tǒng)中相同的優(yōu)先級(jí),另外,優(yōu)先級(jí)對(duì)操作系統(tǒng)而言更多的是一種建議和提示,而非強(qiáng)制,簡(jiǎn)單的說(shuō),在編程中,不要過(guò)于依賴優(yōu)先級(jí)。
狀態(tài)
線程有一個(gè)狀態(tài)的概念,Thread有一個(gè)方法用于獲取線程的狀態(tài):
public State getState()
返回值類型為Thread.State,它是一個(gè)枚舉類型,有如下值:
public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }
關(guān)于這些狀態(tài),我們簡(jiǎn)單解釋下:
Thread還有一個(gè)方法,返回線程是否活著:
public final native boolean isAlive()
線程被啟動(dòng)后,run方法運(yùn)行結(jié)束前,返回值都是true。
是否daemo線程
Thread有一個(gè)是否daemo線程的屬性,相關(guān)方法是:
public final void setDaemon(boolean on) public final boolean isDaemon()
前面我們提到,啟動(dòng)線程會(huì)啟動(dòng)一條單獨(dú)的執(zhí)行流,整個(gè)程序只有在所有線程都結(jié)束的時(shí)候才退出,但daemo線程是例外,當(dāng)整個(gè)程序中剩下的都是daemo線程的時(shí)候,程序就會(huì)退出。
daemo線程有什么用呢?它一般是其他線程的輔助線程,在它輔助的主線程退出的時(shí)候,它就沒(méi)有存在的意義了。在我們運(yùn)行一個(gè)即使最簡(jiǎn)單的"hello world"類型的程序時(shí),實(shí)際上,Java也會(huì)創(chuàng)建多個(gè)線程,除了main線程外,至少還有一個(gè)負(fù)責(zé)垃圾回收的線程,這個(gè)線程就是daemo線程,在main線程結(jié)束的時(shí)候,垃圾回收線程也會(huì)退出。
sleep方法
Thread有一個(gè)靜態(tài)的sleep方法,調(diào)用該方法會(huì)讓當(dāng)前線程睡眠指定的時(shí)間,單位是毫秒:
public static native void sleep(long millis) throws InterruptedException;
睡眠期間,該線程會(huì)讓出CPU,但睡眠的時(shí)間不一定是確切的給定毫秒數(shù),可能有一定的偏差,偏差與系統(tǒng)定時(shí)器和操作系統(tǒng)調(diào)度器的準(zhǔn)確度和精度有關(guān)。
睡眠期間,線程可以被中斷,如果被中斷,sleep會(huì)拋出InterruptedException,關(guān)于中斷以及中斷處理,我們后續(xù)章節(jié)再介紹。
yield方法
Thread還有一個(gè)讓出CPU的方法:
public static native void yield();
這也是一個(gè)靜態(tài)方法,調(diào)用該方法,是告訴操作系統(tǒng)的調(diào)度器,我現(xiàn)在不著急占用CPU,你可以先讓其他線程運(yùn)行。不過(guò),這對(duì)調(diào)度器也僅僅是建議,調(diào)度器如何處理是不一定的,它可能完全忽略該調(diào)用。
join方法
在前面HelloThread的例子中,HelloThread沒(méi)執(zhí)行完,main線程可能就執(zhí)行完了,Thread有一個(gè)join方法,可以讓調(diào)用join的線程等待該線程結(jié)束,join方法的聲明為:
public final void join() throws InterruptedException
在等待線程結(jié)束的過(guò)程中,這個(gè)等待可能被中斷,如果被中斷,會(huì)拋出InterruptedException。
join方法還有一個(gè)變體,可以限定等待的最長(zhǎng)時(shí)間,單位為毫秒,如果為0,表示無(wú)期限等待:
public final synchronized void join(long millis) throws InterruptedException
在前面的HelloThread示例中,如果希望main線程在子線程結(jié)束后再退出,main方法可以改為:
public static void main(String[] args) throws InterruptedException { Thread thread = new HelloThread(); thread.start(); thread.join(); }
過(guò)時(shí)方法
Thread類中還有一些看上去可以控制線程生命周期的方法,如:
public final void stop() public final void suspend() public final void resume()
這些方法因?yàn)楦鞣N原因已被標(biāo)記為了過(guò)時(shí),我們不應(yīng)該在程序中使用它們。
共享內(nèi)存及問(wèn)題
共享內(nèi)存
前面我們提到,每個(gè)線程表示一條單獨(dú)的執(zhí)行流,有自己的程序計(jì)數(shù)器,有自己的棧,但線程之間可以共享內(nèi)存,它們可以訪問(wèn)和操作相同的對(duì)象。我們看個(gè)例子,代碼如下:
public class ShareMemoryDemo { private static int shared = 0; private static void incrShared(){ shared ++; } static class ChildThread extends Thread { Listlist; public ChildThread(List list) { this.list = list; } @Override public void run() { incrShared(); list.add(Thread.currentThread().getName()); } } public static void main(String[] args) throws InterruptedException { List list = new ArrayList (); Thread t1 = new ChildThread(list); Thread t2 = new ChildThread(list); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(shared); System.out.println(list); } }
在代碼中,定義了一個(gè)靜態(tài)變量shared和靜態(tài)內(nèi)部類ChildThread,在main方法中,創(chuàng)建并啟動(dòng)了兩個(gè)ChildThread對(duì)象,傳遞了相同的list對(duì)象,ChildThread的run方法訪問(wèn)了共享的變量shared和list,main方法最后輸出了共享的shared和list的值,大部分情況下,會(huì)輸出期望的值:
[Thread-0, Thread-1]
通過(guò)這個(gè)例子,我們想強(qiáng)調(diào)說(shuō)明執(zhí)行流、內(nèi)存和程序代碼之間的關(guān)系。
該例中有三條執(zhí)行流,一條執(zhí)行main方法,另外兩條執(zhí)行ChildThread的run方法。
當(dāng)多條執(zhí)行流可以操作相同的變量時(shí),可能會(huì)出現(xiàn)一些意料之外的結(jié)果,我們來(lái)看下。
競(jìng)態(tài)條件
所謂競(jìng)態(tài)條件(race condition)是指,當(dāng)多個(gè)線程訪問(wèn)和操作同一個(gè)對(duì)象時(shí),最終執(zhí)行結(jié)果與執(zhí)行時(shí)序有關(guān),可能正確也可能不正確,我們看一個(gè)例子:
public class CounterThread extends Thread { private static int counter = 0; @Override public void run() { try { Thread.sleep((int)(Math.random()*100)); } catch (InterruptedException e) { } counter ++; } public static void main(String[] args) throws InterruptedException { int num = 1000; Thread[] threads = new Thread[num]; for(int i=0; i
這段代碼容易理解,有一個(gè)共享靜態(tài)變量counter,初始值為0,在main方法中創(chuàng)建了1000個(gè)線程,每個(gè)線程就是隨機(jī)睡一會(huì),然后對(duì)counter加1,main線程等待所有線程結(jié)束后輸出counter的值。
期望的結(jié)果是1000,但實(shí)際執(zhí)行,發(fā)現(xiàn)每次輸出的結(jié)果都不一樣,一般都不是1000,經(jīng)常是900多。為什么會(huì)這樣呢?因?yàn)?strong>counter++這個(gè)操作不是原子操作,它分為三個(gè)步驟:
兩個(gè)線程可能同時(shí)執(zhí)行第一步,取到了相同的counter值,比如都取到了100,第一個(gè)線程執(zhí)行完后counter變?yōu)?01,而第二個(gè)線程執(zhí)行完后還是101,最終的結(jié)果就與期望不符。
怎么解決這個(gè)問(wèn)題呢?有多種方法:
關(guān)于這些方法,我們?cè)诤罄m(xù)章節(jié)再介紹。
內(nèi)存可見(jiàn)性
多個(gè)線程可以共享訪問(wèn)和操作相同的變量,但一個(gè)線程對(duì)一個(gè)共享變量的修改,另一個(gè)線程不一定馬上就能看到,甚至永遠(yuǎn)也看不到,這可能有悖直覺(jué),我們來(lái)看一個(gè)例子。
public class VisibilityDemo { private static boolean shutdown = false; static class HelloThread extends Thread { @Override public void run() { while(!shutdown){ // do nothing } System.out.println("exit hello"); } } public static void main(String[] args) throws InterruptedException { new HelloThread().start(); Thread.sleep(1000); shutdown = true; System.out.println("exit main"); } }
在這個(gè)程序中,有一個(gè)共享的boolean變量shutdown,初始為false,HelloThread在shutdown不為true的情況下一直死循環(huán),當(dāng)shutdown為true時(shí)退出并輸出"exit hello",main線程啟動(dòng)HelloThread后睡了一會(huì),然后設(shè)置shutdown為true,最后輸出"exit main"。
期望的結(jié)果是兩個(gè)線程都退出,但實(shí)際執(zhí)行,很可能會(huì)發(fā)現(xiàn)HelloThread永遠(yuǎn)都不會(huì)退出,也就是說(shuō),在HelloThread執(zhí)行流看來(lái),shutdown永遠(yuǎn)為false,即使main線程已經(jīng)更改為了true。
這是怎么回事呢?這就是內(nèi)存可見(jiàn)性問(wèn)題。在計(jì)算機(jī)系統(tǒng)中,除了內(nèi)存,數(shù)據(jù)還會(huì)被緩存在CPU的寄存器以及各級(jí)緩存中,當(dāng)訪問(wèn)一個(gè)變量時(shí),可能直接從寄存器或CPU緩存中獲取,而不一定到內(nèi)存中去取,當(dāng)修改一個(gè)變量時(shí),也可能是先寫到緩存中,而稍后才會(huì)同步更新到內(nèi)存中。在單線程的程序中,這一般不是個(gè)問(wèn)題,但在多線程的程序中,尤其是在有多CPU的情況下,這就是個(gè)嚴(yán)重的問(wèn)題。一個(gè)線程對(duì)內(nèi)存的修改,另一個(gè)線程看不到,一是修改沒(méi)有及時(shí)同步到內(nèi)存,二是另一個(gè)線程根本就沒(méi)從內(nèi)存讀。
怎么解決這個(gè)問(wèn)題呢?有多種方法:
關(guān)于這些方法,我們?cè)诤罄m(xù)章節(jié)再介紹。
線程的優(yōu)點(diǎn)及成本
優(yōu)點(diǎn)
為什么要?jiǎng)?chuàng)建單獨(dú)的執(zhí)行流?或者說(shuō)線程有什么優(yōu)點(diǎn)呢?至少有以下幾點(diǎn):
成本
關(guān)于線程,我們需要知道,它是有成本的。創(chuàng)建線程需要消耗操作系統(tǒng)的資源,操作系統(tǒng)會(huì)為每個(gè)線程創(chuàng)建必要的數(shù)據(jù)結(jié)構(gòu)、棧、程序計(jì)數(shù)器等,創(chuàng)建也需要一定的時(shí)間。
此外,線程調(diào)度和切換也是有成本的,當(dāng)有當(dāng)量可運(yùn)行線程的時(shí)候,操作系統(tǒng)會(huì)忙于調(diào)度,為一個(gè)線程分配一段時(shí)間,執(zhí)行完后,再讓另一個(gè)線程執(zhí)行,一個(gè)線程被切換出去后,操作系統(tǒng)需要保存它的當(dāng)前上下文狀態(tài)到內(nèi)存,上下文狀態(tài)包括當(dāng)前CPU寄存器的值、程序計(jì)數(shù)器的值等,而一個(gè)線程被切換回來(lái)后,操作系統(tǒng)需要恢復(fù)它原來(lái)的上下文狀態(tài),整個(gè)過(guò)程被稱為上下文切換,這個(gè)切換不僅耗時(shí),而且使CPU中的很多緩存失效,是有成本的。
當(dāng)然,這些成本是相對(duì)而言的,如果線程中實(shí)際執(zhí)行的事情比較多,這些成本是可以接受的,但如果只是執(zhí)行本節(jié)示例中的counter++,那相對(duì)成本就太高了。
另外,如果執(zhí)行的任務(wù)都是CPU密集型的,即主要消耗的都是CPU,那創(chuàng)建超過(guò)CPU數(shù)量的線程就是沒(méi)有必要的,并不會(huì)加快程序的執(zhí)行。
看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝您對(duì)創(chuàng)新互聯(lián)的支持。