第5章 多線程編程

第5章 多線程編程

5.1 線程基礎(chǔ)

5.1.1 如何創(chuàng)建線程

在java要?jiǎng)?chuàng)建線程,一般有==兩種方式==:
1)繼承Thread類
2)實(shí)現(xiàn)Runnable接口

1. 繼承Thread類

繼承Thread類,重寫run方法,在run方法中定義需要執(zhí)行的任務(wù)。

class MyThread extends Thread{  
    private static int num = 0;  
    public MyThread(){  
        num++;  
    }  
    @Override  
    public void run() {  
        System.out.println("主動(dòng)創(chuàng)建的第"+num+"個(gè)線程");  
    }  
}  

創(chuàng)建好線程類之后,就可以創(chuàng)建線程對(duì)象了,然后通過start()方法去啟動(dòng)線程。不是調(diào)用run()方法啟動(dòng)線程,run方法中只是定義需要執(zhí)行的任務(wù),如果調(diào)用run方法,即相當(dāng)于在主線程中執(zhí)行run方法,跟普通的方法調(diào)用沒有任何區(qū)別,此時(shí)并不會(huì)創(chuàng)建一個(gè)新的線程來執(zhí)行定義的任務(wù)。

2. 實(shí)現(xiàn)Runnable接口

實(shí)現(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)建一個(gè)新線程來執(zhí)行該子任務(wù)。如果調(diào)用Runnable的run方法的話,是不會(huì)創(chuàng)建新線程的,這根普通的方法調(diào)用沒有任何區(qū)別。

實(shí)現(xiàn)Runnable接口相比繼承Thread類有如下==優(yōu)勢==:
1、可以避免由于Java的單繼承特性而帶來的局限。
2、代碼能夠被多個(gè)線程共享,代碼與數(shù)據(jù)是獨(dú)立的,適合多個(gè)線程去處理同一資源的情況 。

5.1.2 線程的狀態(tài)

線程狀態(tài)

線程包括以下這==7個(gè)狀態(tài)==:創(chuàng)建(new)、就緒(runnable)、運(yùn)行(running)、阻塞(blocked)、time wating、wating、消亡(dead)。

當(dāng)需要新起一個(gè)線程來執(zhí)行某個(gè)子任務(wù)時(shí),就創(chuàng)建了一個(gè)線程。但是線程創(chuàng)建之后,不會(huì)立即進(jìn)入就緒狀態(tài),因?yàn)榫€程的運(yùn)行需要一些條件(比如內(nèi)存資源,程序計(jì)數(shù)器、Java棧、本地方法棧都是線程私有的,所以需要為線程分配一定的內(nèi)存空間),只有線程運(yùn)行需要的所有條件滿足了,才進(jìn)入就緒狀態(tài)。

當(dāng)線程進(jìn)入就緒狀態(tài)后,不代表立刻就能獲取CPU執(zhí)行時(shí)間,也許此時(shí)CPU正在執(zhí)行其他的事情,因此它要等待。當(dāng)?shù)玫紺PU執(zhí)行時(shí)間之后,線程便真正進(jìn)入運(yùn)行狀態(tài)。

線程在運(yùn)行狀態(tài)過程中,可能有多個(gè)原因?qū)е庐?dāng)前線程不繼續(xù)運(yùn)行下去,比如用戶主動(dòng)讓線程睡眠(睡眠一定的時(shí)間之后再重新執(zhí)行)、用戶主動(dòng)讓線程等待,或者被同步塊給阻塞,此時(shí)就對(duì)應(yīng)著多個(gè)狀態(tài):time wating、wating、阻塞。

當(dāng)由于突然中斷或者子任務(wù)執(zhí)行完畢,線程就會(huì)被消亡。

5.1.3 上下文切換

對(duì)于單核CPU來說(對(duì)于多核CPU,此處就理解為一個(gè)核),CPU在一個(gè)時(shí)刻只能運(yùn)行一個(gè)線程,當(dāng)在運(yùn)行一個(gè)線程的過程中轉(zhuǎn)去運(yùn)行另外一個(gè)線程,這個(gè)叫做==線程上下文切換==(對(duì)于進(jìn)程也是類似)。

由于可能當(dāng)前線程的任務(wù)并沒有執(zhí)行完畢,所以在切換時(shí)需要保存線程的運(yùn)行狀態(tài),以便下次重新切換回來時(shí)能夠繼續(xù)切換之前的狀態(tài)運(yùn)行。舉個(gè)簡單的例子:比如一個(gè)線程A正在讀取一個(gè)文件的內(nèi)容,正讀到文件的一半,此時(shí)需要暫停線程A,轉(zhuǎn)去執(zhí)行線程B,當(dāng)再次切換回來執(zhí)行線程A的時(shí)候,我們不希望線程A又從文件的開頭來讀取。因此需要記錄線程A的運(yùn)行狀態(tài),那么會(huì)記錄哪些數(shù)據(jù)呢?因?yàn)橄麓位謴?fù)時(shí)需要知道在這之前當(dāng)前線程已經(jīng)執(zhí)行到哪條指令了,所以需要記錄程序計(jì)數(shù)器的值,另外比如說線程正在進(jìn)行某個(gè)計(jì)算的時(shí)候被掛起了,那么下次繼續(xù)執(zhí)行的時(shí)候需要知道之前掛起時(shí)變量的值時(shí)多少,因此需要記錄CPU寄存器的狀態(tài)。所以一般來說,線程上下文切換過程中會(huì)記錄程序計(jì)數(shù)器、CPU寄存器狀態(tài)等數(shù)據(jù)。說簡單點(diǎn):對(duì)于線程的上下文切換實(shí)際上就是存儲(chǔ)和恢復(fù)CPU狀態(tài)的過程,它使得線程執(zhí)行能夠從中斷點(diǎn)恢復(fù)執(zhí)行。

雖然多線程可以使得任務(wù)執(zhí)行的效率得到提升,但是由于在線程切換時(shí)同樣會(huì)帶來一定的開銷代價(jià),并且多個(gè)線程會(huì)導(dǎo)致系統(tǒng)資源占用的增加,所以在進(jìn)行多線程編程時(shí)要注意這些因素。

5.1.4 理解中斷

每一個(gè)線程都有一個(gè)用來表明當(dāng)前線程是否請(qǐng)求中斷的boolean類型標(biāo)志,當(dāng)一個(gè)線程調(diào)用interrupt()方法時(shí),線程的中斷標(biāo)志將被設(shè)置為true。我們可以通過調(diào)用Thread.currentThread().isInterrupted()或者Thread.interrupted()來檢測線程的中斷標(biāo)志是否被置位。這兩個(gè)方法的區(qū)別:前者是線程對(duì)象的方法,調(diào)用它后==不清除==線程中斷標(biāo)志位;后者是Thread的靜態(tài)方法,調(diào)用它會(huì)==清除==線程中斷標(biāo)志位。

所以說調(diào)用線程的interrupt()方法不會(huì)中斷一個(gè)正在運(yùn)行的線程,只是設(shè)置了一個(gè)線程中斷標(biāo)志位,如果在程序中不檢測線程中斷標(biāo)志位,那么即使設(shè)置了中斷標(biāo)志位為true,線程也一樣照常運(yùn)行。

一般來說中斷線程分為三種情況:
(1):中斷非阻塞線程
(2):中斷阻塞線程
(3):不可中斷線程

1. 中斷非阻塞線程

