進階之路|奇妙的Thread之旅

學習導圖:

進階之路|奇妙的Thread之旅

一。為什麼要學習Thread?

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

本篇文章將帶領大家由淺入深,從

執行緒的基礎

,談到

同步機制

,再講到

阻塞佇列

,接著提及

Android

中的執行緒形態

,最終一覽

執行緒池機制

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

二。核心知識點歸納

2.1 執行緒概述

Q1:

含義

執行緒是CPU排程的最小單位

注意與程序相區分

Q2:

特點

執行緒是一種

受限

的系統資源。即執行緒不可無限制的產生且執行緒的建立和銷燬都有一定的開銷

Q

:如何避免頻繁建立和銷燬執行緒所帶來的系統開銷?

A

:採用

執行緒池

,池中會快取一定數量的執行緒,進而達到效果

Q3:

分類

按用途分為兩類:

主執行緒

:一般一個程序只有一個主執行緒,主要處理

介面互動

相關的邏輯

子執行緒

:除主執行緒之外都是子執行緒,主要用於執行

耗時操作

按形態可分為三類:

AsyncTask:底層封裝了執行緒池和Handler,便於執行後臺任務以及在主執行緒中進行UI操作

HandlerThread:一種具有

訊息迴圈

的執行緒,其內部可使用Handler

IntentService:一種

非同步、會自動停止

的服務,內部採用HandlerThread和Handler

進階之路|奇妙的Thread之旅

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

Q4:

如何安全地終止執行緒?

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

比如:做

Android

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

APP

閃退

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

A1:

為啥不使用

stop

?

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

stop是透過立即丟擲ThreadDeath異常,來達到停止執行緒的目的,此異常丟擲有可能發生在任何一時間點,包括在catch、finally等語句塊中,但是此異常並不會引起程式退出

異常丟擲,導致執行緒會

釋放

全部所持有的

,極可能引起

執行緒安全

問題

A2:

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

示例DEMO:

進階之路|奇妙的Thread之旅

注意:這裡的變數是用

volatile

修飾,以保證

可見性

,關於

volatile

的知識,筆者將在下文為您詳細解析

A3:

採用

interrupt

來終止執行緒

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

進階之路|奇妙的Thread之旅

原理:

呼叫Thread物件的interrupt函式並不是立即中斷執行緒,只是將執行緒

中斷

狀態

標誌

設定為true

當執行緒執行中有呼叫其阻塞的函式時,阻塞函式呼叫之後,會不斷地輪詢檢測中斷狀態標誌是否為true,如果為true,則停止阻塞並丟擲InterruptedException異常,同時還會重置中斷狀態標誌,因此需要在catch程式碼塊中需呼叫interrupt函式,使執行緒再次處於中斷狀態

如果中斷狀態標誌為false,則繼續阻塞,直到阻塞正常結束

具體的

interrupt

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

2.2 同步機制

2.2.1

volatile

有時候僅僅為了讀寫一個或者兩個例項就使用同步

synchronized

的話,顯得開銷過大

volatile

為例項域的同步訪問提供了免鎖的機制

Q1:

先從

Java

記憶體模型聊起

Java記憶體模型定義了

本地記憶體和主存

之間的抽象關係

執行緒之間的

共享變數

儲存在

主存

每個執行緒都有一個

私有的本地記憶體

(工作記憶體),本地記憶體中儲存了該執行緒共享變數的

副本

進階之路|奇妙的Thread之旅

執行緒之間通訊的步驟

執行緒

A

將其

本地記憶體

更新過的共享變數重新整理到主存

中去

執行緒

B

主存

中去

讀取

執行緒A之前已

更新過的共享變數

Q2:

原子性、可見性和有序性

瞭解多少

a1:

原子性

Atomicity

定義:原子性操作就是指這些操作是不可中斷的,要做一定做完,要麼就沒有執行

對基本資料型別變數的

讀取和賦值

操作是原子性操作

注意:這裡的

賦值

操作是指

將數字賦值給某個變數

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

x=3; //原子性操作

