第5章 多線程編程

第5章 多線程編程

5.1 線程基礎(chǔ)

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

在java要創(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("主動創(chuàng)建的第"+num+"個(gè)線程");  
    }  
}  

創(chuàng)建好線程類之后奇颠,就可以創(chuàng)建線程對象了绍刮,然后通過start()方法去啟動線程赦役。不是調(diào)用run()方法啟動線程,run方法中只是定義需要執(zhí)行的任務(wù)妇汗,如果調(diào)用run方法帘不,即相當(dāng)于在主線程中執(zhí)行run方法,跟普通的方法調(diào)用沒有任何區(qū)別杨箭,此時(shí)并不會創(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方法的話,是不會創(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)建之后减俏,不會立即進(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)行下去竖共,比如用戶主動讓線程睡眠(睡眠一定的時(shí)間之后再重新執(zhí)行)蝙叛、用戶主動讓線程等待,或者被同步塊給阻塞公给,此時(shí)就對應(yīng)著多個(gè)狀態(tài):time wating借帘、wating、阻塞淌铐。

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

5.1.3 上下文切換

對于單核CPU來說(對于多核CPU腿准,此處就理解為一個(gè)核)际起,CPU在一個(gè)時(shí)刻只能運(yùn)行一個(gè)線程,當(dāng)在運(yùn)行一個(gè)線程的過程中轉(zhuǎn)去運(yùn)行另外一個(gè)線程,這個(gè)叫做==線程上下文切換==(對于進(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)燕偶,那么會記錄哪些數(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)伯诬。所以一般來說,線程上下文切換過程中會記錄程序計(jì)數(shù)器巫财、CPU寄存器狀態(tài)等數(shù)據(jù)盗似。說簡單點(diǎn):對于線程的上下文切換實(shí)際上就是存儲和恢復(fù)CPU狀態(tài)的過程,它使得線程執(zhí)行能夠從中斷點(diǎn)恢復(fù)執(zhí)行平项。

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

5.1.4 理解中斷

每一個(gè)線程都有一個(gè)用來表明當(dā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ū)別:前者是線程對象的方法椭符,調(diào)用它后==不清除==線程中斷標(biāo)志位荔燎;后者是Thread的靜態(tài)方法,調(diào)用它會==清除==線程中斷標(biāo)志位销钝。

所以說調(diào)用線程的interrupt()方法不會中斷一個(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í),都會使得當(dāng)前線程進(jìn)入阻塞狀態(tài)滤祖。那么此時(shí)如果在線程處于阻塞狀態(tài)下調(diào)用interrupt()方法會拋出一個(gè)異常筷狼,并且會清除線程中斷標(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()方法會清除狀態(tài)標(biāo)志位 所以這里需要再次重置中斷標(biāo)志位 否則線程會繼續(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()這些方法俏险,會檢測線程中斷標(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)

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

2. yield方法

  • `yield()

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

注意,調(diào)用yield方法==并不會讓線程進(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將會等待線程B執(zhí)行完畢后或等待一定的時(shí)間再執(zhí)行。實(shí)際上調(diào)用join方法是調(diào)用了Object的wait()方法山橄。wait方法會讓線程進(jìn)入阻塞狀態(tài)垮媒,并且會釋放線程占有的鎖,并交出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)先級抗愁。
  • 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ù)線程也會隨著消亡饵隙。 在JVM中撮珠,像垃圾收集器線程就是守護(hù)線程。
用戶線程:不依賴創(chuàng)建它的線程金矛,會一直運(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ù)組元素存儲在堆內(nèi)存中娶耍,堆內(nèi)存在線程之間共享(本文使用“共享變量”這個(gè)術(shù)語代指實(shí)例域,靜態(tài)域和數(shù)組元素)饼酿。

Java內(nèi)存模型(本文簡稱為JMM)定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存中榕酒,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(JMM的一個(gè)抽象概念),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本故俐。 線程對變量的所有操作都必須在本地內(nèi)存中進(jìn)行想鹰,而不能直接對主內(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è)沒有被保證,就有可能會導(dǎo)致程序運(yùn)行不正確廷区。

(1) 原子性

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

(2) 可見性

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

(3) 有序性

程序執(zhí)行的順序按照代碼的先后順序執(zhí)行玖绿。
指令重排序:處理器為了提高程序運(yùn)行效率敛瓷,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致斑匪,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的呐籽。不會影響單個(gè)線程的執(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)方法,而此時(shí)context并沒有被初始化垫释,就會導(dǎo)致程序出錯(cuò)丝格。

3. JAVA語言對于原子性撑瞧,可見性棵譬,有序性的保證

原子性

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

請分析以下哪些操作是原子性操作:

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

語句1是直接將數(shù)值10賦值給x,也就是說線程執(zhí)行這個(gè)語句的會直接將數(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í)私股,它會保證修改的值會立即被更新到主存埋心,當(dāng)有其他線程需要讀取時(shí),它會去內(nèi)存中讀取新值抄课。

有序性

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

4. volatile關(guān)鍵字

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

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

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

  • 對變量的寫操作不依賴于當(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)識BlockingQueue

  • 在新增的Concurrent包中葵第,高效且==線程安全==
  • 用來==處理消費(fèi)者生產(chǎn)者問題==绘迁。在多線程領(lǐ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里排在首位的對象,若BlockingQueue為空,阻塞進(jìn)入等待狀態(tài)直到BlockingQueue有新的數(shù)據(jù)被加入律罢。
  • poll(time) 取走排在首位的對象,若不能立即取出,則可以等time參數(shù)規(guī)定的時(shí)間,取不到時(shí)返回null。
  • poll(long timeout, TimeUnit unit) 取出一個(gè)隊(duì)首的對象棍丐,如果在指定時(shí)間內(nèi)误辑,隊(duì)列一旦有數(shù)據(jù)可取,則立即返回隊(duì)列中的數(shù)據(jù)歌逢。否則知道時(shí)間超時(shí)還沒有數(shù)據(jù)可取巾钉,返回失敗。
  • drainTo() 一次性從BlockingQueue獲取所有可用的數(shù)據(jù)對象(還可以指定獲取數(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ù)對象及標(biāo)識隊(duì)列的頭部和尾部在數(shù)組中的位置裕膀。生產(chǎn)和消費(fèi)時(shí)不會產(chǎn)生或銷毀任何額外的對象實(shí)例员串。
  • 在放入數(shù)據(jù)和獲取數(shù)據(jù),都是共用同一個(gè)鎖對象昼扛,兩者==無法并行運(yùn)行==寸齐。
  • 在創(chuàng)建時(shí),默認(rèn)采用非公平鎖》梅蓿可以控制對象的內(nèi)部鎖是否采用公平鎖。

2. LinkedBlockingQueue

  • 基于鏈表實(shí)現(xiàn)斯稳,也維持著一個(gè)數(shù)據(jù)緩沖隊(duì)列(該隊(duì)列由一個(gè)鏈表構(gòu)成)海铆。生產(chǎn)和消費(fèi)的時(shí)候會產(chǎn)生Node對象。
  • 生產(chǎn)者端和消費(fèi)者端分別采用了獨(dú)立的鎖來控制數(shù)據(jù)同步挣惰,高并發(fā)的情況下生產(chǎn)者和消費(fèi)者==可以并行==地操作隊(duì)列中的數(shù)據(jù)卧斟,以此來提高整個(gè)隊(duì)列的并發(fā)性能。
  • 構(gòu)造一個(gè)LinkedBlockingQueue對象憎茂,而沒有指定其容量大小珍语,LinkedBlockingQueue會默認(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í)候纠脾,需要創(chuàng)建Node對象進(jìn)行插入或移除,大批量數(shù)據(jù)的系統(tǒng)中蜕青,其對于GC的壓力會比較大苟蹈;而ArrayBlockingQueue內(nèi)部維護(hù)了一個(gè)數(shù)組,在生產(chǎn)和消費(fèi)的時(shí)候右核,是直接將枚舉對象插入或移除的汉操,不會產(chǎn)生或銷毀任何額外的對象實(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)先級的==無界隊(duì)列==扳抽,==存儲的對象必須是實(shí)現(xiàn)Comparable接口==。隊(duì)列通過這個(gè)接口的compare方法確定對象的priority。越小優(yōu)先級越高贸呢,優(yōu)先級越高镰烧,越優(yōu)先取出。但需要注意的是PriorityBlockingQueue并==不會阻塞數(shù)據(jù)生產(chǎn)者==楞陷,而只會在沒有可消費(fèi)的數(shù)據(jù)時(shí)怔鳖,==阻塞數(shù)據(jù)的消費(fèi)者==。因此使用的時(shí)候要特別注意固蛾,生產(chǎn)者生產(chǎn)數(shù)據(jù)的速度絕對不能快于消費(fèi)者消費(fèi)數(shù)據(jù)的速度结执,否則時(shí)間一長,會最終耗盡所有的可用堆內(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)不會被阻塞铸敏,而只有獲取數(shù)據(jù)的操作(消費(fèi)者)才會被阻塞。

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

5. SynchronousQueue

  • 一個(gè)不存儲元素的阻塞隊(duì)列糕非,可以理解為容量為0蒙具。==每個(gè)插入(移除)操作必須等待另一個(gè)線程的移除(插入)操作==⌒喾剩可以這樣來理解:生產(chǎn)者和消費(fèi)者互相等待對方禁筏,握手,然后一起離開衡招。類似于無中介的直接交易篱昔,有點(diǎn)像原始社會中的生產(chǎn)者和消費(fèi)者,生產(chǎn)者拿著產(chǎn)品去集市銷售給產(chǎn)品的最終消費(fèi)者始腾,而消費(fèi)者必須親自去集市找到所要商品的直接生產(chǎn)者州刽,如果一方?jīng)]有找到合適的目標(biāo),那么對不起浪箭,大家都在集市等待穗椅。相對于有緩沖的BlockingQueue來說,少了一個(gè)中間經(jīng)銷商的環(huán)節(jié)(緩沖區(qū))奶栖,如果有經(jīng)銷商匹表,生產(chǎn)者直接把產(chǎn)品批發(fā)給經(jīng)銷商门坷,而無需在意經(jīng)銷商最終會將這些產(chǎn)品賣給那些消費(fèi)者,由于經(jīng)銷商可以庫存一部分商品袍镀,因此相對于直接交易模式默蚌,總體來說采用中間經(jīng)銷商的模式會吞吐量高一些(可以批量買賣);但另一方面苇羡,又因?yàn)榻?jīng)銷商的引入绸吸,使得產(chǎn)品從生產(chǎn)者到消費(fèi)者中間增加了額外的交易環(huán)節(jié),單個(gè)產(chǎn)品的及時(shí)響應(yīng)性能可能會降低宣虾。
  • 聲明一個(gè)SynchronousQueue有兩種不同的方式:公平鎖和非公平鎖。SynchronousQueue<Integer> sc = new SynchronousQueue<>(true);//fair温数,默認(rèn)是不公平鎖绣硝。
    一個(gè)使用場景:
    在線程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue撑刺,這個(gè)線程池根據(jù)需要(新任務(wù)到來時(shí))創(chuàng)建新的線程鹉胖,如果有空閑線程則會重復(fù)使用,線程空閑了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()方法來看隊(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添加元素才會獲取元素向下執(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)境下点骑,他還自動管理了多線間的自動等待和喚醒功能酣难,從而使得程序員可以忽略這些細(xì)節(jié),關(guān)注更高級的功能黑滴。

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ù)將會直接影響到線程池的功能特性荞彼。

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

如果線程池中的線程數(shù)量未達(dá)到核心線程的數(shù)量, 那么會直接啟動一個(gè)核心線程來執(zhí)行任務(wù).
如果線程池中的線程數(shù)量已經(jīng)達(dá)到或者超過核心線程的數(shù)量, 那么任務(wù)會被插入到任務(wù)隊(duì)列中排隊(duì)等待執(zhí)行.
如果在步驟2中無法將任務(wù)插入到任務(wù)隊(duì)列中,這通常是因?yàn)槿蝿?wù)隊(duì)列已滿,這個(gè)時(shí)候如果線程數(shù)量未達(dá)到線程池的規(guī)定的最大值, 那么會立刻啟動一個(gè)非核心線程來執(zhí)行任務(wù).
如果步驟3中的線程數(shù)量已經(jīng)達(dá)到最大值的時(shí)候, 那么會拒絕執(zhí)行此任務(wù),ThreadPoolExecutor會調(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í), 它們并不會被回收, 除非線程池關(guān)閉了. 當(dāng)所有的線程都處于活動狀態(tài)時(shí), 新任務(wù)都會處于等待狀態(tài), 直到有線程空閑出來. 由于FixedThreadPool只有核心線程并且這些核心線程不會被回收, 這意味著它能夠更加快速地響應(yīng)外界的請求.

2. CachedThreadPool

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

3. ScheduledThreadPool

通過Executor#newScheduledThreadPool()方法來創(chuàng)建. 它的核心線程數(shù)量是固定的, 而非核心線程數(shù)是沒有限制的, 并且當(dāng)非核心線程閑置時(shí)會立刻被回收掉. 這類線程池用于執(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組件。
這樣會導(dǎo)致新啟動的線程無法修改UI趟据,此時(shí)需要Handler消息機(jī)制券犁。

5.5.1 ThreadLocal<T>的工作原理

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

1. 使用場景

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

(2) 復(fù)雜邏輯下的對象傳遞屯阀。比如監(jiān)聽器的傳遞缅帘,有時(shí)候一個(gè)線程中的任務(wù)過于復(fù)雜,表現(xiàn)為函數(shù)調(diào)用棧比較深以及代碼入口的多樣性难衰,而這時(shí)我們又希望監(jiān)聽器能夠貫穿整個(gè)線程的執(zhí)行過程钦无。此時(shí)可以讓監(jiān)聽器作為線程內(nèi)的全局對象而存在,在線程內(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對象中的Entry[]數(shù)組的某個(gè)Entry中决记。Entry類似于一個(gè)Map,key是ThreadLocal對象悠栓,value是具體的值T霉涨,重復(fù)設(shè)置會覆蓋按价。
  • get() T 循環(huán)當(dāng)前線程內(nèi)部的ThreadLocal.ThreadLocalMap對象中的Entry[]數(shù)組惭适,取出當(dāng)前對象的key對應(yīng)的值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);//獲取當(dāng)前線程的ThreadLocal.ThreadLocalMap對象
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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

5.5.2 MessageQueue的工作原理

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

1. Message

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

1)屬性

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

2)方法

  • sendToTarget()
  • obtain() Message 從消息池中獲取一個(gè)消息對象。不建議使用new Message()構(gòu)造描睦。
  • obtain(Message orign) Message 拷貝一個(gè)Message對象
  • 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è)線程對應(yīng)一個(gè)Looper,一個(gè)MessageQueue(創(chuàng)建Looper的時(shí)候創(chuàng)建)韵丑。Looper會不停地從MessageQueue中查看是否有新消息爵卒,如果有新消息就會立即處理,否則就一直阻塞在那里撵彻。

private static void prepare(boolean quitAllowed) {
    ...
    //sThreadLocal是一個(gè)靜態(tài)變量钓株,保證了線程和Looper對象的一對一
    //存一個(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)有新消息弧烤,會返回這條消息并將其從單鏈表中移除
        Message msg = queue.next();
        ...
        //處理懒浮。msg.target是發(fā)送這條消息的Handler對象,這樣Handler發(fā)送的消息最終又交給Handler來處理了
        msg.target.dispatchMessage(msg);
        ...
    }
}

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

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

1.方法

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

5.5.4 Handler的工作原理

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

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

構(gòu)造方法

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

sendMessage(Message msg)

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

dispatchMessage(Message msg)

//handler處理消息的過程。由Looper#loop()調(diào)用爵政,運(yùn)行在Looper所在線程仅讽。若主動調(diào)用,就運(yùn)行在調(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(接口,只有一個(gè)方法boolean handleMessage(Message msg))
        if (mCallback != null) {
        //返回值決定了Handler#handleMessage(Message msg)是否會被執(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ì)列中汤锨,將被執(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對象(主線程不用創(chuàng)建双抽,已經(jīng)有了),然后Looper
.loop()
②:創(chuàng)建Handler子類的實(shí)例泥畅,重寫handleMessages()方法荠诬,處理消息

3. HandlerThread

一個(gè)為了快速創(chuàng)建包含Looper的一個(gè)線程類琅翻, start()時(shí)就創(chuàng)建了Looper和MessageQueue對象(本質(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)會通過Looper.prepareMainLooper()來創(chuàng)建主線程的Looper以及MessageQueue聂抢,并通過Looper.loop()來開啟主線程的消息循環(huán)。

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

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

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

問題1:startActivity的時(shí)候俐筋,會向AMS(ActivityManagerService)發(fā)一個(gè)跨進(jìn)程請求(AMS運(yùn)行在系統(tǒng)進(jìn)程中)牵素,之后AMS啟動對應(yīng)的Activity;AMS也需要調(diào)用App中Activity的生命周期方法(不同進(jìn)程不可直接調(diào)用)澄者,AMS會發(fā)送跨進(jìn)程請求笆呆,然后由App的ActivityThread中的ApplicationThread會來處理,ApplicationThread會通過主線程線程的Handler將執(zhí)行邏輯切換到主線程粱挡。重點(diǎn)來了腰奋,主線程的Handler把消息添加到了MessageQueue,Looper.loop會拿到該消息抱怔,并在主線程中執(zhí)行劣坊。這就解釋了為什么主線程的Looper是個(gè)死循環(huán),而Activity還能啟動屈留,因?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ì)分析請看原書作者的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會自動退出。由于它是一種Service又碌,所以不容易被系統(tǒng)殺死,底層是Thread 教馆。

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

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

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

1. AsyncTask

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

abstract class AsyncTask<Params, Progress, Result>

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

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

(1) 四個(gè)核心方法
  • onPreExecute() void
    在主線程執(zhí)行, 在異步任務(wù)執(zhí)行之前, 此方法會被調(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()方法會調(diào)用onProgressUpdate()方法辫诅。另外此方法需要返回計(jì)算結(jié)果給onPostExecute()
  • onProgressUpdate(Progress... values) void
    在主線程執(zhí)行凭戴,當(dāng)后臺任務(wù)publishProgress()時(shí),會被調(diào)用炕矮。
  • onPostExecute(Result res) void
    在主線程執(zhí)行, 在異步任務(wù)執(zhí)行之后, 此方法會被調(diào)用, 其中result參數(shù)是后臺任務(wù)的返回值, 即doInBackground的返回值么夫。

除了上述的四種方法,還有onCancelled(), 它同樣在主線程執(zhí)行, 當(dāng)異步任務(wù)被取消時(shí)調(diào)用,這個(gè)時(shí)候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()
  • 一個(gè)AsyncTask對象只能執(zhí)行一次, 即只能調(diào)用一次execute()方法, 否則會報(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)會把AsyncTask#Params參數(shù)封裝成FutureTask對象, FutureTask是一個(gè)并發(fā)類, 在這里充當(dāng)了Runnable的作用. 接著這個(gè)FutureTask會交給SerialExecutor#execute()方法去處理. 這個(gè)方法首先會把FutureTask對象插入到任務(wù)隊(duì)列mTasks中, 如果這個(gè)時(shí)候沒有正在活動AsyncTask任務(wù), 那么就會調(diào)用SerialExecutor#scheduleNext()方法來執(zhí)行下一個(gè)AsyncTask任務(wù). 同時(shí)當(dāng)一個(gè)AsyncTask任務(wù)執(zhí)行完后, AsyncTask會繼續(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í)行完成后會自動停止泵督,優(yōu)先級遠(yuǎn)高于普通線程,適合執(zhí)行一些高優(yōu)先級的后臺任務(wù)庶喜; IntentService封裝了HandlerThread和Handler

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

參考文獻(xiàn)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芬失,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子匾灶,更是在濱河造成了極大的恐慌棱烂,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阶女,死亡現(xiàn)場離奇詭異颊糜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)秃踩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進(jìn)店門衬鱼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人憔杨,你說我怎么就攤上這事鸟赫。” “怎么了消别?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵抛蚤,是天一觀的道長。 經(jīng)常有香客問我妖啥,道長霉颠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任荆虱,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘怀读。我一直安慰自己诉位,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布菜枷。 她就那樣靜靜地躺著苍糠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪啤誊。 梳的紋絲不亂的頭發(fā)上岳瞭,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天,我揣著相機(jī)與錄音蚊锹,去河邊找鬼瞳筏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牡昆,可吹牛的內(nèi)容都是我干的姚炕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼丢烘,長吁一口氣:“原來是場噩夢啊……” “哼柱宦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起播瞳,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤掸刊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后赢乓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痒给,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年骏全,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了苍柏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,989評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姜贡,死狀恐怖试吁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情楼咳,我是刑警寧澤熄捍,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站母怜,受9級特大地震影響余耽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苹熏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一碟贾、第九天 我趴在偏房一處隱蔽的房頂上張望币喧。 院中可真熱鬧,春花似錦袱耽、人聲如沸杀餐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽史翘。三九已至,卻和暖如春冀续,著一層夾襖步出監(jiān)牢的瞬間琼讽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工洪唐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钻蹬,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓桐罕,卻偏偏與公主長得像脉让,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子功炮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評論 2 345

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