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