y=x; //非原子性操作 原因:包括2個操作:先讀取x的值,再將x的值寫入工作記憶體

x++; //非原子性操作 原因:包括3個操作:讀取x的值、對x的值進行加1、向工作記憶體寫入新值

volatile不支援原子性

保證整塊程式碼原子性(例如i++)的方法:藉助於synchronized和Lock,以及併發包下的atomic的原子操作類

a2:

可見性

Visibility

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

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

原因:當一個變數被

volatile

修飾時,那麼對它的修改會

立刻重新整理到主存

,同時使

其它執行緒的工作記憶體

中對此變數的

快取行失效

,因此需要讀取該變數時,會去記憶體中讀取新值

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

a3:

有序性

Ordering

指令重排序

的定義:大多數現代微處理器都會採用將指令亂序執行的方法, 在條件允許的情況下, 直接運行當前有能力立即執行的後續指令, 避開獲取下一條指令所需資料時造成的等待

什麼時候不進行

指令重排序

符合資料依賴性:

Copy

//x對a有依賴a = 1;x = a;

as-if-serial

語義:不管怎麼重排序, 單執行緒程式的執行結果不能被改變

程式順序原則

如果A

happens-before

B

如果B

happens-before

C

那麼A

happens-before

C

這就是

happens-before

傳遞性

volatile透過

禁止指令重排序

的方式來保證有序性

Q3:

應用場景有哪些?

狀態量標記

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

DCL

避免指令重排序:

假定建立一個物件需要:

申請記憶體

初始化

instance

指向分配的那塊記憶體

上面的2和3操作是有可能重排序的, 如果3重排序到2的前面, 這時候2操作還沒有執行,

instance!=null

, 當然不是安全的

進階之路|奇妙的Thread之旅

Q4:原理:

如果把加入volatile關鍵字的程式碼和未加入volatile關鍵字的程式碼都生成彙編程式碼,會發現加入volatile關鍵字的程式碼會多出一個lock字首指令

lock字首指令實際相當於一個

記憶體屏障

,記憶體屏障提供了以下功能:

重排序時不能把後面的指令重排序到記憶體屏障之前的位置

使得本

CPU

Cache

寫入記憶體

寫入動作也會引起別的

CPU

或者別的核心無效化其

Cache

,相當於讓新寫入的值對別的執行緒可見

2.2.2 重入鎖與條件物件

synchronized

關鍵字自動為我們提供了鎖以及相關的條件,大多數需要顯式鎖的時候,使用

synchronized

非常方便,但是當我們瞭解了重入鎖和條件物件時,能更好地理解

synchronized

和阻塞佇列

Q1:

重入鎖的定義

可重入鎖指的是可重複可遞迴呼叫的鎖,在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖,這樣的鎖就叫做可重入鎖

ReentrantLock和synchronized都是可重入鎖

重複呼叫鎖的

DEMO

如下:

進階之路|奇妙的Thread之旅

Q2:什麼是條件物件Condition?

條件物件來管理那些已經

獲得了一個鎖

但是卻

不能做有用工作

的執行緒,條件物件又被稱作條件變數

一般要配合ReentrantLock使用,用Condition。await()可以

阻塞當前執行緒

,並

放棄鎖

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

支付寶轉賬

的例子(支付寶打錢,狗頭。jpg)

場景是這樣的:

進階之路|奇妙的Thread之旅

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

2.2.3 synchronized

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

同步程式碼塊

同步方法

Q2:synchronized與ReentrantLock的關係

兩者都是重入鎖

兩者有些方法互相對應

wait

等價於

condition。await()

notifyAll

等價於

condition。signalAll()

Q3:使用場景對比

進階之路|奇妙的Thread之旅

2.3 阻塞佇列

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

Q1:

定義

阻塞佇列BlockingQueue是一個支援兩個附加操作的佇列。這兩個附加的操作是:

在佇列為空時,獲取元素的執行緒會等待佇列變為非空

當佇列滿時,儲存元素的執行緒會等待佇列可用

