【Java基礎】執行緒和併發機制

cryAllen發表於2017-02-15

前言

在Java中,執行緒是一個很關鍵的名詞,也是很高頻使用的一種資源。那麼它的概念是什麼呢,是如何定義的,用法又有哪些呢?為何說Android裡只有一個主執行緒呢,什麼是工作執行緒呢。執行緒又存在併發,併發機制的原理是什麼。這些內容有些瞭解,有些又不是很清楚,所以有必要通過一篇文章的梳理,弄清其中的來龍去脈,為了之後的開發過程中提供更好的支援。

目錄

  • 執行緒定義
  • Java執行緒生命週期
  • 執行緒用法
  • Android中的執行緒
  • 工作執行緒
  • 使用AsyncTask
  • 什麼是併發
  • 併發機制原理
  • 併發具體怎麼用

執行緒定義

說到執行緒,就離不開談到程式了,比如在Android中,一個應用程式基本有一個程式,但是一個程式可以有多個執行緒組成。在應用程式中,執行緒和程式是兩個基本執行單元,都是可以處理比較複雜的操作,比如網路請求、I/O讀寫等等,在Java中我們大部分操作的是執行緒(Thread),當然程式也是很重要的。

程式通常有獨立執行環境,有完整的可設定為私有基本執行資源,比如,每個程式會有自己的記憶體空間。而執行緒呢,去官網的查了下,原話如下:

Threads are sometimes called "lightweight processes". Both processes and threads provide an execution environment, but creating a new thread requires fewer resources than creating a new process.

意思就是:執行緒相比程式所建立的資源要少很多,都是在執行環境下的執行單元。同時,每個執行緒有個優先順序,高的優先順序比低的優先順序優先執行。執行緒是作業系統能夠進行運算排程的最小單位,它被包含在程式之中,是程式中的實際運作單位。

Java執行緒生命週期

  1. 新建狀態(New):當執行緒物件建立後,即進入了新建狀態。僅僅由java虛擬機器分配記憶體,並初始化。如:Thread t = new MyThread();
  2. 就緒狀態(Runnable):當呼叫執行緒物件的start()方法(t.start();),執行緒即進入就緒狀態。處於就緒狀態的執行緒,java虛擬機器建立方法呼叫棧和程式計數器,只是說明此執行緒已經做好了準備,隨時等待CPU排程執行,此執行緒並 沒有執行。
  3. 執行狀態(Running):當CPU開始排程處於就緒狀態的執行緒時,執行run()方法,此時執行緒才得以真正執行,即進入到執行狀態。注:緒狀態是進入到執行狀態的唯一入口,也就是說,執行緒要想進入執行狀態執行,首先必須處於就緒狀態中;
  4. 阻塞狀態(Blocked):處於執行狀態中的執行緒由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU呼叫以進入到執行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:等待阻塞 – 執行狀態中的執行緒執行wait()方法,使本執行緒進入到等待阻塞狀態,JVM會把該執行緒放入等待池中;同步阻塞 – 執行緒在獲取synchronized同步鎖失敗(因為鎖被其它執行緒所佔用),它會進入同步阻塞狀態;其他阻塞 – 通過呼叫執行緒的sleep()或join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
  5. 死亡狀態(Dead):執行緒run()方法執行完了或者因異常退出了run()方法,該執行緒結束生命週期。 當主執行緒結束時,其他執行緒不受任何影響。

執行緒用法

那該如何建立執行緒呢,有兩種方式。

  • 使用Runnable
  • 繼承Thread類,定義子類

使用Runnable:

Runnable介面有個run方法,我們可以定義一個類實現Runnable介面,Thread類有個建構函式,引數是Runnable,我們定義好的類可以當引數傳遞進去。

public class HelloRunnable implements Runnable {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }

}

繼承Thread類:

Thread類它自身就包含了Runnable介面,我們可以定義一個子類來繼承Thread類,進而在Run方法中執行相關程式碼。

public class HelloThread extends Thread {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new HelloThread()).start();
    }

}

從兩個使用方式上看,定義好Thread後,都需要執行start()方法,執行緒才算開始執行。

Android中的執行緒

當某個應用元件啟動且該應用沒有執行其他任何元件時,Android 系統會使用單個執行執行緒為應用啟動新的 Linux 程式。預設情況下,同一應用的所有元件在相同的程式和執行緒(稱為“主”執行緒)中執行。

應用啟動時,系統會為應用建立一個名為“主執行緒”的執行執行緒。 此執行緒非常重要,因為它負責將事件分派給相應的使用者介面小工具,其中包括繪圖事件。 此外,它也是應用與 Android UI 工具包元件(來自 android.widget 和 android.view 軟體包的元件)進行互動的執行緒。因此,主執行緒有時也稱為 UI 執行緒。

系統絕對不會為每個元件例項建立單獨的執行緒。執行於同一程式的所有元件均在 UI 執行緒中例項化,並且對每個元件的系統呼叫均由該執行緒進行分派。因此,響應系統回撥的方法,例如,報告使用者操作的 onKeyDown() 或生命週期回撥方法)始終在程式的 UI 執行緒中執行。例如,當使用者觸控螢幕上的按鈕時,應用的 UI 執行緒會將觸控事件分派給小工具,而小工具反過來又設定其按下狀態,並將無效請求釋出到事件佇列中。UI 執行緒從佇列中取消該請求並通知小工具應該重繪自身。

在應用執行繁重的任務以響應使用者互動時,除非正確實施應用,否則這種單執行緒模式可能會導致效能低下。 特別地,如果 UI 執行緒需要處理所有任務,則執行耗時很長的操作(例如,網路訪問或資料庫查詢)將會阻塞整個 UI。一旦執行緒被阻塞,將無法分派任何事件,包括繪圖事件。從使用者的角度來看,應用顯示為掛起。 更糟糕的是,如果 UI 執行緒被阻塞超過幾秒鐘時間(目前大約是 5 秒鐘),使用者就會看到一個讓人厭煩的“應用無響應”(ANR) 對話方塊。

此外,Android UI 工具包並非執行緒安全工具包。因此,您不得通過工作執行緒操縱 UI,而只能通過 UI 執行緒操縱使用者介面。因此,Android 的單執行緒模式必須遵守兩條規則:

  1. 不要阻塞 UI 執行緒
  2. 不要在 UI 執行緒之外訪問 Android UI 工具包

那為何Andorid是主執行緒模式呢,就不能多執行緒嗎?在Java中預設情況下一個程式只有一個執行緒,這個執行緒就是主線。主執行緒主要處理介面互動相關的邏輯,因為使用者隨時會和介面發生互動,因此主執行緒在任何時候都必須有比較高的響應速度,否則就會產生一種介面卡頓的感覺。同樣Android也是沿用了Java的執行緒模型,Android是基於事件驅動機制執行,如果沒有一個主執行緒進行排程分配,那麼執行緒間的事件傳遞就會顯得雜亂無章,使用起來也冗餘,還有執行緒的安全性因素也是一個值得考慮的一個點。

工作執行緒

既然瞭解主執行緒模式,除了UI執行緒,其他都是叫工作執行緒。根據單執行緒模式,要保證應用 UI 的響應能力,關鍵是不能阻塞 UI 執行緒。如果執行的操作不能很快完成,則應確保它們在單獨的執行緒(“後臺”或“工作”執行緒)中執行。例如以下程式碼表示一個點選監聽從單獨的執行緒下載影象並將其顯示在 ImageView 中:

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() { 
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        } 
    }).start();
} 

咋看起來貌似沒什麼問題,它建立了一個執行緒來處理網路操作, 但是呢,它卻是在UI執行緒中執行,但是,它違反了單執行緒模式的第二條規則:不要在 UI 執行緒之外訪問 Android UI 工具包。

那麼你會問個問題了,為什麼子執行緒中不能更新UI。因為UI訪問是沒有加鎖的,在多個執行緒中訪問UI是不安全的,如果有多個子執行緒都去更新UI,會導致介面不斷改變而混亂不堪。所以最好的解決辦法就是隻有一個執行緒有更新UI的許可權。

當然,Android 提供了幾種途徑來從其他執行緒訪問 UI 執行緒。以下列出了幾種有用的方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

例如,您可以通過使用 View.post(Runnable) 方法修復上述程式碼:

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() { 
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() { 
                    mImageView.setImageBitmap(bitmap);
                } 
            }); 
        } 
    }).start();
} 

現在,上述實現屬於執行緒安全型:在單獨的執行緒中完成網路操作,而在 UI 執行緒中操縱 ImageView

但是,隨著操作日趨複雜,這類程式碼也會變得複雜且難以維護。 要通過工作執行緒處理更復雜的互動,可以考慮在工作執行緒中使用 Handler 處理來自 UI 執行緒的訊息。當然,最好的解決方案或許是擴充套件 AsyncTask 類,此類簡化了與 UI 進行互動所需執行的工作執行緒任務。

使用 AsyncTask

AsyncTask 允許對使用者介面執行非同步操作。它會先阻塞工作執行緒中的操作,然後在 UI 執行緒中釋出結果,而無需你親自處理執行緒和/或處理程式。

要使用它,必須建立 AsyncTask 子類並實現 doInBackground() 回撥方法,該方法將在後臺執行緒池中執行。要更新 UI,必須實現 onPostExecute() 以傳遞doInBackground() 返回的結果並在 UI 執行緒中執行,這樣,即可安全更新 UI。稍後,您可以通過從 UI 執行緒呼叫 execute() 來執行任務。

例如,可以通過以下方式使用 AsyncTask 來實現上述示例:

public void onClick(View v) { 
    new DownloadImageTask().execute("http://example.com/image.png"); 
} 
 
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
    /** The system calls this to perform work in a worker thread and 
      * delivers it the parameters given to AsyncTask.execute() */ 
    protected Bitmap doInBackground(String... urls) {
        return loadImageFromNetwork(urls[0]);
    } 
 
    /** The system calls this to perform work in the UI thread and delivers 
      * the result from doInBackground() */ 
    protected void onPostExecute(Bitmap result) {
        mImageView.setImageBitmap(result);
    } 
} 

現在 UI 是安全的,程式碼也得到簡化,因為任務分解成了兩部分:一部分應在工作執行緒內完成,另一部分應在 UI 執行緒內完成。

下面簡要概述了 AsyncTask 的工作方法,但要全面瞭解如何使用此類,您應閱讀 AsyncTask 參考文件:

  • 可以使用泛型指定引數型別、進度值和任務最終值
  • 方法 doInBackground() 會在工作執行緒上自動執行
  • onPreExecute()onPostExecute() 和 onProgressUpdate() 均在 UI 執行緒中呼叫
  • doInBackground() 返回的值將傳送到 onPostExecute()
  • 您可以隨時在 doInBackground() 中呼叫publishProgress(),以在 UI 執行緒中執行 onProgressUpdate()
  • 您可以隨時取消任何執行緒中的任務

什麼是併發

說到併發,首先需要區別併發和並行這兩個名詞的區別。

併發性和並行性
併發是指在同一時間點只能有一條指令執行,但多個程式指令被快速輪換執行,使得在巨集觀上具有多個程式同時執行的效果。
並行指在同一時間點,有多條指令在多個處理器上同時執行。

那麼我們為什麼需要併發呢?通常是為了提高程式的執行速度或者改善程式的設計。

併發機制原理

Java對併發程式設計提供了語言級別的支援。Java通過執行緒來實現併發程式設計。一個執行緒通常完成某個特定的任務,一個程式可以擁有多個執行緒,當這些執行緒一起執行的時候,就實現了併發。與作業系統中的程式相似,每個執行緒看起來好像擁有自己的CPU,但是其底層是通過切分CPU時間來實現的。與程式不同的是,執行緒並不是相互獨立的,它們通常要相互合作來完成一些任務。

併發具體怎麼用

休眠

我們可以讓一個執行緒暫時休息一會兒。Thread類有一個sleep靜態方法,你可以將一個long型別的資料當做引數傳進去,單位是毫秒,表示執行緒將會休眠的時間。

讓步

Thread類還有一個名為yield()的靜態方法。這個方法的作用是為了建議當前正在執行的執行緒做個讓步,讓出CPU時間給別的執行緒來執行。程式中可能會有一個執行緒在某個時刻已經完成了一大部分的任務,並且這個時候讓別的執行緒來執行比較合理。這樣的情況下,就可以呼叫yield()方法進行讓步。不過,呼叫這個方法並不能保證一定會起作用,畢竟它只是建議性的。所以,不應該用這個方法來控制程式的執行流程。

串入(join)

當一個執行緒t1在另一個執行緒t2上呼叫t1.join()方法的時候,執行緒t2將等待執行緒t1執行結束之後再開始執行。正如下面這個例子:

public class ThreadTest {
  public static void main(String[] args) {
      SimpleThread simpleThread = new SimpleThread();
      Thread t = new Thread(simpleThread);
      t.start();
  }
}
public class SimpleThread implements Runnable{
  @Override
  public void run() {
      Thread tempThread = new Thread() {
                              @Override
                              public void run() {
                                  for(int i = 10; i < 15 ;i++) {
                                      System.out.println(i);
                                  }
                              }
                          };
      
      tempThread.start();
      
      try {
          tempThread.join();        //tempThread串入
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      
      for(int i = 0; i < 5; i++) {
          System.out.println(i);
      }
  }
}

輸出結果為:

10
11
12
13
14
0
1
2
3
4

優先順序

我們可以給一個執行緒設定一個優先順序。執行緒排程器在做排程工作的時候,優先順序越高的執行緒越可能得到先執行的機會。Thread類的setPriority方法和getPriority方法分別用來設定執行緒的優先順序和獲取執行緒的優先順序。由於執行緒排程器根據優先順序的大小來排程執行緒的效果在各種不同的JVM上差別很大,所以在絕大多數情況下,我們不應該依靠設定優先順序來完成我們的工作,保持預設的優先順序是一條很好的建議。

相關文章