1. 程式人生 > >JAVA執行緒與多執行緒

JAVA執行緒與多執行緒

去安卓面試的時候通常會問一些java問題,所以呢你可能覺得答問題時答案很蛋疼,今天來介紹一下執行緒。

先看幾個概念:

執行緒:程序中負責程式執行的執行單元。一個程序中至少有一個執行緒。

多執行緒:解決多工同時執行的需求,合理使用CPU資源。多執行緒的執行是根據CPU切換完成,如何切換由CPU決定,因此多執行緒執行具有不確定性。

● 執行緒

java中的執行緒

使用java.lang.Thread類或者java.lang.Runnable介面編寫程式碼來定義、例項化和啟動新執行緒。

一個Thread類例項只是一個物件,像Java中的任何其他物件一樣,具有變數和方法,生死於堆上。

Java中,每個執行緒都有一個呼叫棧,即使不在程式中建立任何新的執行緒,執行緒也在後臺執行著。

一個Java應用總是從main()方法開始執行,mian()方法執行在一個執行緒內,它被稱為主執行緒。

一旦建立一個新的執行緒,就產生一個新的呼叫棧。

執行緒總體分兩類:使用者執行緒和守候執行緒。

當所有使用者執行緒執行完畢的時候,JVM自動關閉。但是守候執行緒卻不獨立於JVM,守候執行緒一般是由作業系統或者使用者自己建立的

建立執行緒的兩種方式:

一、繼承Thread類,擴充套件執行緒。

class MyThread extends Thread {

    @Override
public void run() { super.run(); // Perform time-consuming operation... } } MyThread t = new MyThread(); t.start();
  • 繼承Thread類,覆蓋run()方法。
  • 建立執行緒物件並用start()方法啟動執行緒。

面試題

  • 1)執行緒和程序有什麼區別?

一個程序是一個獨立(self contained)的執行環境,它可以被看作一個程式或者一個應用。而執行緒是在程序中執行的一個任務。執行緒是程序的子集,一個程序可以有很多執行緒,每條執行緒並行執行不同的任務。不同的程序使用不同的記憶體空間,而所有的執行緒共享一片相同的記憶體空間。別把它和棧記憶體搞混,每個執行緒都擁有單獨的棧記憶體用來儲存本地資料。

  • 2)如何在Java中實現執行緒?

建立執行緒有兩種方式:
一、繼承 Thread 類,擴充套件執行緒。
二、實現 Runnable 介面。

  • 3)Thread 類中的 start() 和 run() 方法有什麼區別?

呼叫 start() 方法才會啟動新執行緒;如果直接呼叫 Thread 的 run() 方法,它的行為就會和普通的方法一樣;為了在新的執行緒中執行我們的程式碼,必須使用 Thread.start() 方法。

擴充套件

Android 系統介面 HandlerThread 繼承了 Thread,它是一個可以使用 Handler 的 Thread,一個具有訊息迴圈的執行緒。run()方法中通過 Looper.prepare() 來建立訊息佇列,通過 Looper.loop() 來開啟訊息迴圈。可以在 run() 方法中執行耗時的任務,而 HandlerThread 內部建立了訊息佇列外界需要通過 Handler 的方式來通知 HandlerThread 執行一個具體任務;HandlerThread 的 run() 方法是一個無限的迴圈,可以通過它的 quite() 或 quitSafely() 方法來終止執行緒的執行;

二、實現Runnable介面。

public class DemoActivity extends BaseActivity implements Runnable {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Thread t = new Thread(this);
        t.start();
    }

    @Override
    public void run() {

    }
}

面試題

  • 1)用 Runnable 還是 Thread ?

我們都知道可以通過繼承 Thread 類或者呼叫 Runnable 介面來實現執行緒,問題是,建立執行緒哪種方式更好呢?什麼情況下使用它?這個問題很容易回答,如果你知道Java不支援類的多重繼承,但允許你呼叫多個介面。所以如果你要繼承其他類,當然是呼叫Runnable介面更好了。

  • 2)Runnable 和 Callable 有什麼不同?

Runnable 和 Callable 都代表那些要在不同的執行緒中執行的任務。Runnable 從 JDK1.0 開始就有了,Callable 是在 JDK1.5 增加的。它們的主要區別是 Callable 的 call() 方法可以返回值和丟擲異常,而 Runnable 的 run() 方法沒有這些功能。Callable 可以返回裝載有計算結果的 Future 物件。

⚠注意:這面第二個面試題主要是為了引出下面的擴充套件,原諒我這樣為難人的出場。

擴充套件

先看一下 Runnable 和 Callable 的原始碼

public interface Runnable {
    public void run();
}

public interface Callable<V> {
    V call() throws Exception;
}

可以得出:

1)Callable 介面下的方法是 call(),Runnable 介面的方法是 run()。
2)Callable 的任務執行後可返回值,而 Runnable 的任務是不能返回值的。
3)call() 方法可以丟擲異常,run()方法不可以的。
4)執行 Callable 任務可以拿到一個 Future 物件,表示非同步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。通過 Future 物件可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果。

但是,但是,凡事都有但是嘛…

單獨使用 Callable,無法在新執行緒中(new Thread(Runnable r))使用,Thread 類只支援 Runnable。不過 Callable 可以使用 ExecutorService (又丟擲一個概念,這個概念將在下篇的執行緒池中說明)。

上面又提到了 Future,看一下 Future 介面:

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future 定義了5個方法:

1)boolean cancel(boolean mayInterruptIfRunning):試圖取消對此任務的執行。如果任務已完成、或已取消,或者由於某些其他原因而無法取消,則此嘗試將失敗。當呼叫 cancel() 時,如果呼叫成功,而此任務尚未啟動,則此任務將永不執行。如果任務已經啟動,則 mayInterruptIfRunning 引數確定是否應該以試圖停止任務的方式來中斷執行此任務的執行緒。此方法返回後,對 isDone() 的後續呼叫將始終返回 true。如果此方法返回 true,則對 isCancelled() 的後續呼叫將始終返回 true。
2)boolean isCancelled():如果在任務正常完成前將其取消,則返回 true。
3)boolean isDone():如果任務已完成,則返回 true。 可能由於正常終止、異常或取消而完成,在所有這些情況中,此方法都將返回 true。
4)V get()throws InterruptedException,ExecutionException:如有必要,等待計算完成,然後獲取其結果。
5)V get(long timeout,TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException: 如有必要,最多等待為使計算完成所給定的時間之後,獲取其結果(如果結果可用)。

看來 Future 介面也不能用線上程中,那怎麼用,誰實現了 Future 介面呢?答:FutureTask。

public class FutureTask<V> implements RunnableFuture<V> {
    ...
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

FutureTask 實現了 Runnable 和 Future,所以兼顧兩者優點,既可以在 Thread 中使用,又可以在 ExecutorService 中使用。

看完上面囉哩囉嗦的介紹後,下面我們具體看一下具體使用的示例:

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        return "Callable 的使用";
    }
};

FutureTask<String> task = new FutureTask<String>(callable);

Thread t = new Thread(task);
t.start(); // 啟動執行緒
task.cancel(true); // 取消執行緒

使用 FutureTask 的好處是 FutureTask 是為了彌補 Thread 的不足而設計的,它可以讓程式設計師準確地知道執行緒什麼時候執行完成並獲得到執行緒執行完成後返回的結果。FutureTask 是一種可以取消的非同步的計算任務,它的計算是通過 Callable 實現的,它等價於可以攜帶結果的 Runnable,並且有三個狀態:等待、執行和完成。完成包括所有計算以任意的方式結束,包括正常結束、取消和異常。

執行緒狀態

  1. 新建狀態(New):新建立了一個執行緒物件。

  2. 就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。

  3. 執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。

  4. 阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:
    (一)、等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。
    (二)、同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。
    (三)、其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。

  5. 死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

● 多執行緒

多執行緒的概念很好理解就是多條執行緒同時存在,但要用好多執行緒確不容易,涉及到多執行緒間通訊,多執行緒共用一個資源等諸多問題。

使用多執行緒的優缺點:
優點:
1)適當的提高程式的執行效率(多個執行緒同時執行)。
2)適當的提高了資源利用率(CPU、記憶體等)。
缺點:
1)佔用一定的記憶體空間。
2)執行緒越多CPU的排程開銷越大。
3)程式的複雜度會上升。

下面我主要列出一些多執行緒的技術點。

synchronized

同步塊大家都比較熟悉,通過 synchronized 關鍵字來實現;所有加上 synchronized 的方法和塊語句,在多執行緒訪問的時候,同一時刻只能有一個執行緒能夠訪問。

wait()、notify()、notifyAll()

這三個方法是 java.lang.Object 的 final native 方法,任何繼承 java.lang.Object 的類都有這三個方法。它們是Java語言提供的實現執行緒間阻塞和控制程序內排程的底層機制,平時我們會很少用到的。

wait():
導致執行緒進入等待狀態,直到它被其他執行緒通過notify()或者notifyAll喚醒,該方法只能在同步方法中呼叫。

notify():
隨機選擇一個在該物件上呼叫wait方法的執行緒,解除其阻塞狀態,該方法只能在同步方法或同步塊內部呼叫。

notifyAll():
解除所有那些在該物件上呼叫wait方法的執行緒的阻塞狀態,同樣該方法只能在同步方法或同步塊內部呼叫。

wait()、notify()、notifyAll()都是Object的例項方法。與每個物件具有鎖一樣,每個物件可以有一個執行緒列表,他們等待來自該訊號(通知)。執行緒通過執行物件上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到呼叫物件的notify()方法為止。如果多個執行緒在同一個物件上等待,則將只選擇一個執行緒(不保證以何種順序)繼續執行。如果沒有執行緒等待,則不採取任何特殊操作。

呼叫這三個方法中任意一個,當前執行緒必須是鎖的持有者,如果不是會丟擲一個 IllegalMonitorStateException 異常。

wait() 與 Thread.sleep(long time) 的區別

sleep():在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),該執行緒不丟失任何監視器的所屬權,sleep() 是 Thread 類專屬的靜態方法,針對一個特定的執行緒。
wait() 方法使實體所處執行緒暫停執行,從而使物件進入等待狀態,直到被 notify() 方法通知或者 wait() 的等待的時間到。sleep() 方法使持有的執行緒暫停執行,從而使執行緒進入休眠狀態,直到用 interrupt 方法來打斷他的休眠或者 sleep 的休眠的時間到。
wait() 方法進入等待狀態時會釋放同步鎖,而 sleep() 方法不會釋放同步鎖。所以,當一個執行緒無限 sleep 時又沒有任何人去 interrupt 它的時候,程式就產生大麻煩了,notify() 是用來通知執行緒,但在 notify() 之前執行緒是需要獲得 lock 的。另個意思就是必須寫在 synchronized(lockobj) {…} 之中。wait() 也是這個樣子,一個執行緒需要釋放某個 lock,也是在其獲得 lock 情況下才能夠釋放,所以 wait() 也需要放在 synchronized(lockobj) {…} 之中。

volatile 關鍵字

volatile 是一個特殊的修飾符,只有成員變數才能使用它。在Java併發程式缺少同步類的情況下,多執行緒對成員變數的操作對其它執行緒是透明的。volatile 變數可以保證下一個讀取操作會在前一個寫操作之後發生。執行緒都會直接從記憶體中讀取該變數並且不快取它。這就確保了執行緒讀取到的變數是同記憶體中是一致的。

ThreadLocal 變數

ThreadLocal 是Java裡一種特殊的變數。每個執行緒都有一個 ThreadLocal 就是每個執行緒都擁有了自己獨立的一個變數,競爭條件被徹底消除了。如果為每個執行緒提供一個自己獨有的變數拷貝,將大大提高效率。首先,通過複用減少了代價高昂的物件的建立個數。其次,你在沒有使用高代價的同步或者不變性的情況下獲得了執行緒安全。

join() 方法

join() 方法定義在 Thread 類中,所以呼叫者必須是一個執行緒,join() 方法主要是讓呼叫該方法的 Thread 完成 run() 方法裡面的東西后,再執行 join() 方法後面的程式碼,看下下面的”意思”程式碼:

Thread t1 = new Thread(計數執行緒一);  
Thread t2 = new Thread(計數執行緒二);  
t1.start();  
t1.join(); // 等待計數執行緒一執行完成,再執行計數執行緒二
t2.start();  

啟動 t1 後,呼叫了 join() 方法,直到 t1 的計數任務結束,才輪到 t2 啟動,然後 t2 才開始計數任務,兩個執行緒是按著嚴格的順序來執行的。如果 t2 的執行需要依賴於 t1 中的完整資料的時候,這種方法就可以很好的確保兩個執行緒的同步性。

Thread.yield() 方法

Thread.sleep(long time):執行緒暫時終止執行(睡眠)一定的時間。
Thread.yield():執行緒放棄執行,將CPU的控制權讓出,暫停當前正在執行的執行緒物件,並執行其他執行緒。

這兩個方法都會將當前執行執行緒的CPU控制權讓出來,但 sleep() 方法在指定的睡眠時間內一定不會再得到執行機會,直到它的睡眠時間完成;而 yield() 方法讓出控制權後,還有可能馬上被系統的排程機制選中來執行,比如,執行yield()方法的執行緒優先順序高於其他的執行緒,那麼這個執行緒即使執行了 yield() 方法也可能不能起到讓出CPU控制權的效果,因為它讓出控制權後,進入排隊佇列,排程機制將從等待執行的執行緒佇列中選出一個等級最高的執行緒來執行,那麼它又(很可能)被選中來執行。

擴充套件

執行緒排程策略

(1) 搶佔式排程策略