中斷非阻塞線程通常有兩種方式:

(1) 采用線程共享變量

這種方式比較簡單可行,需要注意的一點(diǎn)是共享變量必須設(shè)置為volatile,這樣才能保證修改后其他線程立即可見。

public class InterruptThreadTest extends Thread{  
    // 設(shè)置線程共享變量  
    volatile boolean isStop = false;  
      
    public void run() {  
        while(!isStop) {//無限循環(huán)一直執(zhí)行
            System.out.println(Thread.currentThread().getName() + "is running");  
        }  
        if (isStop) {//此時(shí)跳出無限循環(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();  
        }  
        // 線程共享變量設(shè)置為true  
        itt.isStop = true;  
    }  
}  
(2) 采用中斷機(jī)制
public class InterruptThreadTest2 extends Thread{  
    public void run() {  
        // 這里調(diào)用的是非清除中斷標(biā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();  
        }  
        // 設(shè)置線程的中斷標(biāo)志位  
        itt.interrupt();  
    }  
}  

2. 中斷阻塞線程

當(dāng)線程調(diào)用Thread.sleep()、Thread.join()、object.wait()再或者調(diào)用阻塞的I/O操作方法時(shí),都會(huì)使得當(dāng)前線程進(jìn)入阻塞狀態(tài)。那么此時(shí)如果在線程處于阻塞狀態(tài)下調(diào)用interrupt()方法會(huì)拋出一個(gè)異常,并且會(huì)清除線程中斷標(biāo)志位(設(shè)置為false)。這樣一來線程就能退出阻塞狀態(tài)。

代碼實(shí)例如下:

public class InterruptThreadTest3 extends Thread{  
    public void run() {  
        // 這里調(diào)用的是非清除中斷標(biā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()方法會(huì)清除狀態(tài)標(biāo)志位 所以這里需要再次重置中斷標(biāo)志位 否則線程會(huì)繼續(xù)運(yùn)行下去  
                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();  
        }  
        // 設(shè)置線程的中斷標(biāo)志位  
        itt.interrupt();  
    }  
}  

需要注意的地方就是 Thread.sleep()Thread.join()object.wait()這些方法,會(huì)檢測線程中斷標(biāo)志位,如果發(fā)現(xiàn)中斷標(biāo)志位為true則==拋出異常并且將中斷標(biāo)志位設(shè)置為false==。所以while循環(huán)之后每次調(diào)用阻塞方法后都要在捕獲異常之后,調(diào)用Thread.currentThread().interrupt()重置狀態(tài)標(biāo)志位。

3. 不可中斷線程

有一種情況是線程不能被中斷的,就是調(diào)用synchronized關(guān)鍵字獲取到了鎖的線程。

5.1.5 Thread類常用方法

1. sleep方法

  • sleep(long time)
  • sleep(long millis, int nanos)

==不會(huì)釋放鎖==,相當(dāng)于讓線程睡眠,讓出CPU,必須處理InterruptedException異常。當(dāng)線程睡眠時(shí)間滿后,不一定會(huì)立即得到執(zhí)行,因?yàn)榇藭r(shí)可能CPU正在執(zhí)行其他的任務(wù)。所以說調(diào)用sleep方法相當(dāng)于讓線程進(jìn)入阻塞狀態(tài)。

2. yield方法

  • `yield()

==不會(huì)釋放鎖==,不能控制具體的交出CPU的時(shí)間。直接用Thread類調(diào)用,讓出CPU執(zhí)行權(quán)給同等級(jí)的線程,如果沒有相同級(jí)別的線程在等待CPU的執(zhí)行權(quán),則該線程繼續(xù)執(zhí)行。

注意,調(diào)用yield方法==并不會(huì)讓線程進(jìn)入阻塞狀態(tài),而是讓線程重回就緒狀態(tài)==,它只需要等待重新獲取CPU執(zhí)行時(shí)間,這一點(diǎn)是和sleep方法不一樣的。

3. join方法

  • join() 等待thread執(zhí)行完畢
  • join(long millis) 等待一定的時(shí)間
  • join(long millis,int nanoseconds)

==釋放鎖==,如果在一個(gè)線程A中調(diào)用另一個(gè)線程B的join方法,線程A將會(huì)等待線程B執(zhí)行完畢后或等待一定的時(shí)間再執(zhí)行。實(shí)際上調(diào)用join方法是調(diào)用了Object的wait()方法。wait方法會(huì)讓線程進(jìn)入阻塞狀態(tài),并且會(huì)釋放線程占有的鎖,并交出CPU執(zhí)行權(quán)限。

4. interrupt方法

  • interrupt()

即中斷的意思。單獨(dú)調(diào)用==interrupt方法可以使得處于阻塞狀態(tài)的線程拋出一個(gè)異常==,也就說,它可以用來中斷一個(gè)正處于阻塞狀態(tài)的線程;直接調(diào)用interrupt方法不能中斷正在運(yùn)行中的線程。

5. 其他方法

  • getId() 用來得到線程ID
  • getName()setName(String threadName) 用來得到或者設(shè)置線程名稱。
  • getPriority()setPriority(int priority) 用來獲取和設(shè)置線程優(yōu)先級(jí)。
  • setDaemon(boolean isDaemon)isDaemon() 用來設(shè)置線程是否成為守護(hù)線程和判斷線程是否是守護(hù)線程。
  • Thread.currentThread() 獲取當(dāng)前線程

守護(hù)線程和用戶線程的區(qū)別:
守護(hù)線程:依賴于創(chuàng)建它的線程。舉個(gè)簡單的例子:如果在main線程中創(chuàng)建了一個(gè)守護(hù)線程,當(dāng)main方法運(yùn)行完畢之后,守護(hù)線程也會(huì)隨著消亡。 在JVM中,像垃圾收集器線程就是守護(hù)線程。
用戶線程:不依賴創(chuàng)建它的線程,會(huì)一直運(yùn)行完畢。

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中,所有實(shí)例域、靜態(tài)域和數(shù)組元素存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線程之間共享(本文使用“共享變量”這個(gè)術(shù)語代指實(shí)例域,靜態(tài)域和數(shù)組元素)。

Java內(nèi)存模型(本文簡稱為JMM)定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(JMM的一個(gè)抽象概念),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本。 線程對(duì)變量的所有操作都必須在本地內(nèi)存中進(jìn)行,而不能直接對(duì)主內(nèi)存進(jìn)行操作。并且每個(gè)線程不能訪問其他線程的本地內(nèi)存。

內(nèi)存模型的抽象示意圖

從上圖來看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個(gè)步驟:
首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。

2. 并發(fā)編程的三個(gè)概念

并發(fā)編程需要處理的兩個(gè)關(guān)鍵問題:線程通信線程同步
線程通信的兩種方式:共享內(nèi)存消息傳遞。共享內(nèi)存:線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進(jìn)行通信。消息傳遞:線程之間必須通過明確的發(fā)送消息來顯式進(jìn)行通信。
要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個(gè)沒有被保證,就有可能會(huì)導(dǎo)致程序運(yùn)行不正確。

(1) 原子性

一個(gè)操作或者多個(gè)操作要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷,要么就都不執(zhí)行。

(2) 可見性

當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。

(3) 有序性

程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
指令重排序:處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。不會(huì)影響單個(gè)線程的執(zhí)行,但是會(huì)影響到線程并發(fā)執(zhí)行的正確性。

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2
 
//線程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會(huì)被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會(huì)以為初始化工作已經(jīng)完成,那么就會(huì)跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時(shí)context并沒有被初始化,就會(huì)導(dǎo)致程序出錯(cuò)。

3. JAVA語言對(duì)于原子性,可見性,有序性的保證

原子性

基本數(shù)據(jù)類型的變量的讀取賦值操作是原子性操作

請(qǐng)分析以下哪些操作是原子性操作:

x = 10;         //語句1
y = x;         //語句2
x++;           //語句3
x = x + 1;     //語句4

語句1是直接將數(shù)值10賦值給x,也就是說線程執(zhí)行這個(gè)語句的會(huì)直接將數(shù)值10寫入到本地內(nèi)存中。原子性操作。
語句2實(shí)際上包含2個(gè)操作,它先要去讀取x的值,再將x的值寫入本地內(nèi)存,雖然讀取x的值以及將x的值寫入本地內(nèi)存這2個(gè)操作都是原子性操作,但是合起來就不是原子性操作了。同樣的,x++和 x = x+1包括3個(gè)操作:讀取x的值,進(jìn)行加1操作,寫入新的值。

可見性

提供了volatile關(guān)鍵字來保證可見性。當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。

有序性

可以通過volatile保證部分有序。
synchronized也可以保證有序性。

4. volatile關(guān)鍵字

volatile 變量可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 塊相比,volatile 變量所需的編碼較少,并且運(yùn)行時(shí)開銷也較少,但是它所能實(shí)現(xiàn)的功能也僅是synchronized 的一部分。特點(diǎn)如下:

  • 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來說是立即可見的。
  • 禁止進(jìn)行指令重排序。volatile能在一定程度上保證有序性。
    volatile關(guān)鍵字禁止指令重排序有兩層意思:
    1)當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見;在其后面的操作肯定還沒有進(jìn)行;
    2)在進(jìn)行指令優(yōu)化時(shí),不能將在對(duì)volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。
  • 不能保證原子性

只能在有限的一些情形下使用 volatile變量替代鎖。要使volatile變量提供理想的線程安全,必須同時(shí)滿足下面兩個(gè)條件:

  • 對(duì)變量的寫操作不依賴于當(dāng)前值。
  • 該變量沒有包含在具有其他變量的不變式中。
(1) 狀態(tài)標(biāo)志
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 阻塞隊(duì)列

為了更好的理解線程池,本節(jié)學(xué)習(xí)阻塞隊(duì)列。

5.3.1 BlockingQueue

1. 認(rèn)識(shí)BlockingQueue

  • 在新增的Concurrent包中,高效且==線程安全==
  • 用來==處理消費(fèi)者生產(chǎn)者問題==。在多線程領(lǐng)域:所謂阻塞,在某些情況下會(huì)掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會(huì)自動(dòng)被喚醒。

2. 核心方法

  • put(anObject) 把a(bǔ)nObject加到BlockingQueue里,如果沒有空間,則調(diào)用此方法的線程被阻塞直到BlockingQueue里面有空間再繼續(xù)。
  • offer(anObject) 存數(shù)據(jù),如果可以容納,則返回true,否則返回false(不阻塞當(dāng)前執(zhí)行方法的線程)。
  • offer(E o, long timeout, TimeUnit unit) 可以設(shè)定等待的時(shí)間,如果在指定的時(shí)間內(nèi),還不能往隊(duì)列中加入,則返回失敗。
  • take() 取走BlockingQueue里排在首位的對(duì)象,若BlockingQueue為空,阻塞進(jìn)入等待狀態(tài)直到BlockingQueue有新的數(shù)據(jù)被加入。
  • poll(time) 取走排在首位的對(duì)象,若不能立即取出,則可以等time參數(shù)規(guī)定的時(shí)間,取不到時(shí)返回null。
  • poll(long timeout, TimeUnit unit) 取出一個(gè)隊(duì)首的對(duì)象,如果在指定時(shí)間內(nèi),隊(duì)列一旦有數(shù)據(jù)可取,則立即返回隊(duì)列中的數(shù)據(jù)。否則知道時(shí)間超時(shí)還沒有數(shù)據(jù)可取,返回失敗。
  • drainTo() 一次性從BlockingQueue獲取所有可用的數(shù)據(jù)對(duì)象(還可以指定獲取數(shù)據(jù)的個(gè)數(shù)), 通過該方法,可以提升獲取數(shù)據(jù)效率;不需要多次分批加鎖或釋放鎖。

5.3.2 BlockingQueue實(shí)現(xiàn)子類

公平鎖:配合一個(gè)FIFO隊(duì)列來阻塞多余的生產(chǎn)者和消費(fèi)者,從而體系整體的公平策略;
非公平鎖:配合一個(gè)LIFO隊(duì)列來管理多余的生產(chǎn)者和消費(fèi)者,如果生產(chǎn)者和消費(fèi)者的處理速度有差距,則很容易出現(xiàn)饑渴的情況,即可能有某些生產(chǎn)者或者是消費(fèi)者的數(shù)據(jù)永遠(yuǎn)都得不到處理。

1. ArrayBlockingQueue

  • 基于數(shù)組實(shí)現(xiàn),內(nèi)部維護(hù)了一個(gè)定長數(shù)組和兩個(gè)整形變量,分別緩存著隊(duì)列中的數(shù)據(jù)對(duì)象及標(biāo)識(shí)隊(duì)列的頭部和尾部在數(shù)組中的位置。生產(chǎn)和消費(fèi)時(shí)不會(huì)產(chǎn)生或銷毀任何額外的對(duì)象實(shí)例。
  • 在放入數(shù)據(jù)和獲取數(shù)據(jù),都是共用同一個(gè)鎖對(duì)象,兩者==無法并行運(yùn)行==。
  • 在創(chuàng)建時(shí),默認(rèn)采用非公平鎖。可以控制對(duì)象的內(nèi)部鎖是否采用公平鎖。

2. LinkedBlockingQueue

  • 基于鏈表實(shí)現(xiàn),也維持著一個(gè)數(shù)據(jù)緩沖隊(duì)列(該隊(duì)列由一個(gè)鏈表構(gòu)成)。生產(chǎn)和消費(fèi)的時(shí)候會(huì)產(chǎn)生Node對(duì)象。
  • 生產(chǎn)者端和消費(fèi)者端分別采用了獨(dú)立的鎖來控制數(shù)據(jù)同步,高并發(fā)的情況下生產(chǎn)者和消費(fèi)者==可以并行==地操作隊(duì)列中的數(shù)據(jù),以此來提高整個(gè)隊(duì)列的并發(fā)性能。
  • 構(gòu)造一個(gè)LinkedBlockingQueue對(duì)象,而沒有指定其容量大小,LinkedBlockingQueue會(huì)默認(rèn)一個(gè)類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生產(chǎn)者的速度一旦大于消費(fèi)者的速度,也許還沒有等到隊(duì)列滿阻塞產(chǎn)生,系統(tǒng)內(nèi)存就有可能已被消耗殆盡了。

LinkedBlockingQueue和ArrayBlockingQueue的異同
相同
最常用的阻塞隊(duì)列,當(dāng)隊(duì)列為空,消費(fèi)者線程被阻塞;當(dāng)隊(duì)列裝滿,生產(chǎn)者線程被阻塞;
區(qū)別
a. 底層實(shí)現(xiàn)機(jī)制不同:LinkedBlockingQueue基于鏈表實(shí)現(xiàn),在生產(chǎn)和消費(fèi)的時(shí)候,需要?jiǎng)?chuàng)建Node對(duì)象進(jìn)行插入或移除,大批量數(shù)據(jù)的系統(tǒng)中,其對(duì)于GC的壓力會(huì)比較大;而ArrayBlockingQueue內(nèi)部維護(hù)了一個(gè)數(shù)組,在生產(chǎn)和消費(fèi)的時(shí)候,是直接將枚舉對(duì)象插入或移除的,不會(huì)產(chǎn)生或銷毀任何額外的對(duì)象實(shí)例。
b. LinkedBlockingQueue中的消費(fèi)者和生產(chǎn)者是不同的鎖,而ArrayBlockingQueue生產(chǎn)者和消費(fèi)者使用的是同一把鎖;
c. LinkedBlockingQueue有默認(rèn)的容量大小為:Integer.MAX_VALUE,當(dāng)然也可以傳入指定的容量大小;ArrayBlockingQueue在初始化的時(shí)候,必須傳入一個(gè)容量大小的值。

3. PriorityBlockingQueue

基于優(yōu)先級(jí)的==無界隊(duì)列==,==存儲(chǔ)的對(duì)象必須是實(shí)現(xiàn)Comparable接口==。隊(duì)列通過這個(gè)接口的compare方法確定對(duì)象的priority。越小優(yōu)先級(jí)越高,優(yōu)先級(jí)越高,越優(yōu)先取出。但需要注意的是PriorityBlockingQueue并==不會(huì)阻塞數(shù)據(jù)生產(chǎn)者==,而只會(huì)在沒有可消費(fèi)的數(shù)據(jù)時(shí),==阻塞數(shù)據(jù)的消費(fèi)者==。因此使用的時(shí)候要特別注意,生產(chǎn)者生產(chǎn)數(shù)據(jù)的速度絕對(duì)不能快于消費(fèi)者消費(fèi)數(shù)據(jù)的速度,否則時(shí)間一長,會(huì)最終耗盡所有的可用堆內(nèi)存空間。在實(shí)現(xiàn)PriorityBlockingQueue時(shí),內(nèi)部控制線程同步的鎖采用的是公平鎖。
使用案例

4. DelayQueue

==無界隊(duì)列==,只有當(dāng)其指定的延遲時(shí)間到了,才能夠從隊(duì)列中獲取到該元素。一個(gè)沒有大小限制的隊(duì)列,因此往隊(duì)列中插入數(shù)據(jù)的操作(生產(chǎn)者)永遠(yuǎn)不會(huì)被阻塞,而只有獲取數(shù)據(jù)的操作(消費(fèi)者)才會(huì)被阻塞。

使用場景:DelayQueue使用場景較少,但都相當(dāng)巧妙,常見的例子比如使用一個(gè)DelayQueue來管理一個(gè)超時(shí)未響應(yīng)的連接隊(duì)列。

5. SynchronousQueue

  • 一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列,可以理解為容量為0。==每個(gè)插入(移除)操作必須等待另一個(gè)線程的移除(插入)操作==。可以這樣來理解:生產(chǎn)者和消費(fèi)者互相等待對(duì)方,握手,然后一起離開。類似于無中介的直接交易,有點(diǎn)像原始社會(huì)中的生產(chǎn)者和消費(fèi)者,生產(chǎn)者拿著產(chǎn)品去集市銷售給產(chǎn)品的最終消費(fèi)者,而消費(fèi)者必須親自去集市找到所要商品的直接生產(chǎn)者,如果一方?jīng)]有找到合適的目標(biāo),那么對(duì)不起,大家都在集市等待。相對(duì)于有緩沖的BlockingQueue來說,少了一個(gè)中間經(jīng)銷商的環(huán)節(jié)(緩沖區(qū)),如果有經(jīng)銷商,生產(chǎn)者直接把產(chǎn)品批發(fā)給經(jīng)銷商,而無需在意經(jīng)銷商最終會(huì)將這些產(chǎn)品賣給那些消費(fèi)者,由于經(jīng)銷商可以庫存一部分商品,因此相對(duì)于直接交易模式,總體來說采用中間經(jīng)銷商的模式會(huì)吞吐量高一些(可以批量買賣);但另一方面,又因?yàn)榻?jīng)銷商的引入,使得產(chǎn)品從生產(chǎn)者到消費(fèi)者中間增加了額外的交易環(huán)節(jié),單個(gè)產(chǎn)品的及時(shí)響應(yīng)性能可能會(huì)降低。
  • 聲明一個(gè)SynchronousQueue有兩種不同的方式:公平鎖和非公平鎖。SynchronousQueue<Integer> sc = new SynchronousQueue<>(true);//fair,默認(rèn)是不公平鎖。
    一個(gè)使用場景:
    在線程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,這個(gè)線程池根據(jù)需要(新任務(wù)到來時(shí))創(chuàng)建新的線程,如果有空閑線程則會(huì)重復(fù)使用,線程空閑了60秒后會(huì)被回收。

由于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()方法來看隊(duì)列中是否有數(shù)據(jù)元素,因?yàn)閿?shù)據(jù)元素只有當(dāng)你試著取走的時(shí)候才可能存在,不取走而只想偷窺一下是不行的,當(dāng)然遍歷這個(gè)隊(duì)列的操作也是不允許的。

SynchronousQueue 獲取元素:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<Integer> sc = new SynchronousQueue<>(); // 默認(rèn)不指定的話是false,不公平的
        //sc.take();// 沒有元素阻塞在此處,等待其他線程向sc添加元素才會(huì)獲取元素向下執(zhí)行
        sc.poll();//沒有元素不阻塞在此處直接返回null向下執(zhí)行
        sc.poll(5,TimeUnit.SECONDS);//沒有元素阻塞在此處等待指定時(shí)間,如果還是沒有元素直接返回null向下執(zhí)行
    }
}

SynchronousQueue 存入元素:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<Integer> sc = new SynchronousQueue<>(); // 默認(rèn)不指定的話是false,不公平的
        // sc.put(2);//沒有線程等待獲取元素的話,阻塞在此處等待一直到有線程獲取元素時(shí)候放到隊(duì)列繼續(xù)向下運(yùn)行
        sc.offer(2);// 沒有線程等待獲取元素的話,不阻塞在此處,如果該元素已添加到此隊(duì)列,則返回 true;否則返回 false
        sc.offer(2, 5, TimeUnit.SECONDS);// 沒有線程等待獲取元素的話,阻塞在此處等待指定時(shí)間,如果該元素已添加到此隊(duì)列,則返回true;否則返回 false
    }
}

6. 總結(jié)

BlockingQueue不光實(shí)現(xiàn)了一個(gè)完整隊(duì)列所具有的基本功能,同時(shí)在多線程環(huán)境下,他還自動(dòng)管理了多線間的自動(dòng)等待和喚醒功能,從而使得程序員可以忽略這些細(xì)節(jié),關(guān)注更高級(jí)的功能。

5.4 線程池

線程池優(yōu)點(diǎn):
1) 重用線程池的線程,==減少線程創(chuàng)建和銷毀帶來的性能開銷==
2) ==控制線程池的最大并發(fā)數(shù)==,避免大量線程互相搶系統(tǒng)資源導(dǎo)致阻塞
3) ==提供定時(shí)執(zhí)行和間隔循環(huán)執(zhí)行功能==

Android中的線程池的概念來源于Java中的Executor,Executor是一個(gè)接口,真正的線程池的實(shí)現(xiàn)為ThreadPoolExecutor。Android的線程池大部分都是通過Executor提供的工廠方法創(chuàng)建的。ThreadPoolExecutor提供了一系列參數(shù)來配制線程池,通過不同的參數(shù)可以創(chuàng)建不同的線程池。 而從功能的特性來分的話可以分成四類。

5.4.1 ThreadPoolExecutor

ThreadPoolExecutor是線程池的真正實(shí)現(xiàn), 它的構(gòu)造方法提供了一系列參數(shù)來配置線程池, 這些參數(shù)將會(huì)直接影響到線程池的功能特性。

public ThreadPoolExecutor(int corePoolSize,
                 int maximumPoolSize,
                 long keepAliveTime,
                 TimeUnit unit,
                 BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
    Executors.defaultThreadFactory(), defaultHandler);
}
  • corePoolSize: 線程池的核心線程數(shù), 默認(rèn)情況下, 核心線程會(huì)在線程池中一直存活, 即使都處于閑置狀態(tài). 如果將ThreadPoolExecutor#allowCoreThreadTimeOut屬性設(shè)置為true, 那么閑置的核心線程在等待新任務(wù)到來時(shí)會(huì)有超時(shí)的策略, 這個(gè)時(shí)間間隔由keepAliveTime屬性來決定 當(dāng)?shù)却龝r(shí)間超過了keepAliveTime設(shè)定的值那么核心線程將會(huì)終止。
  • maximumPoolSize: 線程池所能容納的最大線程數(shù), 當(dāng)活動(dòng)線程數(shù)達(dá)到這個(gè)數(shù)值之后, 后續(xù)的任務(wù)將會(huì)被阻塞。
  • keepAliveTime: 非核心線程閑置的超時(shí)時(shí)長, 超過這個(gè)時(shí)長, 非核心線程就會(huì)被回收。
  • allowCoreThreadTimeOut這個(gè)屬性為true的時(shí)候, 這個(gè)屬性同樣會(huì)作用于核心線程。
  • unit: 用于指定keepAliveTime參數(shù)的時(shí)間單位, 這是一個(gè)枚舉, 常用的有TimeUtil.MILLISECONDS(毫秒), TimeUtil.SECONDS(秒)以及TimeUtil.MINUTES(分)。
  • workQueue: 線程池中的任務(wù)隊(duì)列, 通過線程池的execute方法提交的Runnable對(duì)象會(huì)存儲(chǔ)在這個(gè)參數(shù)中。
  • threadFactory: 線程工廠, 為線程池提供創(chuàng)建新線程的功能. ThreadFactory是一個(gè)接口。
1. ThreadPoolExecutor執(zhí)行任務(wù)大致遵循規(guī)則

如果線程池中的線程數(shù)量未達(dá)到核心線程的數(shù)量, 那么會(huì)直接啟動(dòng)一個(gè)核心線程來執(zhí)行任務(wù).
如果線程池中的線程數(shù)量已經(jīng)達(dá)到或者超過核心線程的數(shù)量, 那么任務(wù)會(huì)被插入到任務(wù)隊(duì)列中排隊(duì)等待執(zhí)行.
如果在步驟2中無法將任務(wù)插入到任務(wù)隊(duì)列中,這通常是因?yàn)槿蝿?wù)隊(duì)列已滿,這個(gè)時(shí)候如果線程數(shù)量未達(dá)到線程池的規(guī)定的最大值, 那么會(huì)立刻啟動(dòng)一個(gè)非核心線程來執(zhí)行任務(wù).
如果步驟3中的線程數(shù)量已經(jīng)達(dá)到最大值的時(shí)候, 那么會(huì)拒絕執(zhí)行此任務(wù),ThreadPoolExecutor會(huì)調(diào)用RejectedExecution方法來通知調(diào)用者。

2. AsyncTask的THREAD_POOL_EXECUTOR線程池配置
  • 核心線程數(shù)等于CPU核心數(shù)+1
  • 線程池最大線程數(shù)為CPU核心數(shù)的2倍+1
  • 核心線程無超時(shí)機(jī)制,非核心線程的閑置超時(shí)時(shí)間為1秒
  • 任務(wù)隊(duì)列容量是128

5.4.2 線程池的分類

1. FixedThreadPool

通過Executor#newFixedThreadPool()方法來創(chuàng)建。它是一種線程數(shù)量固定的線程池, 當(dāng)線程處于空閑狀態(tài)時(shí), 它們并不會(huì)被回收, 除非線程池關(guān)閉了. 當(dāng)所有的線程都處于活動(dòng)狀態(tài)時(shí), 新任務(wù)都會(huì)處于等待狀態(tài), 直到有線程空閑出來. 由于FixedThreadPool只有核心線程并且這些核心線程不會(huì)被回收, 這意味著它能夠更加快速地響應(yīng)外界的請(qǐng)求.

2. CachedThreadPool

通過Executor#newCachedThreadPool()方法來創(chuàng)建. 它是一種線程數(shù)量不定的線程池, 它只有非核心線程, 并且其最大值線程數(shù)為Integer.MAX_VALUE. 這就可以認(rèn)為這個(gè)最大線程數(shù)為任意大了. 當(dāng)線程池中的線程都處于活動(dòng)的時(shí)候, 線程池會(huì)創(chuàng)建新的線程來處理新任務(wù), 否則就會(huì)利用空閑的線程來處理新任務(wù). 線程池中的空閑線程都有超時(shí)機(jī)制, 這個(gè)超時(shí)時(shí)長為60S, 超過這個(gè)時(shí)間那么空閑線程就會(huì)被回收.
和FixedThreadPool不同的是, CachedThreadPool的任務(wù)隊(duì)列其實(shí)相當(dāng)于一個(gè)空集合, 這將導(dǎo)致任何任務(wù)都會(huì)立即被執(zhí)行, 因?yàn)樵谶@種場景下SynchronousQueue是無法插入任務(wù)的. SynchronousQueue是一個(gè)非常特殊的隊(duì)列, 在很多情況下可以把它簡單理解為一個(gè)無法存儲(chǔ)元素的隊(duì)列. 在實(shí)際使用中很少使用.這類線程比較適合執(zhí)行大量的耗時(shí)較少的任務(wù)

3. ScheduledThreadPool

通過Executor#newScheduledThreadPool()方法來創(chuàng)建. 它的核心線程數(shù)量是固定的, 而非核心線程數(shù)是沒有限制的, 并且當(dāng)非核心線程閑置時(shí)會(huì)立刻被回收掉. 這類線程池用于執(zhí)行定時(shí)任務(wù)和具有固定周期的重復(fù)任務(wù)

4. SingleThreadExecutor

通過Executor#newSingleThreadPool()方法來創(chuàng)建. 這類線程池內(nèi)部只有一個(gè)核心線程, 它確保所有的任務(wù)都在同一個(gè)線程中按順序執(zhí)行. 這類線程池意義在于統(tǒng)一所有的外界任務(wù)到一個(gè)線程中, 這使得在這些任務(wù)之間不需要處理線程同步的問題

5.5 Android的消息機(jī)制分析

出于性能優(yōu)化的考慮,Android中UI的操作是線程不安全的。所以,Android規(guī)定:只有UI線程才能修改UI組件。
這樣會(huì)導(dǎo)致新啟動(dòng)的線程無法修改UI,此時(shí)需要Handler消息機(jī)制。

5.5.1 ThreadLocal<T>的工作原理

ThreadLocal是一個(gè)線程內(nèi)部的數(shù)據(jù)存儲(chǔ)類,通過它可以在指定線程中存儲(chǔ)數(shù)據(jù),數(shù)據(jù)存儲(chǔ)后,只有在指定線程中可以獲取到存儲(chǔ)的數(shù)據(jù),對(duì)于其他線程來說無法獲得數(shù)據(jù)。

1. 使用場景

(1) 當(dāng)某些數(shù)據(jù)是以線程為作用域并且不同線程具有不同的數(shù)據(jù)副本的時(shí)候,就可以考慮采用ThreadLocal。比如對(duì)于Handler來說,它需要獲取當(dāng)前線程的Looper,而Looper的作用域就是線程并且不同的線程具有不同的Looper,通過ThreadLocal可以輕松實(shí)現(xiàn)線程中的存取。

(2) 復(fù)雜邏輯下的對(duì)象傳遞。比如監(jiān)聽器的傳遞,有時(shí)候一個(gè)線程中的任務(wù)過于復(fù)雜,表現(xiàn)為函數(shù)調(diào)用棧比較深以及代碼入口的多樣性,而這時(shí)我們又希望監(jiān)聽器能夠貫穿整個(gè)線程的執(zhí)行過程。此時(shí)可以讓監(jiān)聽器作為線程內(nèi)的全局對(duì)象而存在,在線程內(nèi)部只要通過get方法就可以獲取到監(jiān)聽器。如果不采用ThreadLocal,只能采用函數(shù)參數(shù)的形式在棧中傳遞或作為靜態(tài)變量供線程訪問。第一種方式在調(diào)用棧很深時(shí),看起來設(shè)計(jì)很糟糕,第二種方式不具有擴(kuò)展性,比如同時(shí)N個(gè)線程并發(fā)執(zhí)行。

2. 常用方法

  • set(T value) 設(shè)置到當(dāng)前線程內(nèi)部的ThreadLocal.ThreadLocalMap對(duì)象中的Entry[]數(shù)組的某個(gè)Entry中。Entry類似于一個(gè)Map,key是ThreadLocal對(duì)象,value是具體的值T,重復(fù)設(shè)置會(huì)覆蓋。
  • get() T 循環(huán)當(dāng)前線程內(nèi)部的ThreadLocal.ThreadLocalMap對(duì)象中的Entry[]數(shù)組,取出當(dāng)前對(duì)象的key對(duì)應(yīng)的值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);//獲取當(dāng)前線程的ThreadLocal.ThreadLocalMap對(duì)象
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

在不同線程訪問同一個(gè)ThreadLocal對(duì)象,獲得的值卻是不同的。

5.5.2 MessageQueue的工作原理

用于存放Handler發(fā)送過來的消息。主要包含兩個(gè)操作:插入和讀取。讀取操作本身會(huì)伴隨著刪除操作。內(nèi)部通過一個(gè)單鏈表的數(shù)據(jù)結(jié)構(gòu)來維護(hù)消息列表,因?yàn)槠湓诓迦牒蛣h除上的性能較高。插入和讀取對(duì)應(yīng)的方法分別是:enqueueMessagenext方法。

1. Message

線程之間傳遞的消息,可以攜帶少量數(shù)據(jù)

1)屬性

  • what 用戶自定義的消息碼
  • arg1 攜帶整型數(shù)據(jù)
  • arg2 攜帶整型數(shù)據(jù)
  • obj 攜帶對(duì)象
  • replyTo ==Messenger==類型

2)方法

  • sendToTarget()
  • obtain() Message 從消息池中獲取一個(gè)消息對(duì)象。不建議使用new Message()構(gòu)造。
  • obtain(Message orign) Message 拷貝一個(gè)Message對(duì)象
  • obtain(Handler h, int what) Message h:指定由誰處理,sendToTarget()就是發(fā)給他。what:指定what屬性。本質(zhì)還是調(diào)用Handler.sendMessage進(jìn)行發(fā)送消息
  • obtain(Handler h, Runnable callback) Message callback:message被處理的時(shí)候調(diào)用
  • setData(Bundle data)
  • getData() Bundle

5.5.3 Looper的工作原理

每個(gè)線程的MessageQueue管家,一個(gè)線程對(duì)應(yīng)一個(gè)Looper,一個(gè)MessageQueue(創(chuàng)建Looper的時(shí)候創(chuàng)建)。Looper會(huì)不停地從MessageQueue中查看是否有新消息,如果有新消息就會(huì)立即處理,否則就一直阻塞在那里。

private static void prepare(boolean quitAllowed) {
    ...
    //sThreadLocal是一個(gè)靜態(tài)變量,保證了線程和Looper對(duì)象的一對(duì)一
    //存一個(gè)Looper到線程中
    sThreadLocal.set(new Looper(quitAllowed));
    ...
}

private Looper(boolean quitAllowed) {
    //創(chuàng)建了一個(gè)消息隊(duì)列
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

通過Looper.prepare()方法,創(chuàng)建了一個(gè)Looper,一個(gè)MessageQueue,再通過Looper.loop()開啟消息循環(huán)。

public static void loop() {
    ...
    for (;;) {//無限循環(huán)
        ...
        //next()是一個(gè)無限循環(huán)方法,沒有消息就阻塞,當(dāng)有新消息,會(huì)返回這條消息并將其從單鏈表中移除
        Message msg = queue.next();
        ...
        //處理。msg.target是發(fā)送這條消息的Handler對(duì)象,這樣Handler發(fā)送的消息最終又交給Handler來處理了
        msg.target.dispatchMessage(msg);
        ...
    }
}

loop()方法會(huì)調(diào)用MessageQueue#next()方法來獲取新消息,next()方法是一個(gè)無限循環(huán)的方法,如果消息隊(duì)列中沒有消息,那么next方法會(huì)一直阻塞在這里,這也導(dǎo)致loop方法一直阻塞在那里。當(dāng)有新消息到來時(shí),next()方法會(huì)返回這條消息并將其從單鏈表中移除。如果MessageQueue的next方法返回了新消息,Looper就會(huì)處理這條消息:msg.target.dispatchMessage(msg),這里的msg.target是發(fā)送這條消息的Handler對(duì)象,這樣Handler發(fā)送的消息最終又交給Handler來處理了。

Looper提供quit()quitSafely()來退出一個(gè)Looper,區(qū)別在于quit會(huì)直接退出Looper,而quitSafely會(huì)把消息隊(duì)列中已有的消息處理完畢后才安全地退出。Looper退出后,這時(shí)候通過Handler發(fā)送的消息會(huì)失敗,Handler的send方法會(huì)返回false。在子線程中,如果手動(dòng)為其創(chuàng)建了Looper,在所有事情做完后,應(yīng)該調(diào)用Looper的quit方法來終止消息循環(huán),否則這個(gè)子線程就會(huì)一直處于等待狀態(tài);而如果退出了Looper以后,這個(gè)線程就會(huì)立刻終止,因此建議不需要的時(shí)候終止Looper。

1.方法

  • Looper.getMainLooper() Looper 返回主線程上面的Looper
  • Looper.myLooper() Looper 返回當(dāng)前線程的Looper
  • prepare() 為當(dāng)前線程創(chuàng)建Looper對(duì)象,和關(guān)聯(lián)的MessageQueue(主線程無需創(chuàng)建,已經(jīng)有了)
  • loop() 開始輪詢,記得quit()
  • quit() 此時(shí)Handler.sendMessage將會(huì)返回false
  • quitSafely() 將已經(jīng)在MessageQueue中的消息處理完,再結(jié)束
  • isCurrentThread() boolean 是否是當(dāng)前線程的Looper
  • getThread() Thread 返回對(duì)應(yīng)的線程

5.5.4 Handler的工作原理

Handler用于發(fā)送Message或Runnable到Handler所在線程,進(jìn)行執(zhí)行或處理。

Handler發(fā)送過程僅僅是向消息隊(duì)列中插入了一條消息。MessageQueue的next方法就會(huì)返回這條消息給Looper,Looper拿到這條消息就開始處理,最終消息會(huì)交給Handler的dispatchMessage()來處理,這時(shí)Handler就進(jìn)入了處理消息的階段。

構(gòu)造方法

...
mLooper = Looper.myLooper();//獲取當(dāng)前線程中保存的Looper對(duì)象,主要為了獲取其中的mQueue
mQueue = mLooper.mQueue;
...

sendMessage(Message msg)

在mQueue中插入一個(gè)消息,跨線程通訊了

dispatchMessage(Message msg)

//handler處理消息的過程。由Looper#loop()調(diào)用,運(yùn)行在Looper所在線程。若主動(dòng)調(diào)用,就運(yùn)行在調(diào)用的線程中。
public void dispatchMessage(Message msg) {
    //Message#obtain(Handler h, Runnable callback)中的callback,Handler#handleMessage(Message msg)不會(huì)被執(zhí)行
    if(msg.callback != null){
        handleCallback(msg);
    } else {
        //Handler(Callback callback)中的callback(接口,只有一個(gè)方法boolean handleMessage(Message msg))
        if (mCallback != null) {
        //返回值決定了Handler#handleMessage(Message msg)是否會(huì)被執(zhí)行
            if (mCallback.handleMessage(msg)){
                return;
            }
        }
        handleMessage(msg);
    }
}

1. 方法

  • 構(gòu)造方法:Handler()用當(dāng)前線程的Looper,若當(dāng)前線程沒有Looper,將拋出異常
  • 構(gòu)造方法:Handler(Looper looper) 指定Looper
  • 構(gòu)造方法:Handler(Callback callback)
  • sendEmptyMessage(int what) boolean 發(fā)送一個(gè)僅僅包含what的Message,返回值表示是否成功插入到MessageQueue
  • sendEmptyMessageAtTime(int what, long uptimeMillis) uptimeMillis:指定時(shí)間發(fā)送
  • sendEmptyMessageDelayed(int what, long delayMillis) delayMillis:延遲n秒發(fā)送
  • postDelayed(Runnable r, long delayMillis) 發(fā)送Runnable對(duì)象到消息隊(duì)列中,將被執(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()為當(dāng)前線程創(chuàng)建Looper對(duì)象(主線程不用創(chuàng)建,已經(jīng)有了),然后Looper
.loop()
②:創(chuàng)建Handler子類的實(shí)例,重寫handleMessages()方法,處理消息

3. HandlerThread

一個(gè)為了快速創(chuàng)建包含Looper的一個(gè)線程類, start()時(shí)就創(chuàng)建了Looper和MessageQueue對(duì)象(本質(zhì))。

  • 構(gòu)造方法: HandlerThread(String name)
  • getLooper() Looper
  • quit()
  • 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)會(huì)通過Looper.prepareMainLooper()來創(chuàng)建主線程的Looper以及MessageQueue,并通過Looper.loop()來開啟主線程的消息循環(huán)。

ActivityThread通過ApplicationThread和AMS進(jìn)行進(jìn)程間通信,AMS以進(jìn)程間通信的方式完成ActivityThread的請(qǐng)求后會(huì)回調(diào)ApplicationThread中的Binder方法,然后ApplicationThread會(huì)向H發(fā)送消息,H收到消息后會(huì)將ApplicationThread中的邏輯切換到ActivityTread中去執(zhí)行,即切換到主線程中去執(zhí)行。四大組件的啟動(dòng)過程基本上都是這個(gè)流程。

Looper.loop(),這里是一個(gè)死循環(huán),如果主線程的Looper終止,則應(yīng)用程序會(huì)拋出異常。那么問題來了,既然主線程卡在這里了

  • 那Activity為什么還能啟動(dòng);
  • 點(diǎn)擊一個(gè)按鈕仍然可以響應(yīng)?

問題1:startActivity的時(shí)候,會(huì)向AMS(ActivityManagerService)發(fā)一個(gè)跨進(jìn)程請(qǐng)求(AMS運(yùn)行在系統(tǒng)進(jìn)程中),之后AMS啟動(dòng)對(duì)應(yīng)的Activity;AMS也需要調(diào)用App中Activity的生命周期方法(不同進(jìn)程不可直接調(diào)用),AMS會(huì)發(fā)送跨進(jìn)程請(qǐng)求,然后由App的ActivityThread中的ApplicationThread會(huì)來處理,ApplicationThread會(huì)通過主線程線程的Handler將執(zhí)行邏輯切換到主線程。重點(diǎn)來了,主線程的Handler把消息添加到了MessageQueue,Looper.loop會(huì)拿到該消息,并在主線程中執(zhí)行。這就解釋了為什么主線程的Looper是個(gè)死循環(huán),而Activity還能啟動(dòng),因?yàn)樗拇蠼M件的生命周期都是以消息的形式通過UI線程的Handler發(fā)送,由UI線程的Looper執(zhí)行的。

問題2:和問題1原理一樣,點(diǎn)擊一個(gè)按鈕最終都是由系統(tǒng)發(fā)消息來進(jìn)行的,都經(jīng)過了Looper.loop()處理。 問題2詳細(xì)分析請(qǐng)看原書作者的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í)行任務(wù),當(dāng)任務(wù)執(zhí)行完畢后IntentService會(huì)自動(dòng)退出。由于它是一種Service,所以不容易被系統(tǒng)殺死,底層是Thread 。

操作系統(tǒng)中,線程是操作系統(tǒng)調(diào)度的最小單元,同時(shí)線程又是一種受限的系統(tǒng)資源(不可能無限產(chǎn)生),其創(chuàng)建和銷毀都會(huì)有相應(yīng)的開銷。同時(shí)當(dāng)系統(tǒng)存在大量線程時(shí),系統(tǒng)會(huì)通過時(shí)間片輪轉(zhuǎn)的方式調(diào)度每個(gè)線程,因此線程不可能做到絕對(duì)的并發(fā),除非線程數(shù)量小于等于CPU的核心數(shù)。頻繁創(chuàng)建銷毀線程不明智,使用線程池是正確的做法。線程池會(huì)緩存一定數(shù)量的線程,通過線程池就可以避免因?yàn)轭l繁創(chuàng)建和銷毀線程所帶來的系統(tǒng)開銷。

主線程也叫UI線程,作用是運(yùn)行四大組件以及處理它們和用戶交互。子線程的作用是執(zhí)行耗時(shí)操作,比如I/O,網(wǎng)絡(luò)請(qǐng)求等。從Android 3.0開始,主線程中訪問網(wǎng)絡(luò)將拋出異常。

5.6.1 Android中的線程形態(tài)

1. AsyncTask

AsyncTask是一種輕量級(jí)的異步任務(wù)類,封裝了Thread和Handler,可以在線程池中執(zhí)行后臺(tái)任務(wù),然后把執(zhí)行的進(jìn)度最終的結(jié)果傳遞給主線程并更新UI。但并不適合進(jìn)行特別耗時(shí)的后臺(tái)任務(wù),對(duì)于特別耗時(shí)的任務(wù)來說, 建議使用線程池。

abstract class AsyncTask<Params, Progress, Result>

  • Params:入?yún)㈩愋?/li>
  • Progress:后臺(tái)任務(wù)的執(zhí)行進(jìn)度的類型
  • Result:后臺(tái)任務(wù)的返回結(jié)果的類型

如果不需要傳遞具體的參數(shù), 那么這三個(gè)泛型參數(shù)可以用Void來代替。

(1) 四個(gè)核心方法
  • onPreExecute() void
    在主線程執(zhí)行, 在異步任務(wù)執(zhí)行之前, 此方法會(huì)被調(diào)用, 一般可以用于做一些準(zhǔn)備工作。
  • doInBackground(Params... params) Result
    在線程池中執(zhí)行, 此方法用于執(zhí)行異步任務(wù), 參數(shù)params表示異步任務(wù)的輸入?yún)?shù)。 在此方法中可以通過publishProgress(Progress... values) void方法來更新任務(wù)的進(jìn)度, publishProgress()方法會(huì)調(diào)用onProgressUpdate()方法。另外此方法需要返回計(jì)算結(jié)果給onPostExecute()
  • onProgressUpdate(Progress... values) void
    在主線程執(zhí)行,當(dāng)后臺(tái)任務(wù)publishProgress()時(shí),會(huì)被調(diào)用。
  • onPostExecute(Result res) void
    在主線程執(zhí)行, 在異步任務(wù)執(zhí)行之后, 此方法會(huì)被調(diào)用, 其中result參數(shù)是后臺(tái)任務(wù)的返回值, 即doInBackground的返回值。

除了上述的四種方法,還有onCancelled(), 它同樣在主線程執(zhí)行, 當(dāng)異步任務(wù)被取消時(shí)調(diào)用,這個(gè)時(shí)候onPostExecute()則不會(huì)被調(diào)用.

(2) AsyncTask使用過程中的一些條件限制
  • AsyncTask的類必須在主線程被加載, 這就意味著第一次訪問AsyncTask必須發(fā)生在主線程。在Android 4.1及以上的版本已經(jīng)被系統(tǒng)自動(dòng)完成。
  • AsyncTask的對(duì)象必須在主線程中創(chuàng)建。
  • execute方法必須在UI線程調(diào)用。
  • 不要在程序中直接調(diào)用onPreExecute(), onPostExecute(), doInBackground和onProgressUpdate()
  • 一個(gè)AsyncTask對(duì)象只能執(zhí)行一次, 即只能調(diào)用一次execute()方法, 否則會(huì)報(bào)運(yùn)行時(shí)異常。
  • AsyncTask采用了一個(gè)線程來串行的執(zhí)行任務(wù)。 盡管如此在3.0以后, 仍然可以通過AsyncTask#executeOnExecutor()方法來并行執(zhí)行任務(wù)。
(3) AsyncTask的工作原理

AsyncTask中有兩個(gè)線程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一個(gè)Handler(InternalHandler), 其中線程池SerialExecutor用于任務(wù)的排列, 而線程池THREAD_POOL_EXECUTOR用于真正的執(zhí)行任務(wù), 而InternalHandler用于將執(zhí)行環(huán)境從線程切換到主線程, 其本質(zhì)仍然是線程的調(diào)用過程。

AsyncTask的排隊(duì)過程:首先系統(tǒng)會(huì)把AsyncTask#Params參數(shù)封裝成FutureTask對(duì)象, FutureTask是一個(gè)并發(fā)類, 在這里充當(dāng)了Runnable的作用. 接著這個(gè)FutureTask會(huì)交給SerialExecutor#execute()方法去處理. 這個(gè)方法首先會(huì)把FutureTask對(duì)象插入到任務(wù)隊(duì)列mTasks中, 如果這個(gè)時(shí)候沒有正在活動(dòng)AsyncTask任務(wù), 那么就會(huì)調(diào)用SerialExecutor#scheduleNext()方法來執(zhí)行下一個(gè)AsyncTask任務(wù). 同時(shí)當(dāng)一個(gè)AsyncTask任務(wù)執(zhí)行完后, AsyncTask會(huì)繼續(xù)執(zhí)行其他任務(wù)直到所有的任務(wù)都執(zhí)行完畢為止, 從這一點(diǎn)可以看出, 在默認(rèn)情況下, AsyncTask是串行執(zhí)行的。

5.6.2 HandlerThread

HandlerThread繼承了Thread, 它是一種可以使用Handler的Thread, 它的實(shí)現(xiàn)也很簡單, 就是run方法中通過Looper.prepare()來創(chuàng)建消息隊(duì)列, 并通過Looper.loop()來開啟消息循環(huán), 這樣在實(shí)際的使用中就允許在HandlerThread中創(chuàng)建Handler.

從HandlerThread的實(shí)現(xiàn)來看, 它和普通的Thread有顯著的不同之處. 普通的Thread主要用于在run方法中執(zhí)行一個(gè)耗時(shí)任務(wù); 而HandlerThread在內(nèi)部創(chuàng)建了消息隊(duì)列, 外界需要通過Handler的消息方式來通知HandlerThread執(zhí)行一個(gè)具體的任務(wù). HandlerThread是一個(gè)很有用的類, 在Android中一個(gè)具體使用場景就是IntentService.

由于HandlerThread#run()是一個(gè)無線循環(huán)方法, 因此當(dāng)明確不需要再使用HandlerThread時(shí), 最好通過quit()或者quitSafely()方法來終止線程的執(zhí)行.

5.6.3 IntentService

IntentSercie是一種特殊的Service,繼承了Service并且是抽象類,任務(wù)執(zhí)行完成后會(huì)自動(dòng)停止,優(yōu)先級(jí)遠(yuǎn)高于普通線程,適合執(zhí)行一些高優(yōu)先級(jí)的后臺(tái)任務(wù); IntentService封裝了HandlerThread和Handler

onCreate方法自動(dòng)創(chuàng)建一個(gè)HandlerThread,用它的Looper構(gòu)造了一個(gè)Handler對(duì)象mServiceHandler,這樣通過mServiceHandler發(fā)送的消息都會(huì)在HandlerThread執(zhí)行;IntentServiced的onHandlerIntent方法是一個(gè)抽象方法,需要在子類實(shí)現(xiàn),onHandlerIntent方法執(zhí)行后,stopSelt(int startId)就會(huì)停止服務(wù),如果存在多個(gè)后臺(tái)任務(wù),執(zhí)行完最后一個(gè)stopSelf(int startId)才會(huì)停止服務(wù)。

參考文獻(xiàn)

Java中的多線程你只要看這一篇就夠了
java并發(fā)編程---如何創(chuàng)建線程以及Thread類的使用
Java中繼承thread類與實(shí)現(xiàn)Runnable接口的區(qū)別

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,786評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,964評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,354評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,554評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,106評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,918評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,093評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,342評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,839評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,107評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容