第5章 多線程編程
5.1 線程基礎
5.1.1 如何創(chuàng)建線程
在java要創(chuàng)建線程,一般有==兩種方式==:
1)繼承Thread類
2)實現(xiàn)Runnable接口
1. 繼承Thread類
繼承Thread類,重寫run方法,在run方法中定義需要執(zhí)行的任務。
class MyThread extends Thread{
private static int num = 0;
public MyThread(){
num++;
}
@Override
public void run() {
System.out.println("主動創(chuàng)建的第"+num+"個線程");
}
}
創(chuàng)建好線程類之后,就可以創(chuàng)建線程對象了,然后通過start()方法去啟動線程。不是調(diào)用run()方法啟動線程,run方法中只是定義需要執(zhí)行的任務,如果調(diào)用run方法,即相當于在主線程中執(zhí)行run方法,跟普通的方法調(diào)用沒有任何區(qū)別,此時并不會創(chuàng)建一個新的線程來執(zhí)行定義的任務。
2. 實現(xiàn)Runnable接口
實現(xiàn)Runnable接口必須重寫其run方法。
class MyRunnable implements Runnable{
public MyRunnable() {
}
@Override
public void run() {
System.out.println("子線程ID:"+Thread.currentThread().getId());
}
}
public class Test {
public static void main(String[] args) {
System.out.println("主線程ID:"+Thread.currentThread().getId());
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
這種方式必須將Runnable作為Thread類的參數(shù),然后通過Thread的start方法來創(chuàng)建一個新線程來執(zhí)行該子任務。如果調(diào)用Runnable的run方法的話,是不會創(chuàng)建新線程的,這根普通的方法調(diào)用沒有任何區(qū)別。
實現(xiàn)Runnable接口相比繼承Thread類有如下==優(yōu)勢==:
1、可以避免由于Java的單繼承特性而帶來的局限。
2、代碼能夠被多個線程共享,代碼與數(shù)據(jù)是獨立的,適合多個線程去處理同一資源的情況 。
5.1.2 線程的狀態(tài)
線程包括以下這==7個狀態(tài)==:創(chuàng)建(new)、就緒(runnable)、運行(running)、阻塞(blocked)、time wating、wating、消亡(dead)。
當需要新起一個線程來執(zhí)行某個子任務時,就創(chuàng)建了一個線程。但是線程創(chuàng)建之后,不會立即進入就緒狀態(tài),因為線程的運行需要一些條件(比如內(nèi)存資源,程序計數(shù)器、Java棧、本地方法棧都是線程私有的,所以需要為線程分配一定的內(nèi)存空間),只有線程運行需要的所有條件滿足了,才進入就緒狀態(tài)。
當線程進入就緒狀態(tài)后,不代表立刻就能獲取CPU執(zhí)行時間,也許此時CPU正在執(zhí)行其他的事情,因此它要等待。當?shù)玫紺PU執(zhí)行時間之后,線程便真正進入運行狀態(tài)。
線程在運行狀態(tài)過程中,可能有多個原因?qū)е庐斍熬€程不繼續(xù)運行下去,比如用戶主動讓線程睡眠(睡眠一定的時間之后再重新執(zhí)行)、用戶主動讓線程等待,或者被同步塊給阻塞,此時就對應著多個狀態(tài):time wating、wating、阻塞。
當由于突然中斷或者子任務執(zhí)行完畢,線程就會被消亡。
5.1.3 上下文切換
對于單核CPU來說(對于多核CPU,此處就理解為一個核),CPU在一個時刻只能運行一個線程,當在運行一個線程的過程中轉(zhuǎn)去運行另外一個線程,這個叫做==線程上下文切換==(對于進程也是類似)。
由于可能當前線程的任務并沒有執(zhí)行完畢,所以在切換時需要保存線程的運行狀態(tài),以便下次重新切換回來時能夠繼續(xù)切換之前的狀態(tài)運行。舉個簡單的例子:比如一個線程A正在讀取一個文件的內(nèi)容,正讀到文件的一半,此時需要暫停線程A,轉(zhuǎn)去執(zhí)行線程B,當再次切換回來執(zhí)行線程A的時候,我們不希望線程A又從文件的開頭來讀取。因此需要記錄線程A的運行狀態(tài),那么會記錄哪些數(shù)據(jù)呢?因為下次恢復時需要知道在這之前當前線程已經(jīng)執(zhí)行到哪條指令了,所以需要記錄程序計數(shù)器的值,另外比如說線程正在進行某個計算的時候被掛起了,那么下次繼續(xù)執(zhí)行的時候需要知道之前掛起時變量的值時多少,因此需要記錄CPU寄存器的狀態(tài)。所以一般來說,線程上下文切換過程中會記錄程序計數(shù)器、CPU寄存器狀態(tài)等數(shù)據(jù)。說簡單點:對于線程的上下文切換實際上就是存儲和恢復CPU狀態(tài)的過程,它使得線程執(zhí)行能夠從中斷點恢復執(zhí)行。
雖然多線程可以使得任務執(zhí)行的效率得到提升,但是由于在線程切換時同樣會帶來一定的開銷代價,并且多個線程會導致系統(tǒng)資源占用的增加,所以在進行多線程編程時要注意這些因素。
5.1.4 理解中斷
每一個線程都有一個用來表明當前線程是否請求中斷的boolean類型標志,當一個線程調(diào)用interrupt()方法時,線程的中斷標志將被設置為true。我們可以通過調(diào)用Thread.currentThread().isInterrupted()或者Thread.interrupted()來檢測線程的中斷標志是否被置位。這兩個方法的區(qū)別:前者是線程對象的方法,調(diào)用它后==不清除==線程中斷標志位;后者是Thread的靜態(tài)方法,調(diào)用它會==清除==線程中斷標志位。
所以說調(diào)用線程的interrupt()方法不會中斷一個正在運行的線程,只是設置了一個線程中斷標志位,如果在程序中不檢測線程中斷標志位,那么即使設置了中斷標志位為true,線程也一樣照常運行。
一般來說中斷線程分為三種情況:
(1):中斷非阻塞線程
(2):中斷阻塞線程
(3):不可中斷線程
1. 中斷非阻塞線程
中斷非阻塞線程通常有兩種方式:
(1) 采用線程共享變量
這種方式比較簡單可行,需要注意的一點是共享變量必須設置為volatile,這樣才能保證修改后其他線程立即可見。
public class InterruptThreadTest extends Thread{
// 設置線程共享變量
volatile boolean isStop = false;
public void run() {
while(!isStop) {//無限循環(huán)一直執(zhí)行
System.out.println(Thread.currentThread().getName() + "is running");
}
if (isStop) {//此時跳出無限循環(huán),線程執(zhí)行完畢
System.out.println(Thread.currentThread().getName() + "is interrupted");
}
}
public static void main(String[] args) {
InterruptThreadTest itt = new InterruptThreadTest();
itt.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 線程共享變量設置為true
itt.isStop = true;
}
}
(2) 采用中斷機制
public class InterruptThreadTest2 extends Thread{
public void run() {
// 這里調(diào)用的是非清除中斷標志位的isInterrupted方法
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is running");
}
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is interrupted");
}
}
public static void main(String[] args) {
InterruptThreadTest2 itt = new InterruptThreadTest2();
itt.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 設置線程的中斷標志位
itt.interrupt();
}
}
2. 中斷阻塞線程
當線程調(diào)用Thread.sleep()、Thread.join()、object.wait()再或者調(diào)用阻塞的I/O操作方法時,都會使得當前線程進入阻塞狀態(tài)。那么此時如果在線程處于阻塞狀態(tài)下調(diào)用interrupt()方法會拋出一個異常,并且會清除線程中斷標志位(設置為false)。這樣一來線程就能退出阻塞狀態(tài)。
代碼實例如下:
public class InterruptThreadTest3 extends Thread{
public void run() {
// 這里調(diào)用的是非清除中斷標志位的isInterrupted方法
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " is running");
try {
System.out.println(Thread.currentThread().getName() + " Thread.sleep begin");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " Thread.sleep end");
} catch (InterruptedException e) {
//由于調(diào)用sleep()方法會清除狀態(tài)標志位 所以這里需要再次重置中斷標志位 否則線程會繼續(xù)運行下去
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is interrupted");
}
}
public static void main(String[] args) {
InterruptThreadTest3 itt = new InterruptThreadTest3();
itt.start();
try {
Thread.sleep(5000);//讓出cpu
} catch (InterruptedException e) {
e.printStackTrace();
}
// 設置線程的中斷標志位
itt.interrupt();
}
}
需要注意的地方就是 Thread.sleep()、Thread.join()、object.wait()這些方法,會檢測線程中斷標志位,如果發(fā)現(xiàn)中斷標志位為true則==拋出異常并且將中斷標志位設置為false==。所以while循環(huán)之后每次調(diào)用阻塞方法后都要在捕獲異常之后,調(diào)用Thread.currentThread().interrupt()重置狀態(tài)標志位。
3. 不可中斷線程
有一種情況是線程不能被中斷的,就是調(diào)用synchronized關(guān)鍵字獲取到了鎖的線程。
5.1.5 Thread類常用方法
1. sleep方法
sleep(long time)sleep(long millis, int nanos)
==不會釋放鎖==,相當于讓線程睡眠,讓出CPU,必須處理InterruptedException異常。當線程睡眠時間滿后,不一定會立即得到執(zhí)行,因為此時可能CPU正在執(zhí)行其他的任務。所以說調(diào)用sleep方法相當于讓線程進入阻塞狀態(tài)。
2. yield方法
- `yield()
==不會釋放鎖==,不能控制具體的交出CPU的時間。直接用Thread類調(diào)用,讓出CPU執(zhí)行權(quán)給同等級的線程,如果沒有相同級別的線程在等待CPU的執(zhí)行權(quán),則該線程繼續(xù)執(zhí)行。
注意,調(diào)用yield方法==并不會讓線程進入阻塞狀態(tài),而是讓線程重回就緒狀態(tài)==,它只需要等待重新獲取CPU執(zhí)行時間,這一點是和sleep方法不一樣的。
3. join方法
-
join()等待thread執(zhí)行完畢 -
join(long millis)等待一定的時間 join(long millis,int nanoseconds)
==釋放鎖==,如果在一個線程A中調(diào)用另一個線程B的join方法,線程A將會等待線程B執(zhí)行完畢后或等待一定的時間再執(zhí)行。實際上調(diào)用join方法是調(diào)用了Object的wait()方法。wait方法會讓線程進入阻塞狀態(tài),并且會釋放線程占有的鎖,并交出CPU執(zhí)行權(quán)限。
4. interrupt方法
interrupt()
即中斷的意思。單獨調(diào)用==interrupt方法可以使得處于阻塞狀態(tài)的線程拋出一個異常==,也就說,它可以用來中斷一個正處于阻塞狀態(tài)的線程;直接調(diào)用interrupt方法不能中斷正在運行中的線程。
5. 其他方法
-
getId()用來得到線程ID -
getName()和setName(String threadName)用來得到或者設置線程名稱。 -
getPriority()和setPriority(int priority)用來獲取和設置線程優(yōu)先級。 -
setDaemon(boolean isDaemon)和isDaemon()用來設置線程是否成為守護線程和判斷線程是否是守護線程。 -
Thread.currentThread()獲取當前線程
守護線程和用戶線程的區(qū)別:
守護線程:依賴于創(chuàng)建它的線程。舉個簡單的例子:如果在main線程中創(chuàng)建了一個守護線程,當main方法運行完畢之后,守護線程也會隨著消亡。 在JVM中,像垃圾收集器線程就是守護線程。
用戶線程:不依賴創(chuàng)建它的線程,會一直運行完畢。
5.2 同步
5.2.1 同步方法
public synchronized void methodA() {
System.out.println("methodA.....");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
5.2.2 同步代碼塊
public void methodB() {
synchronized(this) {
System.out.pritntln("methodB.....");
}
}
5.2.3 volatite
1. Java內(nèi)存模型
在java中,所有實例域、靜態(tài)域和數(shù)組元素存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享(本文使用“共享變量”這個術(shù)語代指實例域,靜態(tài)域和數(shù)組元素)。
Java內(nèi)存模型(本文簡稱為JMM)定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存中,每個線程都有一個私有的本地內(nèi)存(JMM的一個抽象概念),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。 線程對變量的所有操作都必須在本地內(nèi)存中進行,而不能直接對主內(nèi)存進行操作。并且每個線程不能訪問其他線程的本地內(nèi)存。

從上圖來看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個步驟:
首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。
2. 并發(fā)編程的三個概念
并發(fā)編程需要處理的兩個關(guān)鍵問題:線程通信和線程同步。
線程通信的兩種方式:共享內(nèi)存和消息傳遞。共享內(nèi)存:線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進行通信。消息傳遞:線程之間必須通過明確的發(fā)送消息來顯式進行通信。
要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。
(1) 原子性
一個操作或者多個操作要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
(2) 可見性
當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
(3) 有序性
程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
指令重排序:處理器為了提高程序運行效率,可能會對輸入代碼進行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。不會影響單個線程的執(zhí)行,但是會影響到線程并發(fā)執(zhí)行的正確性。
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會以為初始化工作已經(jīng)完成,那么就會跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時context并沒有被初始化,就會導致程序出錯。
3. JAVA語言對于原子性,可見性,有序性的保證
原子性
基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作
請分析以下哪些操作是原子性操作:
x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
語句1是直接將數(shù)值10賦值給x,也就是說線程執(zhí)行這個語句的會直接將數(shù)值10寫入到本地內(nèi)存中。原子性操作。
語句2實際上包含2個操作,它先要去讀取x的值,再將x的值寫入本地內(nèi)存,雖然讀取x的值以及將x的值寫入本地內(nèi)存這2個操作都是原子性操作,但是合起來就不是原子性操作了。同樣的,x++和 x = x+1包括3個操作:讀取x的值,進行加1操作,寫入新的值。
可見性
提供了volatile關(guān)鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內(nèi)存中讀取新值。
有序性
可以通過volatile保證部分有序。
synchronized也可以保證有序性。
4. volatile關(guān)鍵字
volatile 變量可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 塊相比,volatile 變量所需的編碼較少,并且運行時開銷也較少,但是它所能實現(xiàn)的功能也僅是synchronized 的一部分。特點如下:
- 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
- 禁止進行指令重排序。volatile能在一定程度上保證有序性。
volatile關(guān)鍵字禁止指令重排序有兩層意思:
1)當程序執(zhí)行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進行,且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行;
2)在進行指令優(yōu)化時,不能將在對volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。 - 不能保證原子性
只能在有限的一些情形下使用 volatile變量替代鎖。要使volatile變量提供理想的線程安全,必須同時滿足下面兩個條件:
- 對變量的寫操作不依賴于當前值。
- 該變量沒有包含在具有其他變量的不變式中。
(1) 狀態(tài)標志
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
(2) 雙重檢查
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;
}
}
5.3 阻塞隊列
為了更好的理解線程池,本節(jié)學習阻塞隊列。
5.3.1 BlockingQueue
1. 認識BlockingQueue
- 在新增的Concurrent包中,高效且==線程安全==
- 用來==處理消費者生產(chǎn)者問題==。在多線程領域:所謂阻塞,在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒。
2. 核心方法
-
put(anObject)把anObject加到BlockingQueue里,如果沒有空間,則調(diào)用此方法的線程被阻塞直到BlockingQueue里面有空間再繼續(xù)。 -
offer(anObject)存數(shù)據(jù),如果可以容納,則返回true,否則返回false(不阻塞當前執(zhí)行方法的線程)。 -
offer(E o, long timeout, TimeUnit unit)可以設定等待的時間,如果在指定的時間內(nèi),還不能往隊列中加入,則返回失敗。
-
take()取走BlockingQueue里排在首位的對象,若BlockingQueue為空,阻塞進入等待狀態(tài)直到BlockingQueue有新的數(shù)據(jù)被加入。 -
poll(time)取走排在首位的對象,若不能立即取出,則可以等time參數(shù)規(guī)定的時間,取不到時返回null。 -
poll(long timeout, TimeUnit unit)取出一個隊首的對象,如果在指定時間內(nèi),隊列一旦有數(shù)據(jù)可取,則立即返回隊列中的數(shù)據(jù)。否則知道時間超時還沒有數(shù)據(jù)可取,返回失敗。 -
drainTo()一次性從BlockingQueue獲取所有可用的數(shù)據(jù)對象(還可以指定獲取數(shù)據(jù)的個數(shù)), 通過該方法,可以提升獲取數(shù)據(jù)效率;不需要多次分批加鎖或釋放鎖。
5.3.2 BlockingQueue實現(xiàn)子類
公平鎖:配合一個FIFO隊列來阻塞多余的生產(chǎn)者和消費者,從而體系整體的公平策略;
非公平鎖:配合一個LIFO隊列來管理多余的生產(chǎn)者和消費者,如果生產(chǎn)者和消費者的處理速度有差距,則很容易出現(xiàn)饑渴的情況,即可能有某些生產(chǎn)者或者是消費者的數(shù)據(jù)永遠都得不到處理。
1. ArrayBlockingQueue
- 基于數(shù)組實現(xiàn),內(nèi)部維護了一個定長數(shù)組和兩個整形變量,分別緩存著隊列中的數(shù)據(jù)對象及標識隊列的頭部和尾部在數(shù)組中的位置。生產(chǎn)和消費時不會產(chǎn)生或銷毀任何額外的對象實例。
- 在放入數(shù)據(jù)和獲取數(shù)據(jù),都是共用同一個鎖對象,兩者==無法并行運行==。
- 在創(chuàng)建時,默認采用非公平鎖??梢钥刂茖ο蟮膬?nèi)部鎖是否采用公平鎖。
2. LinkedBlockingQueue
- 基于鏈表實現(xiàn),也維持著一個數(shù)據(jù)緩沖隊列(該隊列由一個鏈表構(gòu)成)。生產(chǎn)和消費的時候會產(chǎn)生Node對象。
- 生產(chǎn)者端和消費者端分別采用了獨立的鎖來控制數(shù)據(jù)同步,高并發(fā)的情況下生產(chǎn)者和消費者==可以并行==地操作隊列中的數(shù)據(jù),以此來提高整個隊列的并發(fā)性能。
- 構(gòu)造一個LinkedBlockingQueue對象,而沒有指定其容量大小,LinkedBlockingQueue會默認一個類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生產(chǎn)者的速度一旦大于消費者的速度,也許還沒有等到隊列滿阻塞產(chǎn)生,系統(tǒng)內(nèi)存就有可能已被消耗殆盡了。
LinkedBlockingQueue和ArrayBlockingQueue的異同
相同:
最常用的阻塞隊列,當隊列為空,消費者線程被阻塞;當隊列裝滿,生產(chǎn)者線程被阻塞;
區(qū)別:
a. 底層實現(xiàn)機制不同:LinkedBlockingQueue基于鏈表實現(xiàn),在生產(chǎn)和消費的時候,需要創(chuàng)建Node對象進行插入或移除,大批量數(shù)據(jù)的系統(tǒng)中,其對于GC的壓力會比較大;而ArrayBlockingQueue內(nèi)部維護了一個數(shù)組,在生產(chǎn)和消費的時候,是直接將枚舉對象插入或移除的,不會產(chǎn)生或銷毀任何額外的對象實例。
b. LinkedBlockingQueue中的消費者和生產(chǎn)者是不同的鎖,而ArrayBlockingQueue生產(chǎn)者和消費者使用的是同一把鎖;
c. LinkedBlockingQueue有默認的容量大小為:Integer.MAX_VALUE,當然也可以傳入指定的容量大小;ArrayBlockingQueue在初始化的時候,必須傳入一個容量大小的值。
3. PriorityBlockingQueue
基于優(yōu)先級的==無界隊列==,==存儲的對象必須是實現(xiàn)Comparable接口==。隊列通過這個接口的compare方法確定對象的priority。越小優(yōu)先級越高,優(yōu)先級越高,越優(yōu)先取出。但需要注意的是PriorityBlockingQueue并==不會阻塞數(shù)據(jù)生產(chǎn)者==,而只會在沒有可消費的數(shù)據(jù)時,==阻塞數(shù)據(jù)的消費者==。因此使用的時候要特別注意,生產(chǎn)者生產(chǎn)數(shù)據(jù)的速度絕對不能快于消費者消費數(shù)據(jù)的速度,否則時間一長,會最終耗盡所有的可用堆內(nèi)存空間。在實現(xiàn)PriorityBlockingQueue時,內(nèi)部控制線程同步的鎖采用的是公平鎖。
使用案例
4. DelayQueue
==無界隊列==,只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。一個沒有大小限制的隊列,因此往隊列中插入數(shù)據(jù)的操作(生產(chǎn)者)永遠不會被阻塞,而只有獲取數(shù)據(jù)的操作(消費者)才會被阻塞。
使用場景:DelayQueue使用場景較少,但都相當巧妙,常見的例子比如使用一個DelayQueue來管理一個超時未響應的連接隊列。
5. SynchronousQueue
- 一個不存儲元素的阻塞隊列,可以理解為容量為0。==每個插入(移除)操作必須等待另一個線程的移除(插入)操作==??梢赃@樣來理解:生產(chǎn)者和消費者互相等待對方,握手,然后一起離開。類似于無中介的直接交易,有點像原始社會中的生產(chǎn)者和消費者,生產(chǎn)者拿著產(chǎn)品去集市銷售給產(chǎn)品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產(chǎn)者,如果一方?jīng)]有找到合適的目標,那么對不起,大家都在集市等待。相對于有緩沖的BlockingQueue來說,少了一個中間經(jīng)銷商的環(huán)節(jié)(緩沖區(qū)),如果有經(jīng)銷商,生產(chǎn)者直接把產(chǎn)品批發(fā)給經(jīng)銷商,而無需在意經(jīng)銷商最終會將這些產(chǎn)品賣給那些消費者,由于經(jīng)銷商可以庫存一部分商品,因此相對于直接交易模式,總體來說采用中間經(jīng)銷商的模式會吞吐量高一些(可以批量買賣);但另一方面,又因為經(jīng)銷商的引入,使得產(chǎn)品從生產(chǎn)者到消費者中間增加了額外的交易環(huán)節(jié),單個產(chǎn)品的及時響應性能可能會降低。
- 聲明一個SynchronousQueue有兩種不同的方式:公平鎖和非公平鎖。
SynchronousQueue<Integer> sc = new SynchronousQueue<>(true);//fair,默認是不公平鎖。
一個使用場景:
在線程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,這個線程池根據(jù)需要(新任務到來時)創(chuàng)建新的線程,如果有空閑線程則會重復使用,線程空閑了60秒后會被回收。
由于SynchronousQueue是沒有緩沖區(qū)的,所以如下方法不可用:
sc.peek();// Always returns null
sc.clear();
sc.contains(1);
sc.containsAll(new ArrayList<Integer>());
sc.isEmpty();
sc.size();
sc.toArray();
Integer [] in = new Integer[]{new Integer(2)};
sc.toArray(in);
sc.removeAll(new ArrayList<Integer>());
sc.retainAll(new ArrayList<Integer>());
sc.remove("a");
sc.peek();
不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue內(nèi)部并沒有數(shù)據(jù)緩存空間,你不能調(diào)用peek()方法來看隊列中是否有數(shù)據(jù)元素,因為數(shù)據(jù)元素只有當你試著取走的時候才可能存在,不取走而只想偷窺一下是不行的,當然遍歷這個隊列的操作也是不允許的。
SynchronousQueue 獲取元素:
public class Main {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<Integer> sc = new SynchronousQueue<>(); // 默認不指定的話是false,不公平的
//sc.take();// 沒有元素阻塞在此處,等待其他線程向sc添加元素才會獲取元素向下執(zhí)行
sc.poll();//沒有元素不阻塞在此處直接返回null向下執(zhí)行
sc.poll(5,TimeUnit.SECONDS);//沒有元素阻塞在此處等待指定時間,如果還是沒有元素直接返回null向下執(zhí)行
}
}
SynchronousQueue 存入元素:
public class Main {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<Integer> sc = new SynchronousQueue<>(); // 默認不指定的話是false,不公平的
// sc.put(2);//沒有線程等待獲取元素的話,阻塞在此處等待一直到有線程獲取元素時候放到隊列繼續(xù)向下運行
sc.offer(2);// 沒有線程等待獲取元素的話,不阻塞在此處,如果該元素已添加到此隊列,則返回 true;否則返回 false
sc.offer(2, 5, TimeUnit.SECONDS);// 沒有線程等待獲取元素的話,阻塞在此處等待指定時間,如果該元素已添加到此隊列,則返回true;否則返回 false
}
}
6. 總結(jié)
BlockingQueue不光實現(xiàn)了一個完整隊列所具有的基本功能,同時在多線程環(huán)境下,他還自動管理了多線間的自動等待和喚醒功能,從而使得程序員可以忽略這些細節(jié),關(guān)注更高級的功能。
5.4 線程池
線程池優(yōu)點:
1) 重用線程池的線程,==減少線程創(chuàng)建和銷毀帶來的性能開銷==
2) ==控制線程池的最大并發(fā)數(shù)==,避免大量線程互相搶系統(tǒng)資源導致阻塞
3) ==提供定時執(zhí)行和間隔循環(huán)執(zhí)行功能==
Android中的線程池的概念來源于Java中的Executor,Executor是一個接口,真正的線程池的實現(xiàn)為ThreadPoolExecutor。Android的線程池大部分都是通過Executor提供的工廠方法創(chuàng)建的。ThreadPoolExecutor提供了一系列參數(shù)來配制線程池,通過不同的參數(shù)可以創(chuàng)建不同的線程池。 而從功能的特性來分的話可以分成四類。
5.4.1 ThreadPoolExecutor
ThreadPoolExecutor是線程池的真正實現(xiàn), 它的構(gòu)造方法提供了一系列參數(shù)來配置線程池, 這些參數(shù)將會直接影響到線程池的功能特性。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
-
corePoolSize: 線程池的核心線程數(shù), 默認情況下, 核心線程會在線程池中一直存活, 即使都處于閑置狀態(tài). 如果將ThreadPoolExecutor#allowCoreThreadTimeOut屬性設置為true, 那么閑置的核心線程在等待新任務到來時會有超時的策略, 這個時間間隔由keepAliveTime屬性來決定 當?shù)却龝r間超過了keepAliveTime設定的值那么核心線程將會終止。 -
maximumPoolSize: 線程池所能容納的最大線程數(shù), 當活動線程數(shù)達到這個數(shù)值之后, 后續(xù)的任務將會被阻塞。 -
keepAliveTime: 非核心線程閑置的超時時長, 超過這個時長, 非核心線程就會被回收。 -
allowCoreThreadTimeOut這個屬性為true的時候, 這個屬性同樣會作用于核心線程。 -
unit: 用于指定keepAliveTime參數(shù)的時間單位, 這是一個枚舉, 常用的有TimeUtil.MILLISECONDS(毫秒), TimeUtil.SECONDS(秒)以及TimeUtil.MINUTES(分)。 -
workQueue: 線程池中的任務隊列, 通過線程池的execute方法提交的Runnable對象會存儲在這個參數(shù)中。 -
threadFactory: 線程工廠, 為線程池提供創(chuàng)建新線程的功能. ThreadFactory是一個接口。
1. ThreadPoolExecutor執(zhí)行任務大致遵循規(guī)則
如果線程池中的線程數(shù)量未達到核心線程的數(shù)量, 那么會直接啟動一個核心線程來執(zhí)行任務.
如果線程池中的線程數(shù)量已經(jīng)達到或者超過核心線程的數(shù)量, 那么任務會被插入到任務隊列中排隊等待執(zhí)行.
如果在步驟2中無法將任務插入到任務隊列中,這通常是因為任務隊列已滿,這個時候如果線程數(shù)量未達到線程池的規(guī)定的最大值, 那么會立刻啟動一個非核心線程來執(zhí)行任務.
如果步驟3中的線程數(shù)量已經(jīng)達到最大值的時候, 那么會拒絕執(zhí)行此任務,ThreadPoolExecutor會調(diào)用RejectedExecution方法來通知調(diào)用者。
2. AsyncTask的THREAD_POOL_EXECUTOR線程池配置
- 核心線程數(shù)等于CPU核心數(shù)+1
- 線程池最大線程數(shù)為CPU核心數(shù)的2倍+1
- 核心線程無超時機制,非核心線程的閑置超時時間為1秒
- 任務隊列容量是128
5.4.2 線程池的分類
1. FixedThreadPool
通過Executor#newFixedThreadPool()方法來創(chuàng)建。它是一種線程數(shù)量固定的線程池, 當線程處于空閑狀態(tài)時, 它們并不會被回收, 除非線程池關(guān)閉了. 當所有的線程都處于活動狀態(tài)時, 新任務都會處于等待狀態(tài), 直到有線程空閑出來. 由于FixedThreadPool只有核心線程并且這些核心線程不會被回收, 這意味著它能夠更加快速地響應外界的請求.
2. CachedThreadPool
通過Executor#newCachedThreadPool()方法來創(chuàng)建. 它是一種線程數(shù)量不定的線程池, 它只有非核心線程, 并且其最大值線程數(shù)為Integer.MAX_VALUE. 這就可以認為這個最大線程數(shù)為任意大了. 當線程池中的線程都處于活動的時候, 線程池會創(chuàng)建新的線程來處理新任務, 否則就會利用空閑的線程來處理新任務. 線程池中的空閑線程都有超時機制, 這個超時時長為60S, 超過這個時間那么空閑線程就會被回收.
和FixedThreadPool不同的是, CachedThreadPool的任務隊列其實相當于一個空集合, 這將導致任何任務都會立即被執(zhí)行, 因為在這種場景下SynchronousQueue是無法插入任務的. SynchronousQueue是一個非常特殊的隊列, 在很多情況下可以把它簡單理解為一個無法存儲元素的隊列. 在實際使用中很少使用.這類線程比較適合執(zhí)行大量的耗時較少的任務
3. ScheduledThreadPool
通過Executor#newScheduledThreadPool()方法來創(chuàng)建. 它的核心線程數(shù)量是固定的, 而非核心線程數(shù)是沒有限制的, 并且當非核心線程閑置時會立刻被回收掉. 這類線程池用于執(zhí)行定時任務和具有固定周期的重復任務
4. SingleThreadExecutor
通過Executor#newSingleThreadPool()方法來創(chuàng)建. 這類線程池內(nèi)部只有一個核心線程, 它確保所有的任務都在同一個線程中按順序執(zhí)行. 這類線程池意義在于統(tǒng)一所有的外界任務到一個線程中, 這使得在這些任務之間不需要處理線程同步的問題
5.5 Android的消息機制分析
出于性能優(yōu)化的考慮,Android中UI的操作是線程不安全的。所以,Android規(guī)定:只有UI線程才能修改UI組件。
這樣會導致新啟動的線程無法修改UI,此時需要Handler消息機制。
5.5.1 ThreadLocal<T>的工作原理
ThreadLocal是一個線程內(nèi)部的數(shù)據(jù)存儲類,通過它可以在指定線程中存儲數(shù)據(jù),數(shù)據(jù)存儲后,只有在指定線程中可以獲取到存儲的數(shù)據(jù),對于其他線程來說無法獲得數(shù)據(jù)。
1. 使用場景
(1) 當某些數(shù)據(jù)是以線程為作用域并且不同線程具有不同的數(shù)據(jù)副本的時候,就可以考慮采用ThreadLocal。比如對于Handler來說,它需要獲取當前線程的Looper,而Looper的作用域就是線程并且不同的線程具有不同的Looper,通過ThreadLocal可以輕松實現(xiàn)線程中的存取。
(2) 復雜邏輯下的對象傳遞。比如監(jiān)聽器的傳遞,有時候一個線程中的任務過于復雜,表現(xiàn)為函數(shù)調(diào)用棧比較深以及代碼入口的多樣性,而這時我們又希望監(jiān)聽器能夠貫穿整個線程的執(zhí)行過程。此時可以讓監(jiān)聽器作為線程內(nèi)的全局對象而存在,在線程內(nèi)部只要通過get方法就可以獲取到監(jiān)聽器。如果不采用ThreadLocal,只能采用函數(shù)參數(shù)的形式在棧中傳遞或作為靜態(tài)變量供線程訪問。第一種方式在調(diào)用棧很深時,看起來設計很糟糕,第二種方式不具有擴展性,比如同時N個線程并發(fā)執(zhí)行。
2. 常用方法
-
set(T value)設置到當前線程內(nèi)部的ThreadLocal.ThreadLocalMap對象中的Entry[]數(shù)組的某個Entry中。Entry類似于一個Map,key是ThreadLocal對象,value是具體的值T,重復設置會覆蓋。 -
get() T循環(huán)當前線程內(nèi)部的ThreadLocal.ThreadLocalMap對象中的Entry[]數(shù)組,取出當前對象的key對應的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//獲取當前線程的ThreadLocal.ThreadLocalMap對象
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
在不同線程訪問同一個ThreadLocal對象,獲得的值卻是不同的。
5.5.2 MessageQueue的工作原理
用于存放Handler發(fā)送過來的消息。主要包含兩個操作:插入和讀取。讀取操作本身會伴隨著刪除操作。內(nèi)部通過一個單鏈表的數(shù)據(jù)結(jié)構(gòu)來維護消息列表,因為其在插入和刪除上的性能較高。插入和讀取對應的方法分別是:enqueueMessage和next方法。
1. Message
線程之間傳遞的消息,可以攜帶少量數(shù)據(jù)
1)屬性
-
what用戶自定義的消息碼 -
arg1攜帶整型數(shù)據(jù) -
arg2攜帶整型數(shù)據(jù) -
obj攜帶對象 -
replyTo==Messenger==類型
2)方法
sendToTarget()-
obtain() Message從消息池中獲取一個消息對象。不建議使用new Message()構(gòu)造。 -
obtain(Message orign) Message拷貝一個Message對象 -
obtain(Handler h, int what) Messageh:指定由誰處理,sendToTarget()就是發(fā)給他。what:指定what屬性。本質(zhì)還是調(diào)用Handler.sendMessage進行發(fā)送消息 -
obtain(Handler h, Runnable callback) Messagecallback:message被處理的時候調(diào)用 setData(Bundle data)getData() Bundle
5.5.3 Looper的工作原理
每個線程的MessageQueue管家,一個線程對應一個Looper,一個MessageQueue(創(chuàng)建Looper的時候創(chuàng)建)。Looper會不停地從MessageQueue中查看是否有新消息,如果有新消息就會立即處理,否則就一直阻塞在那里。
private static void prepare(boolean quitAllowed) {
...
//sThreadLocal是一個靜態(tài)變量,保證了線程和Looper對象的一對一
//存一個Looper到線程中
sThreadLocal.set(new Looper(quitAllowed));
...
}
private Looper(boolean quitAllowed) {
//創(chuàng)建了一個消息隊列
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
通過Looper.prepare()方法,創(chuàng)建了一個Looper,一個MessageQueue,再通過Looper.loop()開啟消息循環(huán)。
public static void loop() {
...
for (;;) {//無限循環(huán)
...
//next()是一個無限循環(huán)方法,沒有消息就阻塞,當有新消息,會返回這條消息并將其從單鏈表中移除
Message msg = queue.next();
...
//處理。msg.target是發(fā)送這條消息的Handler對象,這樣Handler發(fā)送的消息最終又交給Handler來處理了
msg.target.dispatchMessage(msg);
...
}
}
loop()方法會調(diào)用MessageQueue#next()方法來獲取新消息,next()方法是一個無限循環(huán)的方法,如果消息隊列中沒有消息,那么next方法會一直阻塞在這里,這也導致loop方法一直阻塞在那里。當有新消息到來時,next()方法會返回這條消息并將其從單鏈表中移除。如果MessageQueue的next方法返回了新消息,Looper就會處理這條消息:msg.target.dispatchMessage(msg),這里的msg.target是發(fā)送這條消息的Handler對象,這樣Handler發(fā)送的消息最終又交給Handler來處理了。
Looper提供quit()和quitSafely()來退出一個Looper,區(qū)別在于quit會直接退出Looper,而quitSafely會把消息隊列中已有的消息處理完畢后才安全地退出。Looper退出后,這時候通過Handler發(fā)送的消息會失敗,Handler的send方法會返回false。在子線程中,如果手動為其創(chuàng)建了Looper,在所有事情做完后,應該調(diào)用Looper的quit方法來終止消息循環(huán),否則這個子線程就會一直處于等待狀態(tài);而如果退出了Looper以后,這個線程就會立刻終止,因此建議不需要的時候終止Looper。
1.方法
-
Looper.getMainLooper() Looper返回主線程上面的Looper -
Looper.myLooper() Looper返回當前線程的Looper -
prepare()為當前線程創(chuàng)建Looper對象,和關(guān)聯(lián)的MessageQueue(主線程無需創(chuàng)建,已經(jīng)有了) -
loop()開始輪詢,記得quit() -
quit()此時Handler.sendMessage將會返回false -
quitSafely()將已經(jīng)在MessageQueue中的消息處理完,再結(jié)束 -
isCurrentThread()boolean 是否是當前線程的Looper -
getThread()Thread 返回對應的線程
5.5.4 Handler的工作原理
Handler用于發(fā)送Message或Runnable到Handler所在線程,進行執(zhí)行或處理。
Handler發(fā)送過程僅僅是向消息隊列中插入了一條消息。MessageQueue的next方法就會返回這條消息給Looper,Looper拿到這條消息就開始處理,最終消息會交給Handler的dispatchMessage()來處理,這時Handler就進入了處理消息的階段。
構(gòu)造方法
...
mLooper = Looper.myLooper();//獲取當前線程中保存的Looper對象,主要為了獲取其中的mQueue
mQueue = mLooper.mQueue;
...
sendMessage(Message msg)
在mQueue中插入一個消息,跨線程通訊了
dispatchMessage(Message msg)
//handler處理消息的過程。由Looper#loop()調(diào)用,運行在Looper所在線程。若主動調(diào)用,就運行在調(diào)用的線程中。
public void dispatchMessage(Message msg) {
//Message#obtain(Handler h, Runnable callback)中的callback,Handler#handleMessage(Message msg)不會被執(zhí)行
if(msg.callback != null){
handleCallback(msg);
} else {
//Handler(Callback callback)中的callback(接口,只有一個方法boolean handleMessage(Message msg))
if (mCallback != null) {
//返回值決定了Handler#handleMessage(Message msg)是否會被執(zhí)行
if (mCallback.handleMessage(msg)){
return;
}
}
handleMessage(msg);
}
}
1. 方法
- 構(gòu)造方法:
Handler()用當前線程的Looper,若當前線程沒有Looper,將拋出異常 - 構(gòu)造方法:
Handler(Looper looper)指定Looper - 構(gòu)造方法:
Handler(Callback callback) -
sendEmptyMessage(int what) boolean發(fā)送一個僅僅包含what的Message,返回值表示是否成功插入到MessageQueue -
sendEmptyMessageAtTime(int what, long uptimeMillis) uptimeMillis:指定時間發(fā)送 -
sendEmptyMessageDelayed(int what, long delayMillis) delayMillis:延遲n秒發(fā)送 -
postDelayed(Runnable r, long delayMillis)發(fā)送Runnable對象到消息隊列中,將被執(zhí)行在Handler所在的線程 removeCallbacks(Runnable r)-
handleMessage(Message msg)必須要重寫的方法 removeMessages(int what)obtainMessage(int what)sendMessage(Message msg) boolean-
dispatchMessage(Message msg)在調(diào)用此方法所在線程直接執(zhí)行
2. 使用步驟
①:調(diào)用Looper.prepare()為當前線程創(chuàng)建Looper對象(主線程不用創(chuàng)建,已經(jīng)有了),然后Looper
.loop()
②:創(chuàng)建Handler子類的實例,重寫handleMessages()方法,處理消息
3. HandlerThread
一個為了快速創(chuàng)建包含Looper的一個線程類, start()時就創(chuàng)建了Looper和MessageQueue對象(本質(zhì))。
- 構(gòu)造方法:
HandlerThread(String name) getLooper() Looperquit()quitSafely()
用法:
mCheckMsgThread = new HandlerThread("check-message-coming");
mCheckMsgThread.start();
mCheckMsgHandler = new Handler(mCheckMsgThread.getLooper()){...}
5.5.5 主線程的消息循環(huán)
Android的主線程就是ActivityThread,主線程的入口方法為main(String[] args),在main方法中系統(tǒng)會通過Looper.prepareMainLooper()來創(chuàng)建主線程的Looper以及MessageQueue,并通過Looper.loop()來開啟主線程的消息循環(huán)。
ActivityThread通過ApplicationThread和AMS進行進程間通信,AMS以進程間通信的方式完成ActivityThread的請求后會回調(diào)ApplicationThread中的Binder方法,然后ApplicationThread會向H發(fā)送消息,H收到消息后會將ApplicationThread中的邏輯切換到ActivityTread中去執(zhí)行,即切換到主線程中去執(zhí)行。四大組件的啟動過程基本上都是這個流程。
Looper.loop(),這里是一個死循環(huán),如果主線程的Looper終止,則應用程序會拋出異常。那么問題來了,既然主線程卡在這里了
- 那Activity為什么還能啟動;
- 點擊一個按鈕仍然可以響應?
問題1:startActivity的時候,會向AMS(ActivityManagerService)發(fā)一個跨進程請求(AMS運行在系統(tǒng)進程中),之后AMS啟動對應的Activity;AMS也需要調(diào)用App中Activity的生命周期方法(不同進程不可直接調(diào)用),AMS會發(fā)送跨進程請求,然后由App的ActivityThread中的ApplicationThread會來處理,ApplicationThread會通過主線程線程的Handler將執(zhí)行邏輯切換到主線程。重點來了,主線程的Handler把消息添加到了MessageQueue,Looper.loop會拿到該消息,并在主線程中執(zhí)行。這就解釋了為什么主線程的Looper是個死循環(huán),而Activity還能啟動,因為四大組件的生命周期都是以消息的形式通過UI線程的Handler發(fā)送,由UI線程的Looper執(zhí)行的。
問題2:和問題1原理一樣,點擊一個按鈕最終都是由系統(tǒng)發(fā)消息來進行的,都經(jīng)過了Looper.loop()處理。 問題2詳細分析請看原書作者的Android中MotionEvent的來源和ViewRootImpl。
5.6 Android中的線程
在Android中,線程的形態(tài)有很多種:
- AsyncTask 封裝了線程池和Handler,主要為了方便開發(fā)者在子線程中更新UI,底層是線程池。
- HandlerThread 具有消息循環(huán)的線程,內(nèi)部可以使用handler,底層是Thread。
- IntentService 一種Service,內(nèi)部采用HandlerThread來執(zhí)行任務,當任務執(zhí)行完畢后IntentService會自動退出。由于它是一種Service,所以不容易被系統(tǒng)殺死,底層是Thread 。
操作系統(tǒng)中,線程是操作系統(tǒng)調(diào)度的最小單元,同時線程又是一種受限的系統(tǒng)資源(不可能無限產(chǎn)生),其創(chuàng)建和銷毀都會有相應的開銷。同時當系統(tǒng)存在大量線程時,系統(tǒng)會通過時間片輪轉(zhuǎn)的方式調(diào)度每個線程,因此線程不可能做到絕對的并發(fā),除非線程數(shù)量小于等于CPU的核心數(shù)。頻繁創(chuàng)建銷毀線程不明智,使用線程池是正確的做法。線程池會緩存一定數(shù)量的線程,通過線程池就可以避免因為頻繁創(chuàng)建和銷毀線程所帶來的系統(tǒng)開銷。
主線程也叫UI線程,作用是運行四大組件以及處理它們和用戶交互。子線程的作用是執(zhí)行耗時操作,比如I/O,網(wǎng)絡請求等。從Android 3.0開始,主線程中訪問網(wǎng)絡將拋出異常。
5.6.1 Android中的線程形態(tài)
1. AsyncTask
AsyncTask是一種輕量級的異步任務類,封裝了Thread和Handler,可以在線程池中執(zhí)行后臺任務,然后把執(zhí)行的進度和最終的結(jié)果傳遞給主線程并更新UI。但并不適合進行特別耗時的后臺任務,對于特別耗時的任務來說, 建議使用線程池。
abstract class AsyncTask<Params, Progress, Result>
- Params:入?yún)㈩愋?/li>
- Progress:后臺任務的執(zhí)行進度的類型
- Result:后臺任務的返回結(jié)果的類型
如果不需要傳遞具體的參數(shù), 那么這三個泛型參數(shù)可以用Void來代替。
(1) 四個核心方法
-
onPreExecute() void
在主線程執(zhí)行, 在異步任務執(zhí)行之前, 此方法會被調(diào)用, 一般可以用于做一些準備工作。 -
doInBackground(Params... params) Result
在線程池中執(zhí)行, 此方法用于執(zhí)行異步任務, 參數(shù)params表示異步任務的輸入?yún)?shù)。 在此方法中可以通過publishProgress(Progress... values) void方法來更新任務的進度,publishProgress()方法會調(diào)用onProgressUpdate()方法。另外此方法需要返回計算結(jié)果給onPostExecute() -
onProgressUpdate(Progress... values) void
在主線程執(zhí)行,當后臺任務publishProgress()時,會被調(diào)用。 -
onPostExecute(Result res) void
在主線程執(zhí)行, 在異步任務執(zhí)行之后, 此方法會被調(diào)用, 其中result參數(shù)是后臺任務的返回值, 即doInBackground的返回值。
除了上述的四種方法,還有onCancelled(), 它同樣在主線程執(zhí)行, 當異步任務被取消時調(diào)用,這個時候onPostExecute()則不會被調(diào)用.
(2) AsyncTask使用過程中的一些條件限制
- AsyncTask的類必須在主線程被加載, 這就意味著第一次訪問AsyncTask必須發(fā)生在主線程。在Android 4.1及以上的版本已經(jīng)被系統(tǒng)自動完成。
- AsyncTask的對象必須在主線程中創(chuàng)建。
- execute方法必須在UI線程調(diào)用。
- 不要在程序中直接調(diào)用onPreExecute(), onPostExecute(), doInBackground和onProgressUpdate()
- 一個AsyncTask對象只能執(zhí)行一次, 即只能調(diào)用一次execute()方法, 否則會報運行時異常。
- AsyncTask采用了一個線程來串行的執(zhí)行任務。 盡管如此在3.0以后, 仍然可以通過
AsyncTask#executeOnExecutor()方法來并行執(zhí)行任務。
(3) AsyncTask的工作原理
AsyncTask中有兩個線程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一個Handler(InternalHandler), 其中線程池SerialExecutor用于任務的排列, 而線程池THREAD_POOL_EXECUTOR用于真正的執(zhí)行任務, 而InternalHandler用于將執(zhí)行環(huán)境從線程切換到主線程, 其本質(zhì)仍然是線程的調(diào)用過程。
AsyncTask的排隊過程:首先系統(tǒng)會把AsyncTask#Params參數(shù)封裝成FutureTask對象, FutureTask是一個并發(fā)類, 在這里充當了Runnable的作用. 接著這個FutureTask會交給SerialExecutor#execute()方法去處理. 這個方法首先會把FutureTask對象插入到任務隊列mTasks中, 如果這個時候沒有正在活動AsyncTask任務, 那么就會調(diào)用SerialExecutor#scheduleNext()方法來執(zhí)行下一個AsyncTask任務. 同時當一個AsyncTask任務執(zhí)行完后, AsyncTask會繼續(xù)執(zhí)行其他任務直到所有的任務都執(zhí)行完畢為止, 從這一點可以看出, 在默認情況下, AsyncTask是串行執(zhí)行的。
5.6.2 HandlerThread
HandlerThread繼承了Thread, 它是一種可以使用Handler的Thread, 它的實現(xiàn)也很簡單, 就是run方法中通過Looper.prepare()來創(chuàng)建消息隊列, 并通過Looper.loop()來開啟消息循環(huán), 這樣在實際的使用中就允許在HandlerThread中創(chuàng)建Handler.
從HandlerThread的實現(xiàn)來看, 它和普通的Thread有顯著的不同之處. 普通的Thread主要用于在run方法中執(zhí)行一個耗時任務; 而HandlerThread在內(nèi)部創(chuàng)建了消息隊列, 外界需要通過Handler的消息方式來通知HandlerThread執(zhí)行一個具體的任務. HandlerThread是一個很有用的類, 在Android中一個具體使用場景就是IntentService.
由于HandlerThread#run()是一個無線循環(huán)方法, 因此當明確不需要再使用HandlerThread時, 最好通過quit()或者quitSafely()方法來終止線程的執(zhí)行.
5.6.3 IntentService
IntentSercie是一種特殊的Service,繼承了Service并且是抽象類,任務執(zhí)行完成后會自動停止,優(yōu)先級遠高于普通線程,適合執(zhí)行一些高優(yōu)先級的后臺任務; IntentService封裝了HandlerThread和Handler
onCreate方法自動創(chuàng)建一個HandlerThread,用它的Looper構(gòu)造了一個Handler對象mServiceHandler,這樣通過mServiceHandler發(fā)送的消息都會在HandlerThread執(zhí)行;IntentServiced的onHandlerIntent方法是一個抽象方法,需要在子類實現(xiàn),onHandlerIntent方法執(zhí)行后,stopSelt(int startId)就會停止服務,如果存在多個后臺任務,執(zhí)行完最后一個stopSelf(int startId)才會停止服務。
參考文獻
Java中的多線程你只要看這一篇就夠了
java并發(fā)編程---如何創(chuàng)建線程以及Thread類的使用
Java中繼承thread類與實現(xiàn)Runnable接口的區(qū)別