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