Q2:

使用場景

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

Q3:

核心方法

進階之路|奇妙的Thread之旅

Q4:

JAVA

中的阻塞佇列

進階之路|奇妙的Thread之旅

進階之路|奇妙的Thread之旅

Q5:

實現原理:

底層利用了ReentrantLock&Condition來實現自動加鎖和解鎖的功能

如果想詳細瞭解阻塞佇列實現原理的原始碼,筆者推薦一篇文章:Android併發學習之阻塞佇列

2.4 Android中的執行緒形態

2.4.1 AsyncTask

Q1:

定義

:一種輕量級的

非同步

任務類

Android

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

AsyncTask

Handler

機制存在的

問題

:程式碼相對臃腫;多工同時執行時不易精確控制執行緒。

引入

AsyncTask

好處

:建立非同步任務更簡單,直接繼承它可方便實現後臺非同步任務的執行和進度的回撥更新UI,而無需編寫任務執行緒和

Handler

例項就能完成相同的任務。

Q2:

五個核心方法:

進階之路|奇妙的Thread之旅

注意:

不要直接呼叫上述方法

AsyncTask物件必須在

主執行緒

建立

Q3:

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

execute()

必須在

主執行緒

中呼叫

作用:表示開始一個非同步任務

注意:一個非同步物件只能呼叫一次execute()方法

cancel()

必須在

主執行緒

中呼叫

作用:表示停止一個非同步任務

Q4:工作原理:

內部有一個靜態的Handler物件即InternalHandler

作用:將執行環境從執行緒池切換到主執行緒;透過它來發送任務執行的進度以及執行結束等訊息

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

內部有兩個執行緒池:

SerialExecutor

:用於任務的排隊,預設是

序列

的執行緒池

THREAD_POOL_EXECUTOR

:用於真正執行任務

排隊執行過程:

把引數

Params

封裝為

FutureTask

物件,相當於

Runnable

呼叫

SerialExecutor。execute()

FutureTask

插入到任務佇列

tasks

若沒有正在活動的

AsyncTask

任務,則就會執行下一個

AsyncTask

任務。執行完畢後會繼續執行其他任務直到所有任務都完成。即預設使用

序列

方式執行任務。

執行流程圖:

進階之路|奇妙的Thread之旅

注意

: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()方法來終止執行緒的執行

進階之路|奇妙的Thread之旅

進階之路|奇妙的Thread之旅

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(),以實現自動停止

進階之路|奇妙的Thread之旅

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

如果對

IntentService

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

2.5 執行緒池

Q1:

優點

重用

執行緒池中的執行緒,避免執行緒的建立和銷燬帶來的效能消耗

有效控制執行緒池的最大併發數,避免大量的執行緒之間因互相搶佔系統資源而導致阻塞現象

進行

執行緒管理

,提供定時/迴圈間隔執行等功能

Q2:

構造方法分析

執行緒池的概念來源:Java中的

Executor

,它是一個介面

執行緒池的真正實現:

ThreadPoolExecutor

,提供一系列引數來配置執行緒池

進階之路|奇妙的Thread之旅

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

的預設工作策略

進階之路|奇妙的Thread之旅

Q4:

執行緒池的分類

進階之路|奇妙的Thread之旅

三。再聊聊

AsyTask

的不足#

AsyncTask

看似十分美好,但實際上存在著非常多的

不足

,這些不足使得它逐漸退出了歷史舞臺,因此如今已經被

RxJava

、協程等新興框架所取代

生命週期

AsyncTask沒有與 Activity、Fragment的生命週期繫結,即使Activity被銷燬,它的doInBackground 任務仍然會繼續執行

取消任務

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

記憶體洩漏

由於它沒有與 Activity等生命週期進行繫結,因此它的生命週期仍然可能比 Activity 長

如果將它作為Activity的非static內部類,則它會持有Activity的引用,導致Activity 的記憶體無法釋放。

並行/序列

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

A。Android 1。6 之前:各個 AsyncTask 按序列的順序進行執行

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

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