【Java基礎】線程和並發機制


前言

在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上差別很大,所以在絕大多數情況下,我們不應該依靠設定優先級來完成我們的工作,保持默認的優先級是一條很好的建議。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM