進階之路 | 奇妙的Thread之旅

前言

本文已經收錄到我的Github個人部落格,歡迎大佬們光臨寒舍:

我的GIthub部落格

需要已經具備的知識:

  • Thread的基本概念及使用
  • AsyncTask的基本概念及使用

學習導圖:

学习导图

一.為什麼要學習Thread?

在Android中,幾乎完全採用了Java中的執行緒機制。執行緒是最小的調度單位,在很多情況下為了使APP更加流程地運行,我們不可能將很多事情都放在主執行緒上執行,這樣會造成嚴重卡頓(ANR),那麼這些事情應該交給子執行緒去做,但對於一個系統而言,創建、銷毀、調度執行緒的過程是需要開銷的,所以我們並不能無限量地開啟執行緒,那麼對執行緒的了解就變得尤為重要了。

本篇文章將帶領大家由淺入深,從執行緒的基礎,談到同步機制,再講到阻塞隊列,接著提及Android中的執行緒形態,最終一覽執行緒池機制。

話不多說,趕緊開始奇妙的Thread之旅吧!

二.核心知識點歸納

2.1 執行緒概述

Q1:含義

執行緒是CPU調度的最小單位

注意與進程相區分

Q2:特點

執行緒是一種受限的系統資源。即執行緒不可無限制的產生且執行緒的創建和銷毀都有一定的開銷

Q:如何避免頻繁創建和銷毀執行緒所帶來的系統開銷?
A:採用執行緒池,池中會快取一定數量的執行緒,進而達到效果

Q3:分類

  • 按用途分為兩類:
  • 主執行緒:一般一個進程只有一個主執行緒,主要處理介面交互相關的邏輯

  • 子執行緒:除主執行緒之外都是子執行緒,主要用於執行耗時操作

  • 按形態可分為三類:
  • AsyncTask:底層封裝了執行緒池和Handler,便於執行後台任務以及在主執行緒中進行UI操作
  • HandlerThread:一種具有消息循環的執行緒,其內部可使用Handler
  • IntentService:一種非同步、會自動停止的服務,內部採用HandlerThread和Handler

关系图

PS:想詳細了解Handler機制的讀者,推薦一篇筆者的文章:進階之路 | 奇妙的Handler之旅

Q4:如何安全地終止執行緒?

對於有多執行緒開發經驗的開發者,應該大多數在開發過程中都遇到過這樣的需求,就是在某種情況下,希望立即停止一個執行緒

比如:做Android開發,當打開一個介面時,需要開啟執行緒請求網路獲取介面的數據,但有時候由於網路特別慢,用戶沒有耐心等待數據獲取完成就將介面關閉,此時就應該立即停止執行緒任務,不然一般會記憶體泄露,造成系統資源浪費,如果用戶不斷地打開又關閉介面,記憶體泄露會累積,最終導致記憶體溢出,APP閃退

所以,筆者希望能和大家探究下:如何安全地終止執行緒?

A1:為啥不使用stop?

Java官方早已將它廢棄,不推薦使用

  • stop是通過立即拋出ThreadDeath異常,來達到停止執行緒的目的,此異常拋出有可能發生在任何一時間點,包括在catch、finally等語句塊中,但是此異常並不會引起程式退出
  • 異常拋出,導致執行緒會釋放全部所持有的鎖,極可能引起執行緒安全問題

A2:提供單獨的取消方法來終止執行緒

示例DEMO:

    public class MoonRunner implements Runnable {          private long i;          //注意的是這裡的變數是用volatile修飾          volatile boolean on = true;            @Override          public void run() {              while (on) {                  i++;              }              System.out.println("sTop");          }            //設置一個取消的方法          void cancel() {              on = false;          }      }

注意:這裡的變數是用volatile修飾,以保證可見性,關於volatile的知識,筆者將在下文為您詳細解析

A3:採用interrupt來終止執行緒

Thread類定義了如下關於中斷的方法:

中断的方法

原理:

  • 調用Thread對象的interrupt函數並不是立即中斷執行緒,只是將執行緒中斷狀態標誌設置為true

  • 當執行緒運行中有調用其阻塞的函數時,阻塞函數調用之後,會不斷地輪詢檢測中斷狀態標誌是否為true,如果為true,則停止阻塞並拋出InterruptedException異常,同時還會重置中斷狀態標誌,因此需要在catch程式碼塊中需調用interrupt函數,使執行緒再次處於中斷狀態
  • 如果中斷狀態標誌為false,則繼續阻塞,直到阻塞正常結束

具體的interrupt的使用方式可以參考這篇文章:Java執行緒中斷的正確姿勢

2.2 同步機制

2.2.1 volatile

  • 有時候僅僅為了讀寫一個或者兩個實例就使用同步synchronized的話,顯得開銷過大
  • 而volatile為實例域的同步訪問提供了免鎖的機制

Q1:先從Java記憶體模型聊起

  • Java 記憶體模型定義了本地記憶體和主存之間的抽象關係
  • 執行緒之間的共享變數存儲在主存中
  • 每個執行緒都有一個私有的本地記憶體(工作記憶體),本地記憶體中存儲了該執行緒共享變數的副本。

内存关系

  • 執行緒之間通訊的步驟
  • 執行緒A將其本地記憶體中更新過的共享變數刷新到主存中去
  • 執行緒B到主存中去讀取執行緒A之前已更新過的共享變數

Q2:原子性、可見性和有序性了解多少

a1:原子性Atomicity:

  • 定義:原子性操作就是指這些操作是不可中斷的,要做一定做完,要麼就沒有執行
  • 對基本數據類型變數的讀取和賦值操作是原子性操作

注意:這裡的賦值操作是指將數字賦值給某個變數

下面由DEMO解釋更加通俗易懂

x=3;  //原子性操作  y=x;  //非原子性操作  原因:包括2個操作:先讀取x的值,再將x的值寫入工作記憶體  x++;  //非原子性操作  原因:包括3個操作:讀取x的值、對x的值進行加1、向工作記憶體寫入新值
  • volatile不支援原子性(想探究原因的,筆者推薦一篇文章:面試官最愛的volatile關鍵字)
  • 保證整塊程式碼原子性(例如i++)的方法:藉助於synchronized和Lock,以及並發包下的atomic的原子操作類

a2:可見性Visibility

  • 定義:一個執行緒修改的結果,另一個執行緒馬上就能看到

  • Java就是利用volatile來提供可見性的

原因:當一個變數被volatile修飾時,那麼對它的修改會立刻刷新到主存,同時使其它執行緒的工作記憶體中對此變數的快取行失效,因此需要讀取該變數時,會去記憶體中讀取新值

  • 其實通過synchronized和Lock也能夠保證可見性,但是synchronized和Lock的開銷都更大

a3:有序性Ordering

  • 指令重排序的定義:大多數現代微處理器都會採用將指令亂序執行的方法, 在條件允許的情況下, 直接運行當前有能力立即執行的後續指令, 避開獲取下一條指令所需數據時造成的等待
  • 什麼時候不進行指令重排序:
  • 符合數據依賴性:
//x對a有依賴  a = 1;  x = a;
  • as-if-serial語義:不管怎麼重排序, 單執行緒程式的執行結果不能被改變
  • 程式順序原則
  1. 如果A happens-before B
  2. 如果B happens-before C
  3. 那麼A happens-before C

這就是happens-before傳遞性

  • volatile通過禁止指令重排序的方式來保證有序性

Q3:應用場景有哪些?

  • 狀態量標記

執行緒的終止的時候的狀態控制,示例DEMO如前文

  • DCL

避免指令重排序:

假定創建一個對象需要:

  1. 申請記憶體
  2. 初始化
  3. instance指向分配的那塊記憶體

上面的2和3操作是有可能重排序的, 如果3重排序到2的前面, 這時候2操作還沒有執行, instance!=null, 當然不是安全的

class Singleton{      private volatile static Singleton instance = null;        private Singleton() {}        public static Singleton getInstance() {          if(instance==null) {              synchronized (Singleton.class) {                  if(instance==null)                      instance = new Singleton();              }          }          return instance;      }  }

Q4:原理:

  • 如果把加入volatile關鍵字的程式碼和未加入volatile關鍵字的程式碼都生成彙編程式碼,會發現加入volatile關鍵字的程式碼會多出一個lock前綴指令
  • lock前綴指令實際相當於一個記憶體屏障,記憶體屏障提供了以下功能:
  • 重排序時不能把後面的指令重排序到記憶體屏障之前的位置
  • 使得本CPU的Cache寫入記憶體
  • 寫入動作也會引起別的CPU或者別的內核無效化其Cache,相當於讓新寫入的值對別的執行緒可見

2.2.2 重入鎖與條件對象

synchronized 關鍵字自動為我們提供了鎖以及相關的條件,大多數需要顯式鎖的時候,使用synchronized 非常方便,但是當我們了解了重入鎖和條件對象時,能更好地理解synchronized 和阻塞隊列

Q1:重入鎖的定義

  • 可重入鎖指的是可重複可遞歸調用的鎖,在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖,這樣的鎖就叫做可重入鎖
  • ReentrantLock和synchronized都是可重入鎖

重複調用鎖的DEMO如下:

public class ReentrantTest implements Runnable {        public synchronized void get() {          System.out.println(Thread.currentThread().getName());          set();      }        public synchronized void set() {          System.out.println(Thread.currentThread().getName());      }        public void run() {          get();      }        public static void main(String[] args) {          ReentrantTest rt = new ReentrantTest();          for(;;){              new Thread(rt).start();          }      }  }  

Q2:什麼是條件對象Condition?

  • 條件對象來管理那些已經獲得了一個鎖但是卻不能做有用工作的執行緒,條件對象又被稱作條件變數
  • 一般要配合ReentrantLock使用,用Condition.await()可以阻塞當前執行緒,並放棄鎖

Q3:下面說明重入鎖與條件對象如何協同使用

  • 用支付寶轉賬的例子(支付寶打錢,狗頭.jpg)
  • 場景是這樣的:
//轉賬的方法  public void transfer(int from, int to, int amount){      //alipay是ReentrantLock的實例      alipay.lock();      try{          //當要轉給別人的錢大於你所擁有的錢的時候,調用Condition的await可以阻塞當前執行緒,並放棄鎖          while(accounts[from] < amount){              condition.await();          }            ...//一系列轉賬的操作              //阻塞狀態解除,進入可運行狀態          condition.signalAll();      }      finally{          alipay.unlock();      }  }

想要更深一步了解重入鎖的讀者,可以看下這篇文章:究竟什麼是可重入鎖?

2.2.3 synchronized

Q1:synchronized有哪幾種實現方式?

  • 同步程式碼塊
  • 同步方法

Q2:synchronized與ReentrantLock的關係

  • 兩者都是重入鎖
  • 兩者有些方法互相對應
  • wait等價於condition.await()
  • notifyAll等價於condition.signalAll()

Q3:使用場景對比

類型 使用場景
阻塞隊列 一般實現同步的時候使用
同步方法 如果同步方法適合你的程式
同步程式碼塊 不建議使用
Lock/Condition 需要使用Lock/Condition的獨有特性時

2.3 阻塞隊列

為了更好地理解執行緒池的知識,我們需要了解下阻塞隊列

Q1:定義

  • 阻塞隊列BlockingQueue是一個支援兩個附加操作的隊列。這兩個附加的操作是:
  • 在隊列為空時,獲取元素的執行緒會等待隊列變為非空
  • 當隊列滿時,存儲元素的執行緒會等待隊列可用

Q2:使用場景:

阻塞隊列常用於生產者和消費者的場景,生產者是往隊列里添加元素的執行緒,消費者是從隊列里拿元素的執行緒。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器里拿元素。

Q3:核心方法

方法處理方式 拋出異常 返回特殊值 一直阻塞 超時退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
檢查方法 element() peek() 不可用 不可用

Q4:JAVA中的阻塞隊列

名稱 含義
ArrayBlockingQueue 由數組結構組成的有界阻塞隊列(最常用)
LinkedBlockingQueue 由鏈表結構組成的有界阻塞隊列(最常用)注意:一定要指定大小
PriorityBlockingQueue 支援優先順序排序的無界阻塞隊列。默認自然升序排列
DelayQueue 支援延時獲取元素的無界阻塞隊列。
SynchronousQueue 不存儲元素的阻塞隊列(可以看成是一個傳球手,負責把生產者執行緒處理的數據直接傳遞給消費者執行緒)
LinkedTransferQueue 由鏈表結構組成的無界阻塞隊列
LinkedBlockingDeque 由鏈表結構組成的雙向阻塞隊列(雙向隊列指的是可以從隊列的兩端插入和移出元素)

JAVA中的阻塞队列

Q5:實現原理:

  • 底層利用了ReentrantLock&Condition來實現自動加鎖和解鎖的功能
  • 如果想詳細了解阻塞隊列實現原理的源碼,筆者推薦一篇文章:Android並發學習之阻塞隊列

2.4 Android中的執行緒形態

2.4.1 AsyncTask

Q1:定義:一種輕量級的非同步任務類

在Android中實現非同步任務機制有兩種方式:Handler和AsyncTask

  • Handler機制存在的問題:程式碼相對臃腫;多任務同時執行時不易精確控制執行緒。
  • 引入AsyncTask的好處:創建非同步任務更簡單,直接繼承它可方便實現後台非同步任務的執行和進度的回調更新UI,而無需編寫任務執行緒和Handler實例就能完成相同的任務。

Q2:五個核心方法:

方法 運行執行緒 調用時刻 作用
onPreExecute() 主執行緒 在非同步任務執行之前被調用 可用於進行一些介面上的初始化操作
doInBackground() 子執行緒 非同步任務執行時 可用於處理所有的耗時任務。若需要更新UI需調用 publishProgress()
onProgressUpdate() 主執行緒 調用publishProgress()之後 可利用方法中攜帶的參數如Progress來對UI進行相應地更新
onPostExecute() 主執行緒 在非同步任務執行完畢並通過return語句返回時被調用 可利用方法中返回的數據來進行一些UI操作
onCancelled() 主執行緒 當非同步任務被取消時被調用 可用於做介面取消的更新

注意:

  • 不要直接調用上述方法
  • AsyncTask對象必須在主執行緒創建

Q3:開始和結束非同步任務的方法

  • execute()
  • 必須在主執行緒中調用
  • 作用:表示開始一個非同步任務
  • 注意:一個非同步對象只能調用一次execute()方法
  • cancel()
  • 必須在主執行緒中調用
  • 作用:表示停止一個非同步任務

Q4:工作原理:

  • 內部有一個靜態的Handler對象即InternalHandler
  • 作用:將執行環境從執行緒池切換到主執行緒;通過它來發送任務執行的進度以及執行結束等消息

  • 注意:必須在主執行緒中創建

  • 內部有兩個執行緒池:
  • SerialExecutor:用於任務的排隊,默認是串列的執行緒池
  • THREAD_POOL_EXECUTOR:用於真正執行任務
  • 排隊執行過程:
  • 把參數Params封裝為FutureTask對象,相當於Runnable
  • 調用SerialExecutor.execute()將FutureTask插入到任務隊列tasks
  • 若沒有正在活動的AsyncTask任務,則就會執行下一個AsyncTask任務。執行完畢後會繼續執行其他任務直到所有任務都完成。即默認使用串列方式執行任務。

執行流程圖:

AsyncTask工作原理

注意:AsyncTask不適用於進行特別耗時的後台任務,而是建議用執行緒池

如果想要了解具體源碼的讀者,筆者推薦一篇文章:Android AsyncTask完全解析,帶你從源碼的角度徹底理解

2.4.2 HandlerThread

Q1:定義:

HandlerThread是一個執行緒類,它繼承自Thread

與普通Thread的區別:具有消息循環的效果。原理:

  • 內部HandlerThread.run()方法中有Looper,通過Looper.prepare()來創建消息隊列,並通過Looper.loop()來開啟消息循環

Q2:實現方法

  • 實例化一個HandlerThread對象,參數是該執行緒的名稱
  • 通過 HandlerThread.start()開啟執行緒
  • 實例化一個Handler並傳入HandlerThread中的Looper對象,使得與HandlerThread綁定
  • 利用Handler即可執行非同步任務
  • 當不需要HandlerThread時,通過HandlerThread.quit()/quitSafely()方法來終止執行緒的執行
private HandlerThread myHandlerThread ;  private Handler handler ;  @Override  protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);     setContentView(R.layout.activity_main);     //實例化HandlerThread     myHandlerThread = new HandlerThread("myHandler") ;     //開啟HandlerThread     myHandlerThread.start();     //將Handler對象與HandlerThread執行緒綁定     handler =new Handler(myHandlerThread.getLooper()){         @Override          publicvoid handleMessage(Message msg) {             super.handleMessage(msg);              // 這裡接收Handler發來的消息,運行在handler_thread執行緒中              //TODO...          }      };       //在主執行緒給Handler發送消息     handler.sendEmptyMessage(1) ;     new Thread(new Runnable() {         @Override          publicvoid run() {             //在子執行緒給Handler發送數據             handler.sendEmptyMessage(2) ;          }      }).start();  }  @Override  protected void onDestroy() {     super.onDestroy();     //終止HandlerThread運行     myHandlerThread.quit() ;  }  

Q3:用途

  • 進行串列非同步通訊
  • 構造IntentService
  • 方便實現在子執行緒(工作執行緒)中使用Handler

Q4:原理:

  • 實際就是HandlerThread.run()裡面封裝了Looper.prepare()和Looper.loop(),以便能在子執行緒中使用Handler
  • 同時,HandlerThread.getLooper()中使用了wait()和synchronized程式碼塊,當Looper==NULL的時候,鎖住了當前的對象,那什麼時候喚醒等待呢?當然是在初始化完該執行緒關聯Looper對象的地方,也就是run()

想了解源碼的話,筆者推薦一篇文章:淺析HandlerThread

2.4.3 IntentService

Q1:定義:

IntentService是一個繼承自Service的抽象類

Q2:優點:

  • 相比於執行緒:由於是服務,優先順序比執行緒高,更不容易被系統殺死。因此較適合執行一些高優先順序的後台任務
  • 相比於普通Service:可自動創建子執行緒來執行任務,且任務執行完畢後自動退出

Q3:使用方法

  • 新建類並繼承IntentService,重寫onHandleIntent(),該方法:
  • 運行在子執行緒,因此可以進行一些耗時操作
  • 作用:從Intent參數中區分具體的任務並執行這些任務
  • 在配置文件中進行註冊
  • 在活動中利用Intent實現IntentService的啟動:
Intent intent = new Intent(this, MyService.class);  intent.putExtra("xxx",xxx);  startService(intent);//啟動服務

注意:無需手動停止服務,onHandleIntent()執行結束之後,IntentService會自動停止。

Q4:工作原理

  • 在IntentService.onCreate()里創建一個Thread對象即HandlerThread,利用其內部的Looper會實例化一個ServiceHandler
  • 任務請求的Intent會被封裝到Message並通過ServiceHandler發送給Looper的MessageQueue,最終在HandlerThread中執行
  • 在ServiceHandler.handleMessage()中會調用IntentService.onHandleIntent(),可在該方法中處理後台任務的邏輯,執行完畢後會調用stopSelf(),以實現自動停止

总体流程图

下面繼續來研究下:將Intent 傳遞給服務 & 依次插入到工作隊列中的流程

Intent传递流程

如果對IntentService的具體源碼感興趣的話,筆者推薦一篇文章:Android多執行緒:IntentService用法&源碼分析

2.5 執行緒池

Q1:優點

  • 重用執行緒池中的執行緒,避免執行緒的創建和銷毀帶來的性能消耗
  • 有效控制執行緒池的最大並發數,避免大量的執行緒之間因互相搶佔系統資源而導致阻塞現象
  • 進行執行緒管理,提供定時/循環間隔執行等功能

Q2:構造方法分析

  • 執行緒池的概念來源:Java中的Executor,它是一個介面
  • 執行緒池的真正實現:ThreadPoolExecutor,提供一系列參數來配置執行緒池
//構造參數  public ThreadPoolExecutor(int corePoolSize,                                int maximumPoolSize,                                long keepAliveTime,                                TimeUnit unit,                                BlockingQueue<Runnable> workQueue,                                ThreadFactory threadFactory,                                RejectedExecutionHandler handler) {
  • corePoolSize:核心執行緒數
  • 默認情況下,核心執行緒會在執行緒中一直存活

  • 當設置ThreadPoolExecutor的allowCoreThreadTimeOut屬性為

    A.true:表示核心執行緒閑置超過超時時長,會被回收

    B.false: 表示核心執行緒不會被回收,會在執行緒池中一直存活

  • maximumPoolSize:最大執行緒數

當活動執行緒數達到這個數值後,後續的任務將會被阻塞

  • keepAliveTime:非核心執行緒超時時間
  • 超過這個時長,閑置的非核心執行緒就會被回收
  • 當設置ThreadPoolExecutor的allowCoreThreadTimeTout屬性為true時,keepAliveTime對核心執行緒同樣有效
  • unit:用於指定keepAliveTime參數的時間單位

單位有:TimeUnit.MILLISECONDS、TimeUnit.SECONDS、TimeUnit.MINUTES等;

  • workQueue:任務隊列

通過執行緒池的execute()方法提交的Runnable對象會存儲在這個參數中

  • threadFactory:執行緒工廠,可創建新執行緒

一個介面,只有一個方法Thread newThread(Runnable r)

  • handler:在執行緒池無法執行新任務時進行調度

Q3:ThreadPoolExecutor的默認工作策略

处理流程

​ Q4:執行緒池的分類

名稱 含義 特點
FixThreadPool 執行緒數量固定的執行緒池,所有執行緒都是核心執行緒,當執行緒空閑時不會被回收 能快速響應外界請求
CachedThreadPool 執行緒數量不定的執行緒池(最大執行緒數為Integer.MAX_VALUE),只有非核心執行緒,空閑執行緒有超時機制,超時回收 適合於執行大量的耗時較少的任務
ScheduledThreadPool 核心執行緒數量固定,非核心執行緒數量不定 定時任務和固定周期的任務
SingleThreadExecutor 只有一個核心執行緒,可確保所有的任務都在同一個執行緒中按順序執行 無需處理執行緒同步問題

三.再聊聊AsyTask的不足

AsyncTask 看似十分美好,但實際上存在著非常多的不足,這些不足使得它逐漸退出了歷史舞台,因此如今已經被 RxJava、協程等新興框架所取代(PS:有機會希望能和大家一起探究下RxJava的源碼)

  • 生命周期

AsyncTask 沒有與 Activity、Fragment 的生命周期綁定,即使 Activity 被銷毀,它的 doInBackground 任務仍然會繼續執行

  • 取消任務

AsyncTask 的 cancel 方法的參數 mayInterruptIfRunning 存在的意義不大,並且它無法保證任務一定能取消,只能儘快讓任務取消(比如如果正在進行一些無法打斷的操作時,任務就仍然會運行)

  • 記憶體泄漏
  • 由於它沒有與 Activity 等生命周期進行綁定,因此它的生命周期仍然可能比 Activity 長
  • 如果將它作為 Activity 的非 static 內部類,則它會持有 Activity 的引用,導致 Activity 的記憶體無法釋放。(PS:與 Handler的記憶體泄漏問題類似,參考文章:進階之路 | 奇妙的Handler之旅)
  • 並行/串列

由於 AsyncTask 的串列和並行執行在多個版本上都進行了修改,所以當多個 AsyncTask 依次執行時,它究竟是串列還是並行執行取決於用戶手機的版本。具體修改如下:

A.Android 1.6 之前:各個 AsyncTask 按串列的順序進行執行

B.Android 3.0 之前:由於設計者認為串列執行效率太低,因此改為了並行執行,最多五個 AsyncTask 同時執行

C.Android 3.0 之後:由於之前的改動,很多應用出現了並發問題,因此引入 SerialExecutor 改回了串列執行,但對並行執行進行了支援


如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力

本文參考鏈接:

Tags: