Java多線程基礎總結

來源:互聯網
上載者:User

標籤:java   thread   多線程基礎   

背景

   Java採用多線程方式實現並行計算,當然並行計算也可以採用多進程方式實現,但是進程切換耗費比較高。而且進程間是隔離的,處理序間通訊機制比較麻煩,最後JVM本身在作業系統中就一個進程,由它再啟動一個進程不太合適,所以Java採用多線程方式實現並行計算。

   Java從誕生之初,多線程就圍繞的是Runnable介面和Thread類展開的。它的底層採用的是c的p線程方式,而且由於多線程的複雜性,p線程的很多概念知識被延伸到了Java層面,這對Java開發人員來說算是一個不幸的訊息。但是由於多線程的複雜性,這種延伸不得不有。


1.執行個體化和啟動 1.1傳統啟動方式 Runnable介面

   Runnable介面是Java被設計線上程體中運行某項任務的介面。通過實現Runnable介面的run方法,在run方法中執行線程代碼,實現某項任務。《Java編程思想》的作者曾在書中提到認為Runnable這個介面的命名很不準確,叫Task比較合適。而實現了該介面的類,表示該類可以並行的運行run方法,也就是run的語句執行並不是連續的,很可能在t1時間執行第一條,t3時間執行第二條。這裡面也就帶來了原子操作、原語、同步、互斥、協同等問題。


Thread類

   Thread是Java中的線程類,通過執行個體化Thread和調用start方法可以啟動線程。Thread是java.lang包下面的一個類,它本身實現了Runnable介面。所以線程的第二種執行個體化和啟動寫法可以繼承Thread類,並重寫Run方法,Threa類的run()方法的源碼如下:

public void run() {         if(target != null) {             target.run();         }}

   Thread以後很多重載的構造方法。可以指定線程的名字,Runnable對象,所屬線程組等資訊。一般只指定Runnable對象就夠了。Java線程的基本用法基本上就是對Thread類裡面一些方法的用法和注意實現的介紹。

 

2.JDK1.5 後新的執行個體化和啟動方法

   JDK 1.5是Java比較重要的一個版本,有很多改進和提高,其中JDK 1.5就有志於改進Java線程的一些內容,並因此引入了java.util.concurrent包。通過該包下面的Executors及相關類,可以獲得一個Java標準的線程池,而不採用其他的線程架構。JDK 1.5之後Java目標盡量減少直接操作Thread類,所以用concurrent包後很多時候我們常見到的Thread方法調用,Synchronized關鍵字都可以省略。


2.1 Executor相關

   Executors有三個主要的static方法,這三個方法採用策略模式,封裝了線程池的管理和操作實現,統一返回ExecutorService介面實現對象。

newCachedThreadPool()該方法會建立一個可根據需要建立新線程的線程池,但是在以前構造的線程可用時將重用它們。對於執行很多短期非同步任務的程式而言,這些線程池通常可提高程式效能。 newFixedThreadPool()建立一個可重用固定線程數的線程池,以共用的LinkedBlockQueue方式來運行這些線程。 newSingleThreadExecutor()建立一個使用單個 worker 線程的 Executor,以LinkedBlockQueue方式來運行該線程。

用法:

ExecutorService exec = Executors.newCachedThreadPool();exec.execute(new MyThread()); exec.shutdown();


   調用execute可以將runnable對象加入到線程隊列中,executorService的根據自身的實現啟動線程運行任務。shutdown方法執行後表示線程池不再接受新的任務。

  

2.2 ThreadFactory

   上面執行個體化線程池的時候,可以傳入一個實現了ThreadFactory介面的對象來自訂線程屬性,如大量建立守護線程。ThreadFactory這種工程模式,表明Java線程的設計其實就是完成某些並發任務,而Executor可以執行一組相似的任務,所以這些Thread可以通過一個工廠產出。

ExecutorService exec =Executors.newCachedThreadPool(new ThreadFactory(){         ThreadnewThread(Runnable r){         Thread t = new Thread(r);         t.setDaemon(true);}});for(int i=0;i++;i<5)exec.execute(new MyThread()); exec.shutdown();

 

2.3帶返回值的任務:

   多線程(並發計算)的一個好處是,我們可以把耗時的計算放到後台,這樣就可以建立有響應不會假死的UI介面。Runnable介面的run方法為void方法,不帶有任何返回值,對於一些計算需求不是很方便。JDK 1.5之後,可以實現帶有返回值的Thread。

   想要讓任務帶有返回值,那麼我們的類就要實現Callable<V>介面,這是一個泛型介面,可以認為這個介面是Runnable的一個平級兄弟介面。只不過Runnable介面的run方法不能拋出受檢查的異常,不帶有返回值,但是這個介面可以帶有返回值,可以跑出異常。該介面規定了一個泛型方法:

V call() throws Exception;

   可以在這個方法中拋出受檢查的異常和返回值。返回值被Future<T>對象包圍,這麼做可以保證我們在多線程環境下擷取到計算完畢的值。因為如果直接返回一個變數的地址,那麼我們無法確定這個變數是否被計算完畢或者說call方法是否執行完畢,通過Future可以由JVM來幫我們確保這項工作。並且Future還有更深的用法,比如中斷線程池中的單個線程。

 

3.Thread類的一些其他動作

   Thread類還提供了Sleep、setPriority、setDaemon、join等操作,這些種操作都是針對線程的,因此他們都屬於Thread裡面的方法。而且這些操作不會涉及到線程鎖,因此他們的操作一般都是安全的。

 

3.1休眠Sleep

   Thread類的靜態方法,讓線程休眠一段時間,是一個native方法,單位是毫秒。會拋出InterruptedException異常,需要捕捉,該異常在Thread中斷中再解釋。比如一般我們都會在Demo中通過Sleep方法類比一些耗時操作,讓線程等待一段時間,讓Thread慢下來以便重現一些現象。

 

public class A extends Thread{        public void run(){     try {<span style="white-space:pre"></span>TimeUnit.SECONDS.sleep(2);     } catch (InterruptedException e) {     }    }  }

3.2設定優先權,setPriority

   Thread類的對象方法,設定線程優先順序,優先順序越高的可以被儘可能多的輪詢。JDK規定了10個優先順序,但是JVM於多數作業系統都不能很好的映射,而且要注意優先順序只是一種建議,並不是低優先順序的一定在高優先順序的Thread完成後才能執行。

   線程的優先順序最終是由JVM映射到OS層面實現的,但是不同OS的優先順序數不一樣,少的比如Windows也就7個而且不固定,Solairs有2的31次方個優先順序。所以我們在設定的時候一般設定為Thread類的三個常量MAX_PRIORITY/NORM_PRIORITY/MIN_PRIORITY三個層級就夠了。

 

<span style="white-space:pre"></span>public class B implements Runnable {<span style="white-space:pre"></span>public static void main(String[] args) {<span style="white-space:pre"></span>ExecutorService exec = Executors<span style="white-space:pre"></span>.newCachedThreadPool(new DaemonThreadFactory());<span style="white-space:pre"></span>for (int i = 0; i < 10; i++)<span style="white-space:pre"></span>exec.execute(new DaemonFromFactory());<span style="white-space:pre"></span>}<span style="white-space:pre"></span>@Override<span style="white-space:pre"></span>public void run() {<span style="white-space:pre"></span>Thread.currentThread().setPriority(Thread.MAX_PRIORITY);<span style="white-space:pre"></span>}}

3.3讓步yield

   Thread類的靜態方法,native方法,表示讓出當前CPU時間,但是讓步操作僅僅是一個建議,具體會不會讓出CPU還要由JVM調度決定。這個方法可以在測試和示範時加快線程切換頻率,讓一些問題更快的發生。需要注意的是讓步操作不會操作鎖,也就是說,如果讓步操作中發生在同步塊兒內,線程讓出CPU但是不會讓出對象鎖。

 

public class C implements Runnable {public static void main(String[] args) {ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());for (int i = 0; i < 10; i++)exec.execute(new DaemonFromFactory());}@Overridepublic void run() {Thread.yield();synchronized (this) {Thread.yield();}}}


3.4擷取當前線程currentThread

   Thread類的靜態方法,native方法,返回當前的線程對象。通過這個方法,可以線上程中擷取自身的一些屬性,狀態等,比如示範或做日誌時,經常用Thread.currentThread.getName()輸出當前Thread的名字,方便觀察。

 

3.5後台線程setDaemon

   Thread對象方法,final方法不可覆蓋。後台線程或叫守護線程,是指程式啟動並執行時候後台提供的一種泛型服務線程,而且這種線程不是程式不可或缺的一部分,當所有非後台線程結束時,程式終止,它們也就自動被終止了,而且從後台線中程構造啟動的線程都預設是後台線程(也就是Thread會繼承建立它的Thread的一些屬性)

 

public class D implements Runnable {public static void main(String[] args) {ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());for (int i = 0; i < 10; i++)exec.execute(new DaemonFromFactory());}@Overridepublic void run() {Thread.currentThread().setDaemon(true);}}

3.6加入join

   Thread對象方法,final方法,而且是同步的,拋出InterruptedExecption異常。join允許讓一個線程加入到另一個線程中,但是要注意join的時候是將誰(Thread t1)加入到誰(Threadt2),因為join的意思是將當前線程(t2)掛起,直到目標線程(t1)結束才恢複當前線程(t2)

join有多個重載的方法,調用時可以加上逾時參數,單位是毫秒,這樣在逾時時間內,join總可以返回,而且join可以被打斷。

 

public class E extends Thread {private double num = 0;public static void main(String[] args) {E e = new E();Thread f = new F(e);f.start();e.start();}@Overridepublic void run() {System.out.println("計算E");for (int i = 0; i < 10; i++) {num = Math.PI * Math.E + num;}System.out.println("E計算完成" + num);}public synchronized double getNum() {return num;}}class F extends Thread {private E e = null;public F(E e) {this.e = e;}@Overridepublic void run() {try {System.out.println("執行F");e.join();System.out.println("E執行完畢" + e.getNum());} catch (InterruptedException e) {System.out.print("被打斷");}}}

3.7是否存活isAlive

   Thread對象方法,final方法,native方法,通過它可以判斷一個Thread是否存活。但是要注意,Thread死亡是從run退出,也就是run執行完畢或執行了return語句。中斷並不代表線程死亡,或者即使任務正確處理了中斷,Thread應該結束了,但是也不要立即判斷Thread對象狀態。比如,下面的如果不Sleep,兩次輸出都會是true。


import java.util.concurrent.TimeUnit;public classTestAlive {    public static void main(String[] args) throws InterruptedException {        TMt = new TM();        t.start();        System.out.println(t.isAlive());        TimeUnit.SECONDS.sleep(1);        t.interrupt();        //TimeUnit.SECONDS.sleep(1);        System.out.println(t.isAlive());    } } class TM extendsThread {    public void run() {        while (true) {            // System.out.println("存活……");            if (Thread.interrupted()){                return;            }        }    } }


 

3.8其他

   其他的Thread方法,Interrupt和isInterrupted會在Thread中斷中總結,剩下的stop、resume、suspend等有些是JDK廢棄的了,有些是與ThreadGroup有關的。廢棄方法知道就行,沒必要浪費精力,ThreadGroup是一次失敗的嘗試,不值得在浪費精力學習了。

 

4.線程組:

   線程組是Java一次不成功的嘗試,而且JDK也一直在有意的慢慢的遺忘、淡化它,可以不學習。Java線程組是承諾升級理論的現實寫照:“繼續錯誤的代價由別人來承擔,而承認錯誤的代價由自己承擔”。因此Java一直沒有官方表明線程組是好還是壞。

 

5.異常捕捉:

   線程是很特殊的,因此我們不能捕獲從線程中逃逸出來的異常,只能線上程內處理。但是如果代碼中拋出了一個RuntimeException,這個報錯會拋給控制台。JDK5之後可以使用Executor建立線程池,然後給它添加一個異常處理器,來捕獲線程拋出的任何異常。

   PS:不能捕捉異常的意思是,我們線上程代碼外,無法用try-catch方式捕獲run拋出的任何異常。也就是下面的代碼是不對的。

try {    Threadm =new Thread(new Runnable() {        public void run() {            throw newRuntimeException();        }    });    m.start();} catch (Exceptione) {    System.out.println("捕捉到了異常");}


 

Thread.UncaughtExceptionHandler介面

     這個介面是JDK1.5之後出現的介面,Thread的內部介面。把該介面的執行個體對象設定給線程對象(t.setUncaughtExceptionHandler)或者設定為全域預設的異常處理器(Thread.setDefalutUncaughtException)即可捕捉Thread中拋出的異常。

    PS:這裡討論的異常,是我們的任務代碼拋出的異常,不應把線程的中斷異常也包括進來。

publicclassUncatchExceptionThread extends Thread {    publicvoid run() {        throw newRuntimeException("出錯了!");    }     publicstatic void main(String[]args) {        UncatchExceptionThreadt =new UncatchExceptionThread();        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){            public void uncaughtException(Threadt, Throwablee) {                System.out.println("發生了異常:" + e.getLocalizedMessage());            }        });        try {            ExecutorServiceexec = Executors.newCachedThreadPool();            exec.execute(t);//這種異常更友好!            // t.start();        }catch(Exceptione) {            System.out.println("捕捉了異常" + e);        }    }}

6.TimeUnit

      TimeUnit是JDK 1.5新增的一個枚舉類,位於java.util.concurrent包下。該枚舉的主要意圖是將程式猿從毫秒的“暴政”中解脫出來。在JDK 1.5之前,如果我們想基於時間單位做一些事情,比如Sleep 3秒,那麼我們需要計算3等於多少毫秒,雖然這個計算並不複雜,但是有了TimeUnit,我們可以直接調用TimeUnit的SECONDS的Sleep傳入long值,TimeUnit的相關方法自動幫我們轉換時間單位。

     使用TimeUnit類能讓代碼更清晰易讀,畢竟我們讀代碼的時間可能比寫代碼的時間要長。

@Overridepublic void run() {try {// 傳統的時間寫法long mills = 3 * 1000;// 休眠3秒,一秒等於1000毫秒Thread.sleep(mills);// 用TimeUnit休眠TimeUnit.SECONDS.sleep(3);// 休眠3秒} catch (InterruptedException e) {System.out.print("被打斷");}}

總結:

      Java的多線程是Java實現並發的基礎,多線程編程本身不是什麼新鮮的,也不是Java特有的,但是Java語言本身支援多線程,這比C等語言編寫多線程代碼要容易的多,而且Java本身努力消除OS層面的線程差異。Java的多線程基本文法都很簡單,比較困難的是基本概念。如果有電腦作業系統進程調度管理等方面的知識的話這些學習理解起來就比較容易了。

     Java多線程的基礎是首先要理解Thread的各個方法、概念,明白Thread的四種狀態,狀態之間的轉換。能夠合理的利用Thread類提供的各種方法完成一些事情。理解了Java多線程基礎,之後才能更好的研究Java多線程之間的同步、協同機制。對於JDK 1.5提供的concurrent包,先明白基礎之後再研究這個包比較妥當。並發專家很多都建議先使用Java傳統的做法,等有特殊情境或者需要效能最佳化的時候再用concurrent包相關的工具替換自己的實現。比較concurrent包裡面提供的很多東西都太進階了,而且多線程效能調優本身就應根據實際情境甚至是機器、JVM版本來調試。



著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

Java多線程基礎總結

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.