學習導圖:
一。為什麼要學習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:
注意:這裡的變數是用
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不支援原子性
保證整塊程式碼原子性(例如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
, 當然不是安全的
Q4:原理:
如果把加入volatile關鍵字的程式碼和未加入volatile關鍵字的程式碼都生成彙編程式碼,會發現加入volatile關鍵字的程式碼會多出一個lock字首指令
lock字首指令實際相當於一個
記憶體屏障
,記憶體屏障提供了以下功能:
重排序時不能把後面的指令重排序到記憶體屏障之前的位置
使得本
CPU
的
Cache
寫入記憶體
寫入動作也會引起別的
CPU
或者別的核心無效化其
Cache
,相當於讓新寫入的值對別的執行緒可見
2.2.2 重入鎖與條件物件
synchronized
關鍵字自動為我們提供了鎖以及相關的條件,大多數需要顯式鎖的時候,使用
synchronized
非常方便,但是當我們瞭解了重入鎖和條件物件時,能更好地理解
synchronized
和阻塞佇列
Q1:
重入鎖的定義
可重入鎖指的是可重複可遞迴呼叫的鎖,在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖,這樣的鎖就叫做可重入鎖
ReentrantLock和synchronized都是可重入鎖
重複呼叫鎖的
DEMO
如下:
Q2:什麼是條件物件Condition?
條件物件來管理那些已經
獲得了一個鎖
但是卻
不能做有用工作
的執行緒,條件物件又被稱作條件變數
一般要配合ReentrantLock使用,用Condition。await()可以
阻塞當前執行緒
,並
放棄鎖
Q3:下面說明重入鎖與條件物件如何協同使用
用
支付寶轉賬
的例子(支付寶打錢,狗頭。jpg)
場景是這樣的:
想要更深一步瞭解重入鎖的讀者,可以看下這篇文章:究竟什麼是可重入鎖?
2.2.3 synchronized
Q1:synchronized有哪幾種實現方式?
同步程式碼塊
同步方法
Q2:synchronized與ReentrantLock的關係
兩者都是重入鎖
兩者有些方法互相對應
wait
等價於
condition。await()
notifyAll
等價於
condition。signalAll()
Q3:使用場景對比
2.3 阻塞佇列
為了更好地理解執行緒池的知識,我們需要了解下阻塞佇列
Q1:
定義
阻塞佇列BlockingQueue是一個支援兩個附加操作的佇列。這兩個附加的操作是:
在佇列為空時,獲取元素的執行緒會等待佇列變為非空
當佇列滿時,儲存元素的執行緒會等待佇列可用
Q2:
使用場景
:
阻塞佇列常用於生產者和消費者的場景,生產者是往佇列裡新增元素的執行緒,消費者是從佇列裡拿元素的執行緒。阻塞佇列就是生產者存放元素的容器,而消費者也只從容器裡拿元素。
Q3:
核心方法
Q4:
JAVA
中的阻塞佇列
Q5:
實現原理:
底層利用了ReentrantLock&Condition來實現自動加鎖和解鎖的功能
如果想詳細瞭解阻塞佇列實現原理的原始碼,筆者推薦一篇文章:Android併發學習之阻塞佇列
2.4 Android中的執行緒形態
2.4.1 AsyncTask
Q1:
定義
:一種輕量級的
非同步
任務類
在
Android
中實現非同步任務機制有兩種方式:Handler和
AsyncTask
Handler
機制存在的
問題
:程式碼相對臃腫;多工同時執行時不易精確控制執行緒。
引入
AsyncTask
的
好處
:建立非同步任務更簡單,直接繼承它可方便實現後臺非同步任務的執行和進度的回撥更新UI,而無需編寫任務執行緒和
Handler
例項就能完成相同的任務。
Q2:
五個核心方法:
注意:
不要直接呼叫上述方法
AsyncTask物件必須在
主執行緒
建立
Q3:
開始和結束非同步任務的方法
execute()
必須在
主執行緒
中呼叫
作用:表示開始一個非同步任務
注意:一個非同步物件只能呼叫一次execute()方法
cancel()
必須在
主執行緒
中呼叫
作用:表示停止一個非同步任務
Q4:工作原理:
內部有一個靜態的Handler物件即InternalHandler
作用:將執行環境從執行緒池切換到主執行緒;透過它來發送任務執行的進度以及執行結束等訊息
注意:必須在主執行緒中建立
內部有兩個執行緒池:
SerialExecutor
:用於任務的排隊,預設是
序列
的執行緒池
THREAD_POOL_EXECUTOR
:用於真正執行任務
排隊執行過程:
把引數
Params
封裝為
FutureTask
物件,相當於
Runnable
呼叫
SerialExecutor。execute()
將
FutureTask
插入到任務佇列
tasks
若沒有正在活動的
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()方法來終止執行緒的執行
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傳遞給服務 & 依次插入到工作佇列中的流程
如果對
IntentService
的具體原始碼感興趣的話,筆者推薦一篇文章:Android多執行緒:IntentService用法&原始碼分析
2.5 執行緒池
Q1:
優點
重用
執行緒池中的執行緒,避免執行緒的建立和銷燬帶來的效能消耗
有效控制執行緒池的最大併發數,避免大量的執行緒之間因互相搶佔系統資源而導致阻塞現象
進行
執行緒管理
,提供定時/迴圈間隔執行等功能
Q2:
構造方法分析
執行緒池的概念來源:Java中的
Executor
,它是一個介面
執行緒池的真正實現:
ThreadPoolExecutor
,提供一系列引數來配置執行緒池
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:
執行緒池的分類
三。再聊聊
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 改回了序列執行,但對並行執行進行了支援