Java執行時系統的執行緒排程演算法是搶佔式的。Java執行時系統支援一種簡單的固定優先順序的排程演算法。如果一個優先順序比其他任何處於可執行狀態的執行緒都高的執行緒進入就緒狀態,那麼執行時系統就會選擇該執行緒執行。新的優先順序較高的執行緒搶佔了其他執行緒。但是Java執行時系統並不搶佔同優先順序的執行緒。換句話說,Java執行時系統不是分時的。然而,基於Java Thread類的實現系統可能是支援分時的,因此編寫程式碼時不要依賴分時。當系統中的處於就緒狀態的執行緒都具有相同優先順序時,執行緒排程程式採用一種簡單的、非搶佔式的輪轉的排程順序。

(2) 時間片輪轉排程策略

有些系統的執行緒排程採用時間片輪轉排程策略。這種排程策略是從所有處於就緒狀態的執行緒中選擇優先順序最高的執行緒分配一定的CPU時間執行。該時間過後再選擇其他執行緒執行。只有當執行緒執行結束、放棄(yield)CPU或由於某種原因進入阻塞狀態,低優先順序的執行緒才有機會執行。如果有兩個優先順序相同的執行緒都在等待CPU,則排程程式以輪轉的方式選擇執行的執行緒。

執行緒的同步與鎖

1、鎖的原理

Java中每個物件都有一個內建鎖

當程式執行到非靜態的synchronized同步方法上時,自動獲得與正在執行程式碼類的當前例項(this例項)有關的鎖。獲得一個物件的鎖也稱為獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。

當程式執行到synchronized同步方法或程式碼塊時才該物件鎖才起作用。

一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,直到第一個執行緒釋放(或返回)鎖。這也意味著任何其他執行緒都不能進入該物件上的synchronized方法或程式碼塊,直到該鎖被釋放。

釋放鎖是指持鎖執行緒退出了synchronized同步方法或程式碼塊。

關於鎖和同步,有一下幾個要點:

1)、只能同步方法,而不能同步變數和類;

2)、每個物件只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個物件上同步?

3)、不必同步類中所有的方法,類可以同時擁有同步和非同步方法。

4)、如果兩個執行緒要執行一個類中的synchronized方法,並且兩個執行緒使用相同的例項來呼叫方法,那麼一次只能有一個執行緒能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個執行緒在物件上獲得一個鎖,就沒有任何其他執行緒可以進入(該物件的)類中的任何一個同步方法。

5)、如果執行緒擁有同步和非同步方法,則非同步方法可以被多個執行緒自由訪問而不受鎖的限制。

6)、執行緒睡眠時,它所持的任何鎖都不會釋放。

7)、執行緒可以獲得多個鎖。比如,在一個物件的同步方法裡面呼叫另外一個物件的同步方法,則獲取了兩個物件的同步鎖。

8)、同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分程式碼塊。

9)、在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。例如:

    public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }

當然,同步方法也可以改寫為非同步方法,但功能完全一樣的,例如:

    public synchronized int getX() {
        return x++;
    }

    public int getX() {
        synchronized (this) {
            return x;
        }
    }

效果是完全一樣的。

靜態方法同步

要同步靜態方法,需要一個用於整個類物件的鎖,這個物件是就是這個類(XXX.class)。

例如:

public static synchronized int setName(String name){

      Xxx.name = name;

}

等價於

public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

如果執行緒不能不能獲得鎖會怎麼樣

如果執行緒試圖進入同步方法,而其鎖已經被佔用,則執行緒在該物件上被阻塞。實質上,執行緒進入該物件的的一種池中,必須在哪裡等待,直到其鎖被釋放,該執行緒再次變為可執行或執行為止。

當考慮阻塞時,一定要注意哪個物件正被用於鎖定:

1、呼叫同一個物件中非靜態同步方法的執行緒將彼此阻塞。如果是不同物件,則每個執行緒有自己的物件的鎖,執行緒間彼此互不干預。

2、呼叫同一個類中的靜態同步方法的執行緒將彼此阻塞,它們都是鎖定在相同的Class物件上。

3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class物件上,非靜態方法鎖定在該類的物件上。

4、對於同步程式碼塊,要看清楚什麼物件已經用於鎖定(synchronized後面括號的內容)。在同一個物件上進行同步的執行緒將彼此阻塞,在不同物件上鎖定的執行緒將永遠不會彼此阻塞。

何時需要同步

在多個執行緒同時訪問互斥(可交換)資料時,應該同步以保護資料,確保兩個執行緒不會同時修改更改它。

對於非靜態欄位中可更改的資料,通常使用非靜態方法訪問。

對於靜態欄位中可更改的資料,通常使用靜態方法訪問。

 JAVA學習網站:

更多精彩內容: