多線程

??一個(gè)任務(wù)通常就是一個(gè)程序似扔,每個(gè)運(yùn)行中的程序就是一個(gè)進(jìn)程逛尚。當(dāng)一個(gè)程序運(yùn)行時(shí)垄惧,內(nèi)部可能包含了多個(gè)順序執(zhí)行流,每個(gè)順序執(zhí)行流就是一個(gè)線程绰寞。
??

進(jìn)程


定義:

??當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí)到逊,即變成一個(gè)進(jìn)程。進(jìn)程是處于運(yùn)行過程中的程序滤钱,并且具有一定的獨(dú)立功能觉壶,進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。

進(jìn)程的特點(diǎn):

  1. 獨(dú)立性:是系統(tǒng)獨(dú)立存在的實(shí)體菩暗,擁有自己獨(dú)立的資源掰曾,有自己私有的地址空間。在沒有經(jīng)過進(jìn)程本身允許的情況下停团,一個(gè)用戶的進(jìn)程不可以直接訪問其他進(jìn)程的地址空間旷坦。

  2. 動(dòng)態(tài)性:進(jìn)程與程序的區(qū)別在于:程序只是一個(gè)靜態(tài)的指令集合,而進(jìn)程是一個(gè)正在系統(tǒng)中活動(dòng)的指令集和佑稠,進(jìn)程中加入了時(shí)間的概念秒梅。進(jìn)程具有自己的生命周期和不同的狀態(tài),這些都是程序不具備的舌胶。

  3. 并發(fā)性:多個(gè)進(jìn)程可以在單個(gè)處理器上并發(fā)執(zhí)行捆蜀,多個(gè)進(jìn)程之間不會(huì)相互影響。

??

并行性和并發(fā)性


??并行:指在同一時(shí)刻幔嫂,有多條指令在多個(gè)處理上同時(shí)執(zhí)行辆它。(多核同時(shí)工作)

??并發(fā):指在同一時(shí)刻只能有一條指令執(zhí)行,但多個(gè)進(jìn)程指令被快速輪換執(zhí)行履恩,使得在宏觀上具有多個(gè)進(jìn)程同時(shí)執(zhí)行的效果锰茉。(單核在工作,單核不停輪詢)

??

線程


??多線程擴(kuò)展了多進(jìn)程的概念切心,使得同一個(gè)進(jìn)程可以同時(shí)并發(fā)處理多個(gè)任務(wù)飒筑。
??線程(Thread)也被成為輕量級的進(jìn)程片吊,線程是進(jìn)程執(zhí)行的單元,線程在程序中是獨(dú)立的协屡、并發(fā)的執(zhí)行流

??當(dāng)進(jìn)程被初始化后俏脊,主線程就被創(chuàng)建了。絕大數(shù)應(yīng)用程序只需要有一個(gè)主線程肤晓,但也可以在進(jìn)程內(nèi)創(chuàng)建多條的線程爷贫,每個(gè)線程也是相互獨(dú)立的。

??一個(gè)進(jìn)程可以擁有多個(gè)線程补憾,一個(gè)線程必須有一個(gè)父進(jìn)程沸久。

??線程可以擁有自己的堆棧、自己的程序計(jì)數(shù)器和自己的局部變量余蟹,但不擁有系統(tǒng)資源卷胯,它與父進(jìn)程的其他線程共享該進(jìn)程所擁有的全部資源,因此編程更加方便威酒。

??線程是獨(dú)立運(yùn)行的窑睁,它并不知道進(jìn)程中是否還有其他的線程存在。線程的執(zhí)行是搶占式的葵孤,即:當(dāng)前運(yùn)行的線程在任何時(shí)候都有可能被掛起担钮,以便另外一個(gè)線程可以運(yùn)行。

??一個(gè)線程可以創(chuàng)建和撤銷另一個(gè)線程尤仍,同一個(gè)進(jìn)程中多個(gè)線程之間可以并發(fā)執(zhí)行箫津。

??線程的調(diào)度和管理由進(jìn)程本身負(fù)責(zé)完成。

??歸納而言:操作系統(tǒng)可以同時(shí)執(zhí)行多個(gè)任務(wù)宰啦,每個(gè)任務(wù)就是進(jìn)程苏遥;進(jìn)程可以同時(shí)執(zhí)行多個(gè)任務(wù),每個(gè)任務(wù)就是線程

??

多線程的優(yōu)點(diǎn):


  1. 進(jìn)程之間不能共享內(nèi)存赡模,但線程之間共享內(nèi)存非常容易

  2. 系統(tǒng)創(chuàng)建進(jìn)程要為該進(jìn)程重新分配系統(tǒng)資源田炭,但創(chuàng)建線程的代價(jià)則小得多。因此多線程實(shí)現(xiàn)多任務(wù)并發(fā)比多線程的效率高漓柑。

  3. Java語言內(nèi)置了多線程功能支撐教硫,簡化了多線程的編程。

??
??

線程的創(chuàng)建和啟動(dòng)


一辆布、繼承Thread類創(chuàng)建線程類

步驟:
① 定義Thread類的子類瞬矩,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務(wù)锋玲,稱為線程執(zhí)行體

② 創(chuàng)建Thread子類的實(shí)例景用,即創(chuàng)建了線程對象

③ 調(diào)用線程對象的start()方法來啟動(dòng)該線程
示例:

// 通過繼承Thread類來創(chuàng)建線程類
public class FirstThread extends Thread
{
    private int i ;
    // 重寫run方法,run方法的方法體就是線程執(zhí)行體
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            // 當(dāng)線程類繼承Thread類時(shí)嫩絮,直接使用this即可獲取當(dāng)前線程
            // Thread對象的getName()返回當(dāng)前該線程的名字
            // 因此可以直接調(diào)用getName()方法返回當(dāng)前線程的名
            System.out.println(getName() +  " " + i);
        }
    }
    public static void main(String[] args)
    {
        for (int i = 0; i < 100;  i++)
        {
            // 調(diào)用Thread的currentThread方法獲取當(dāng)前線程
            System.out.println(Thread.currentThread().getName() +  " " + i);
            if (i == 20)
            {
                // 創(chuàng)建丛肢、并啟動(dòng)第一條線程
                new FirstThread().start();
                // 創(chuàng)建、并啟動(dòng)第二條線程
                new FirstThread().start();
            }
        }
    }
}
注意點(diǎn):

① 當(dāng)Java程序開始運(yùn)行后剿干,程序至少會(huì)創(chuàng)建一個(gè)主線程蜂怎,main()方法的方法體代表主線程的線程執(zhí)行體

② 當(dāng)線程類繼承Tread類時(shí),直接使用this即可以獲取當(dāng)前線程

③ 繼承Thread類創(chuàng)建線程類置尔,多個(gè)線程之間無法共享線程類的實(shí)例變量

??

二杠步、實(shí)現(xiàn)Runnable接口創(chuàng)建線程類

步驟:

① 定義Runnable接口的實(shí)現(xiàn)類,并重寫該接口的run()方法

② 創(chuàng)建Runnable實(shí)現(xiàn)類的實(shí)例榜轿,并以此實(shí)例作為Thread的target來創(chuàng)建Tread對象幽歼,該Tread對象才是真正的線程對象

// 通過實(shí)現(xiàn)Runnable接口來創(chuàng)建線程類
public class SecondThread implements Runnable
{
    private int i ;

    // run方法同樣是線程執(zhí)行體
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            // 當(dāng)線程類實(shí)現(xiàn)Runnable接口時(shí),
            // 如果想獲取當(dāng)前線程谬盐,只能用Thread.currentThread()方法甸私。
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100;  i++)
        {
            System.out.println(Thread.currentThread().getName() + "  " + i);
            if (i == 20)
            {
                SecondThread st = new SecondThread();     // ①

                // 通過new Thread(target , name)方法創(chuàng)建新線程
                new Thread(st , "新線程1").start();
                new Thread(st , "新線程2").start();
            }
        }
    }
}
注意點(diǎn):

① 實(shí)現(xiàn)Runnable接口創(chuàng)建線程類,必須通過Thread.currentThread()方法來獲得當(dāng)前線程對象

② 實(shí)現(xiàn)Runnable接口創(chuàng)建線程類飞傀,多個(gè)線程可以共享線程類的實(shí)例變量

??

三皇型、使用Callable和Future創(chuàng)建線程

Callable接口提供了一個(gè)call()方法,call()方法比run()方法更強(qiáng)大:
① call()方法可以由返回值

② call()方法可以聲明拋出異常

步驟:

① 創(chuàng)建Callable接口的實(shí)現(xiàn)類砸烦,并實(shí)現(xiàn)call()方法弃鸦,該call()方法作為線程執(zhí)行體,且該call()方法有返回值

② 使用FutureTask類來包裝Callable對象幢痘,該FutureTask對象封裝了該Callable對象的call()方法的返回值

③ 調(diào)用FutureTask對象的get()方法獲得子線程執(zhí)行結(jié)束的返回值

示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//使用Callable接口和Future來創(chuàng)建線程
public class ThreadFuture {
    //拋出異常
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //創(chuàng)建FutureTask對象唬格,包裝 Callable接口實(shí)例
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
            int sum = 0;
            for(int i = 0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
            //注意看這里有返回值
            return sum;
        });
        //使用task作為 Thread類的target 來創(chuàng)建一個(gè)線程
        Thread instance = new Thread(task);
        //啟動(dòng)線程
        instance.start();
        //sleep一段時(shí)間,讓上面的線程執(zhí)行完畢
        Thread.sleep(1000);
        
        //這里可以調(diào)用task.get() 獲取上面的那個(gè)線程的返回值
        System.out.println("線程返回值:"+task.get());
        
    }
}

??
??

創(chuàng)建線程三種方式的對比:


實(shí)現(xiàn)Runnable接口颜说、Callable接口創(chuàng)建線程

優(yōu)點(diǎn):
①實(shí)現(xiàn)的是接口购岗,還可以繼承其他類

② 多個(gè)線程可以共享同一個(gè)target對象,適合多個(gè)相同的線程來處理同一份資源的情況

缺點(diǎn):
① 編程稍微復(fù)雜

② 獲取當(dāng)前線程必須用Thread.currentThread()方法來獲得

繼承Tread類創(chuàng)建線程

優(yōu)點(diǎn):
①編程簡單

② 獲取當(dāng)前線程门粪,可以直接使用this來獲得

缺點(diǎn):
① 已經(jīng)繼承了Thread類藕畔,不能繼承其他類

??
??

線程的生命周期


線程的生命周期要經(jīng)歷新建(New)、就緒(Runnable)庄拇、運(yùn)行(Running)注服、阻塞(Blocke)和死亡(Dead)5種狀態(tài)。

尤其是當(dāng)線程啟動(dòng)以后措近,它不可能一直“霸占”著CPU獨(dú)自運(yùn)行溶弟,所以CPU需要在多條線程之間切換,于是線程狀態(tài)也會(huì)多次在運(yùn)行瞭郑、阻塞之間切換辜御。

1、新建和就緒狀態(tài)

當(dāng)程序使用new關(guān)鍵字創(chuàng)建了一個(gè)線程之后屈张,該線程就處于新建狀態(tài)擒权,此時(shí)它僅僅由Java虛擬機(jī)為其分配內(nèi)存袱巨,并且初始化其成員變量的值。此時(shí)的線程對象沒有表現(xiàn)出任何線程隊(duì)動(dòng)態(tài)特征碳抄,程序也不會(huì)執(zhí)行線程的線程執(zhí)行體愉老。

當(dāng)線程對象調(diào)用了start()方法之后,該線程處于就緒狀態(tài)剖效,Java虛擬機(jī)會(huì)為其創(chuàng)建方法調(diào)用棧和程序計(jì)數(shù)器嫉入,處于這個(gè)狀態(tài)中的線程并沒有開始運(yùn)行,只是表示該線程可以運(yùn)行了璧尸,至于該線程何時(shí)開始運(yùn)行咒林,取決于JVM里線程調(diào)度器的調(diào)度。

tips:

  • 啟動(dòng)線程使用start()方法爷光,而不是run()方法垫竞,如果調(diào)用run()方法,則run()方法立即就會(huì)被執(zhí)行蛀序,而且在run()方法返回之前件甥,其他線程無法并發(fā)執(zhí)行,也就是說哼拔,如果直接調(diào)用線程對象的run()方法引有,系統(tǒng)把線程對象當(dāng)成一個(gè)普通對象,而run()方法也是一個(gè)普通方法倦逐,而不是線程執(zhí)行體譬正。

  • 如果直接調(diào)用線程對象的run()方法,則run()方法里不能直接通過getName()方法來獲得當(dāng)前執(zhí)行線程的名字檬姥,而是需要使用Thread.currentThread()方法先獲得當(dāng)前線程曾我,再調(diào)用線程對象的getName()方法來獲得線程的名字。啟動(dòng)線程的正確方法是調(diào)用Thread對象的start()方法健民,而不是直接調(diào)用run()方法抒巢,否則就變成單線程程序了。

  • 調(diào)用了線程的run()方法之后秉犹,該線程已經(jīng)不再處于新建狀態(tài)蛉谜,不要再次調(diào)用線程對象的start()方法。

2崇堵、運(yùn)行和阻塞狀態(tài)

如果處于就緒狀態(tài)的線程獲得了CPU型诚,開始執(zhí)行run()方法的線程執(zhí)行體,則該線程處于運(yùn)行狀態(tài)鸳劳。

但線程不可能一直處于運(yùn)行狀態(tài)狰贯,它在運(yùn)行過程中會(huì)被中斷,從而進(jìn)入一個(gè)阻塞的狀態(tài)

當(dāng)發(fā)生如下情況時(shí),線程將會(huì)進(jìn)入阻塞狀態(tài):

1涵紊、線程調(diào)用sleep()方法主動(dòng)放棄所占用的處理器資源傍妒。

2、線程調(diào)用了一個(gè)阻塞式IO方法摸柄,在該方法返回之前颤练,該線程被阻塞。

3塘幅、線程試圖獲得一個(gè)同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有尿贫。

4电媳、線程在等待某個(gè)通知(notify)。

5庆亡、程序調(diào)用了線程的suspend()方法將該線程掛起匾乓。但這個(gè)方法容易導(dǎo)致死鎖,所以應(yīng)該盡量避免使用該方法又谋。

針對上面幾種情況拼缝,當(dāng)發(fā)生如下特定的情況時(shí)可以解除上面的阻塞,讓該線程重新進(jìn)入就緒狀態(tài)彰亥。

1咧七、調(diào)用sleep()方法的線程經(jīng)過了指定時(shí)間。

2任斋、線程調(diào)用的阻塞式IO方法已經(jīng)返回继阻。

3、 線程成功地獲得了試圖取得的同步監(jiān)視器废酷。

4瘟檩、 線程正在等待某個(gè)通知時(shí),其他線程發(fā)出了一個(gè)通知澈蟆。

5墨辛、處于掛起狀態(tài)的線程被調(diào)用了resume()恢復(fù)方法。

線程狀態(tài)轉(zhuǎn)化圖

從圖中可以看出趴俘,線程從阻塞狀態(tài)只能進(jìn)入就緒狀態(tài)睹簇,無法直接進(jìn)入運(yùn)行狀態(tài)。

而就緒和運(yùn)行狀態(tài)之間的轉(zhuǎn)換通常不受程序控制寥闪,而是由系統(tǒng)線程調(diào)度所決定带膀。

當(dāng)處于就緒狀態(tài)的線程獲得處理器資源時(shí),該線程進(jìn)入運(yùn)行狀態(tài)橙垢;當(dāng)處于運(yùn)行狀態(tài)的線程失去處理器資源時(shí)垛叨,該線程進(jìn)入就緒狀態(tài)。

但有一個(gè)方法例外,調(diào)用yield()方法可以讓運(yùn)行狀態(tài)的線程轉(zhuǎn)入就緒狀態(tài)嗽元。

線程死亡

線程會(huì)以如下三種方式結(jié)束敛纲,結(jié)束后就處于死亡狀態(tài)。

  • run()call()方法執(zhí)行完成剂癌,線程正常結(jié)束淤翔。

  • 線程拋出一個(gè)未捕獲的ExceptionError

  • 直接調(diào)用該線程的stop()方法來結(jié)束該線程——該方法容易導(dǎo)致死鎖佩谷,通常不推薦使用旁壮。

tips:

1、當(dāng)主線程結(jié)束時(shí)谐檀,其他線程不受任何影響抡谐,并不會(huì)隨之結(jié)束。一旦子線程啟動(dòng)起來后桐猬,它就擁有和主線程相同的地位麦撵,它不會(huì)受主線程的影響。

2溃肪、為了測試某個(gè)線程是否已經(jīng)死亡免胃,可以調(diào)用線程對象的isAlive()方法,當(dāng)線程處于就緒惫撰、運(yùn)行羔沙、阻塞三種狀態(tài)時(shí),該方法將返回true厨钻;當(dāng)線程處于新建撬碟、死亡兩種狀態(tài)時(shí),該方法將返回false莉撇。

3呢蛤、不要試圖對一個(gè)已經(jīng)死亡的線程調(diào)用start()方法使它重新啟動(dòng),死亡就是死亡棍郎,該線程將不可再次作為線程執(zhí)行其障。在線程已經(jīng)死亡的情況下再次調(diào)用start()方法將會(huì)引發(fā)IIIegalThreadException異常。

4涂佃、不能對死亡的線程調(diào)用start()方法励翼,程序只能對新建狀態(tài)的線程調(diào)用start()方法,對新建的線程兩次調(diào)用start()方法也是錯(cuò)誤的辜荠,會(huì)引發(fā)IIIegalThreadStateException異常汽抚。

??

控制線程


1、join線程

Thread提供了讓一個(gè)線程等待另一個(gè)線程完成的方法:join()方法伯病。當(dāng)在某個(gè)程序執(zhí)行流中調(diào)用其他線程的join()方法時(shí)造烁,調(diào)用線程將被阻塞,直到被join()方法加入的join線程執(zhí)行完為止。

join()方法通常由使用線程的程序調(diào)用惭蟋,以將大問題劃分成許多小問題苗桂,每個(gè)小問題分配一個(gè)線程。當(dāng)所有的小問題都得到處理后告组,再調(diào)用主線程來進(jìn)一步操作煤伟。

代碼示例:

public class JoinThread extends Thread
{
    // 提供一個(gè)有參數(shù)的構(gòu)造器,用于設(shè)置該線程的名字
    public JoinThread(String name)
    {
        super(name);
    }
    // 重寫run()方法木缝,定義線程執(zhí)行體
    public void run()
    {
        for (int i = 0; i < 100 ; i++ )
        {
            System.out.println(getName() + "  " + i);
        }
    }
    public static void main(String[] args)throws Exception
    {
        // 啟動(dòng)子線程
        new JoinThread("新線程").start();
        for (int i = 0; i < 100 ; i++ )
        {
            if (i == 20)
            {
                JoinThread jt = new JoinThread("被Join的線程");
                jt.start();
                // main線程調(diào)用了jt線程的join()方法便锨,main線程必須等jt執(zhí)行結(jié)束才會(huì)向下執(zhí)行
                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

2解取、后臺(tái)線程

有一種線程杠输,它是在后臺(tái)運(yùn)行的,它的任務(wù)是為其他的線程提供服務(wù)帚稠,這種線程被稱為“后臺(tái)線程(Daemon Thread)”怎囚,又稱為“守護(hù)線程”或“精靈線程”卿叽。JVM的垃圾回收線程就是典型的后臺(tái)線程桥胞。

后臺(tái)線程有個(gè)特征:如果所有的前臺(tái)線程都死亡恳守,后臺(tái)線程會(huì)自動(dòng)死亡。

調(diào)用Thread對象的setDaemon(true)方法可將指定線程設(shè)置成后臺(tái)線程贩虾。

tips:
1催烘、Thread類還提供了一個(gè)isDaemon()方法,用于判斷指定線程是否為后臺(tái)線程缎罢。

2伊群、前臺(tái)線程創(chuàng)建的子線程默認(rèn)是前臺(tái)線程,后臺(tái)線程子線程默認(rèn)是后臺(tái)線程策精。

3舰始、前臺(tái)線程死亡后,JVM會(huì)通知后臺(tái)線程死亡咽袜,但從它接收指令到做出響應(yīng)丸卷,需要一定時(shí)間。

而且要將某個(gè)線程設(shè)置為后臺(tái)線程询刹,必須在該線程啟動(dòng)之前設(shè)置谜嫉,也就是說,setDaemon(true)必須在start()方法之前調(diào)用凹联,否則會(huì)引發(fā)llegalThreadStateException異常沐兰。

3、線程睡眠:sleep

如果需要讓當(dāng)前正在執(zhí)行的線程暫停一段時(shí)間蔽挠,并進(jìn)入阻塞狀態(tài)住闯,則可以通過調(diào)用Thread類的靜態(tài)sleep()方法來實(shí)現(xiàn)。

4、線程讓步yield()

yield()方法是一個(gè)和sleep()方法有點(diǎn)相似的方法寞秃,它也是Thread類提供的一個(gè)靜態(tài)方法斟叼,它也可以讓當(dāng)前正在執(zhí)行的線程暫停,但它不會(huì)阻塞該線程春寿,它只是將該線程轉(zhuǎn)入就緒狀態(tài)朗涩。

yield()只是讓當(dāng)前線程暫停一下,讓系統(tǒng)的線程調(diào)度器重新調(diào)度一次绑改,完全可能的情況是:當(dāng)某個(gè)線程調(diào)用了yield()方法暫停之后谢床,線程調(diào)度器又將其調(diào)度出來重新執(zhí)行。

實(shí)際上厘线,當(dāng)某個(gè)線程調(diào)用了yield()方法暫停之后识腿,只有優(yōu)先級與當(dāng)前線程相同,或者優(yōu)先級比當(dāng)前線程更高的處于就緒狀態(tài)的線程才會(huì)獲得執(zhí)行的機(jī)會(huì)造壮。

關(guān)于sleep()方法和yield()方法的區(qū)別如下

  1. sleep()方法暫停當(dāng)前線程后渡讼,會(huì)給其他線程執(zhí)行機(jī)會(huì),不會(huì)理會(huì)其他線程的優(yōu)先級耳璧;但yield()方法只會(huì)給優(yōu)先級相同成箫,或優(yōu)先級更高的線程執(zhí)行機(jī)會(huì)。

  2. sleep()方法會(huì)將線程轉(zhuǎn)入阻塞狀態(tài)旨枯,直到經(jīng)過阻塞時(shí)間才會(huì)轉(zhuǎn)入就緒狀態(tài)蹬昌;而yield()不會(huì)將線程轉(zhuǎn)入阻塞狀態(tài),它只是強(qiáng)制當(dāng)前線程進(jìn)入就緒狀態(tài)攀隔。因此完全有可能某個(gè)線程調(diào)用yield()方法暫停之后皂贩,立即再次獲得處理器資源被執(zhí)行。

  3. sleep()方法聲明拋出了InterruptedException 異常昆汹,所以調(diào)用sleep()方法時(shí)要么捕捉該異常明刷,要么顯式聲明拋出該異常;而yield()方法則沒有聲明拋出任何異常满粗。

  4. sleep()方法比yield()方法有更好的可移植性辈末,通常不建議使用yield()方法來控制并發(fā)線程的執(zhí)行。

5败潦、改變線程優(yōu)先級

通過Thread類提供的setPriority(int newPriority)本冲、getPriority()方法來設(shè)置和返回指定線程的優(yōu)先級。

setPriority()方法的參數(shù)可以是一個(gè)整數(shù)劫扒,范圍是1~10之間檬洞,也可以使用Thread類的如下三個(gè)靜態(tài)常量。

MAXPRIORITY:其值是10沟饥。

MIN PRIORITY:其值是1添怔。

NORM_PRIORITY:其值是5湾戳。

??

線程同步


為了解決多個(gè)線程訪問同一個(gè)數(shù)據(jù)時(shí),會(huì)出現(xiàn)問題广料,因此需要進(jìn)行線程同步砾脑。就像前面介紹的文件并發(fā)訪問,當(dāng)有兩個(gè)進(jìn)程并發(fā)修改同一個(gè)文件時(shí)就有可能造成異常艾杏。

1韧衣、同步代碼塊

為了解決線程同步問題,Java的多線程支持引入了同步監(jiān)視器來解決這個(gè)問題购桑,使用同步監(jiān)視器的通用方法就是同步代碼塊畅铭。同步代碼塊的語法格式如下:

synchronized(obj)
  {.....
        //此處的代碼就是同步代碼塊
   }

上面語法格式中synchronized后括號里的obj就是同步監(jiān)視器,上面代碼的含義是:線程開始執(zhí)行同步代碼塊之前勃蜘,必須先獲得對同步監(jiān)視器的鎖定硕噩。

任何時(shí)刻只能有一個(gè)線程可以獲得對同步監(jiān)視器的鎖定,當(dāng)同步代碼塊執(zhí)行完成后缭贡,該線程會(huì)釋放對該同步監(jiān)視器的鎖定炉擅。

通常推薦使用可能被并發(fā)訪問的共享資源充當(dāng)同步監(jiān)視器,代碼示例如下:

public class DrawThread extends Thread
{
    // 模擬用戶賬戶
    private Account account;
    // 當(dāng)前取錢線程所希望取的錢數(shù)
    private double drawAmount;
    public DrawThread(String name , Account account
        , double drawAmount)
    {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    // 當(dāng)多條線程修改同一個(gè)共享數(shù)據(jù)時(shí)阳惹,將涉及數(shù)據(jù)安全問題谍失。
    public void run()
    {
        // 使用account作為同步監(jiān)視器,任何線程進(jìn)入下面同步代碼塊之前穆端,
        // 必須先獲得對account賬戶的鎖定——其他線程無法獲得鎖袱贮,也就無法修改它
        // 這種做法符合:“加鎖 → 修改 → 釋放鎖”的邏輯
        synchronized (account)
        {
            // 賬戶余額大于取錢數(shù)目
            if (account.getBalance() >= drawAmount)
            {
                // 吐出鈔票
                System.out.println(getName()
                    + "取錢成功仿便!吐出鈔票:" + drawAmount);
                try
                {
                    Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                // 修改余額
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t余額為: " + account.getBalance());
            }
            else
            {
                System.out.println(getName() + "取錢失斕鍐!余額不足嗽仪!");
            }
        }
        // 同步代碼塊結(jié)束荒勇,該線程釋放同步鎖
    }
}

??

2、同步方法

同步方法就是使用synchronized關(guān)鍵字來修飾某個(gè)方法闻坚,則該方法稱為同步方法沽翔。

對于synchronized修飾的實(shí)例方法(非static方法)而言,無須顯式指定同步監(jiān)視器窿凤,同步方法的同步監(jiān)視器是this仅偎,也就是調(diào)用該方法的對象。

通過使用同步方法可以非常方便地實(shí)現(xiàn)線程安全的類雳殊,線程安全的類具有如下特征橘沥。

  1. 該類的對象可以被多個(gè)線程安全地訪問。

  2. 每個(gè)線程調(diào)用該對象的任意方法之后都將得到正確結(jié)果夯秃。

  3. 每個(gè)線程調(diào)用該對象的任意方法之后座咆,該對象狀態(tài)依然保持合理狀態(tài)痢艺。

代碼示例:

public class Account
{
    // 封裝賬戶編號、賬戶余額兩個(gè)成員變量
    private String accountNo;
    private double balance;
    public Account(){}
    // 構(gòu)造器
    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
        return this.accountNo;
    }
    // 因此賬戶余額不允許隨便修改介陶,所以只為balance提供getter方法堤舒,
    public double getBalance()
    {
        return this.balance;
    }

    // 提供一個(gè)線程安全draw()方法來完成取錢操作
    public synchronized void draw(double drawAmount)
    {
        // 賬戶余額大于取錢數(shù)目
        if (balance >= drawAmount)
        {
            // 吐出鈔票
            System.out.println(Thread.currentThread().getName()
                + "取錢成功!吐出鈔票:" + drawAmount);
            try
            {
                Thread.sleep(1);
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
            // 修改余額
            balance -= drawAmount;
            System.out.println("\t余額為: " + balance);
        }
        else
        {
            System.out.println(Thread.currentThread().getName()
                + "取錢失敳肝亍舌缤!余額不足!");
        }
    }

    // 下面兩個(gè)方法根據(jù)accountNo來重寫hashCode()和equals()方法
    public int hashCode()
    {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if (obj !=null
            && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

??
上面程序中增加了一個(gè)代表取錢的draw()方法某残,并使用了synchronized關(guān)鍵字修飾該方法友驮,把該方法變成同步方法。

該同步方法的同步監(jiān)視器是this驾锰,因此對于同一個(gè)Account賬戶而言卸留,任意時(shí)刻只能有一個(gè)線程獲得對Account對象的鎖定,然后進(jìn)入draw()方法執(zhí)行取錢操作椭豫,這樣也可以保證多個(gè)線程并發(fā)取錢的線程安全耻瑟。

3、釋放同步監(jiān)視器的鎖定

程序無法顯式釋放對同步監(jiān)視器的鎖定赏酥,線程會(huì)在如下情況下釋放對同步監(jiān)視器的鎖定喳整。

  • 當(dāng)前線程的同步方法、同步代碼塊執(zhí)行結(jié)束裸扶,當(dāng)前線程即釋放同步監(jiān)視器框都。

  • 當(dāng)前線程在同步代碼塊、同步方法中遇到break呵晨、return終止了該代碼塊魏保、該方法的繼續(xù)執(zhí)行,當(dāng)前線程將會(huì)釋放同步監(jiān)視器摸屠。

  • 當(dāng)前線程在同步代碼塊谓罗、同步方法中出現(xiàn)了未處理的Error 或Exception,導(dǎo)致了該代碼塊季二、該方法異常結(jié)束時(shí)檩咱,當(dāng)前線程將會(huì)釋放同步監(jiān)視器。

  • 當(dāng)前線程執(zhí)行同步代碼塊或同步方法時(shí)胯舷,程序執(zhí)行了同步監(jiān)視器對象的wait0方法刻蚯,則當(dāng)前線程暫停,并釋放同步監(jiān)視器桑嘶。

??
在如下所示的情況下炊汹,線程不會(huì)釋放同步監(jiān)視器:

  • 線程執(zhí)行同步代碼塊或同步方法時(shí),程序調(diào)用Thread.sleep()不翩、Thread.yield()方法來暫停當(dāng)前線程的執(zhí)行兵扬,當(dāng)前線程不會(huì)釋放同步監(jiān)視器麻裳。

  • 線程執(zhí)行同步代碼塊時(shí),其他線程調(diào)用了該線程的suspend()方法將該線程掛起器钟,該線程不會(huì)釋放同步監(jiān)視器津坑。當(dāng)然,程序應(yīng)該盡量避免使用suspend()resume()方法來控制線程傲霸。

4疆瑰、同步鎖(Lock)

Lock、ReadWriteLock是Java5提供的兩個(gè)根接口昙啄,并為Lock提供ReentrantLock(可重入鎖)實(shí)現(xiàn)類穆役,為ReadWriteLock提供了ReentrantReadWriteLock 實(shí)現(xiàn)類。

Java8新增了新型的StampedLock類梳凛,在大多數(shù)場景中它可以替代傳統(tǒng)的ReentrantReadWriteLock耿币。

ReentrantReadWriteLock為讀寫操作提供了三種鎖模式:Writing、ReadingOptimistic韧拒、Reading淹接。

在實(shí)現(xiàn)線程安全的控制中,比較常用的是ReentrantLock(可重入鎖)叛溢。使用該Lock對象可以顯式地加鎖塑悼、釋放鎖,通常使用ReentrantLock的代碼格式如下:

public class Account
{
    // 定義鎖對象
    private final ReentrantLock lock = new ReentrantLock();
    // 封裝賬戶編號楷掉、賬戶余額的兩個(gè)成員變量
    private String accountNo;
    private double balance;
    public Account(){}
    // 構(gòu)造器
    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
        return this.accountNo;
    }
    // 因此賬戶余額不允許隨便修改厢蒜,所以只為balance提供getter方法,
    public double getBalance()
    {
        return this.balance;
    }

    // 提供一個(gè)線程安全draw()方法來完成取錢操作
    public void draw(double drawAmount)
    {
        // 加鎖
        lock.lock();
        try
        {
            // 賬戶余額大于取錢數(shù)目
            if (balance >= drawAmount)
            {
                // 吐出鈔票
                System.out.println(Thread.currentThread().getName()
                    + "取錢成功烹植!吐出鈔票:" + drawAmount);
                try
                {
                    Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                // 修改余額
                balance -= drawAmount;
                System.out.println("\t余額為: " + balance);
            }
            else
            {
                System.out.println(Thread.currentThread().getName()
                    + "取錢失敯哐弧!余額不足刊橘!");
            }
        }
        finally
        {
            // 修改完成鄙才,釋放鎖
            lock.unlock();
        }
    }

    // 下面兩個(gè)方法根據(jù)accountNo來重寫hashCode()和equals()方法
    public int hashCode()
    {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if (obj !=null
            && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

??
同步方法或同步代碼塊使用與競爭資源相關(guān)的颂鸿、隱式的同步監(jiān)視器促绵,并且強(qiáng)制要求加鎖和釋放鎖要出現(xiàn)在一個(gè)塊結(jié)構(gòu)中,而且當(dāng)獲取了多個(gè)鎖時(shí)嘴纺,它們必須以相反的順序釋放败晴,且必須在與所有鎖被獲取時(shí)相同的范圍內(nèi)釋放所有鎖。

Lock提供了同步方法和同步代碼塊所沒有的其他功能栽渴,包括用于非塊結(jié)構(gòu)的tryLock()方法尖坤,以及試圖獲取可中斷鎖的lockInterruptibly()方法,還有獲取超時(shí)失效鎖的tryLock(long闲擦,TimeUnit)方法慢味。

ReentrantLock鎖具有可重入性场梆,也就是說,一個(gè)線程可以對已被加鎖的ReentrantLock鎖再次加鎖纯路,ReentrantLock對象會(huì)維持一個(gè)計(jì)數(shù)器來追蹤lock()方法的嵌套調(diào)用或油,線程在每次調(diào)用lock()加鎖后,必須顯式調(diào)用unlock()來釋放鎖驰唬,所以一段被鎖保護(hù)的代碼可以調(diào)用另一個(gè)被相同鎖保護(hù)的方法顶岸。

死鎖

當(dāng)兩個(gè)線程相互等待對方釋放同步監(jiān)視器時(shí)就會(huì)發(fā)生死鎖一旦出現(xiàn)死鎖,整個(gè)程序既不會(huì)發(fā)生任何異常叫编,也不會(huì)給出任何提示辖佣,只是所有線程處于阻塞狀態(tài),無法繼續(xù)搓逾。

死鎖示例:

有兩個(gè)類 A 和 B 卷谈,這兩個(gè)類每個(gè)類都各含有兩個(gè)同步方法,利用兩個(gè)線程來進(jìn)行操作霞篡。

首先線程1調(diào)用 A 類的同步方法 A1雏搂,然后休眠,此時(shí)線程2會(huì)開始工作寇损,它會(huì)調(diào)用 B 類的同步方法 B1凸郑,然后也休眠。

此時(shí)線程1休眠結(jié)束矛市,它繼續(xù)執(zhí)行方法 A1 芙沥,A1的下一步操作是調(diào)用 B 中的同步方法 B2,因?yàn)榇藭r(shí) B 的對象示例正被線程2所占據(jù)浊吏,因此線程1只能等待對 B 的鎖的釋放而昨。

此時(shí)線程2又蘇醒了,它繼續(xù)執(zhí)行方法 B1找田,B1的下一步操作是調(diào)用 A 中的同步方法 A2歌憨,因此是 A 類的對象也被線程1給鎖住了,因此線程2也只能等待墩衙,這樣就造成了線程1和線程2相互等待务嫡,從而導(dǎo)致了死鎖的發(fā)生。

代碼示例:

//A類
class A
{
    public synchronized void foo( B b )
    {
        System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
            + " 進(jìn)入了A實(shí)例的foo()方法" );     // ①
        try
        {
            Thread.sleep(200);
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
            + " 企圖調(diào)用B實(shí)例的last()方法");    // ③
        b.last();
    }
    public synchronized void last()
    {
        System.out.println("進(jìn)入了A類的last()方法內(nèi)部");
    }
}

//B類
class B
{
    public synchronized void bar( A a )
    {
        System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
            + " 進(jìn)入了B實(shí)例的bar()方法" );   // ②
        try
        {
            Thread.sleep(200);
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
            + " 企圖調(diào)用A實(shí)例的last()方法");  // ④
        a.last();
    }
    public synchronized void last()
    {
        System.out.println("進(jìn)入了B類的last()方法內(nèi)部");
    }
}

//線程類
public class DeadLock implements Runnable
{
    A a = new A();
    B b = new B();
    public void init()
    {
        Thread.currentThread().setName("主線程");
        // 調(diào)用a對象的foo方法
        a.foo(b);
        System.out.println("進(jìn)入了主線程之后");
    }
    public void run()
    {
        Thread.currentThread().setName("副線程");
        // 調(diào)用b對象的bar方法
        b.bar(a);
        System.out.println("進(jìn)入了副線程之后");
    }
    
    //主函數(shù)
    public static void main(String[] args)
    {
        DeadLock dl = new DeadLock();
        // 以dl為target啟動(dòng)新線程
        new Thread(dl).start();
        // 調(diào)用init()方法
        dl.init();
    }
}

??

線程通信


1漆改、傳統(tǒng)的線程通信——通過Object類提供的方法實(shí)現(xiàn)

借助于Object類提供的wait()心铃、notify()notifyAll()三個(gè)方法。

這三個(gè)方法并不屬于Thread類挫剑,而是屬于Object類去扣。但這三個(gè)方法必須由同步監(jiān)視器對象來調(diào)用,這可分成以下兩種情況樊破。

  • 對于使用synchronized修飾的同步方法愉棱,因?yàn)樵擃惖哪J(rèn)實(shí)例(this)就是同步監(jiān)視器唆铐,所以可以在同步方法中直接調(diào)用這三個(gè)方法。

  • 對于使用synchronized修飾的同步代碼塊奔滑,同步監(jiān)視器是synchronized后括號里的對象或链,所以必須使用該對象調(diào)用這三個(gè)方法。

關(guān)于這三個(gè)方法的解釋如下:

  • wait():導(dǎo)致當(dāng)前線程等待档押,直到其他線程調(diào)用該同步監(jiān)視器的notify()方法或notifyAll()方法來喚醒該線程澳盐。

  • notify():喚醒在此同步監(jiān)視器上等待的單個(gè)線程。如果所有線程都在此同步監(jiān)視器上等待令宿,則會(huì)選擇喚醒其中一個(gè)線程叼耙。選擇是任意性的。只有當(dāng)前線程放棄對該同步監(jiān)視器的鎖定后(使用wait()方法)粒没,才可以執(zhí)行被喚醒的線程筛婉。

  • notifyAll:喚醒在此同步監(jiān)視器上等待的所有線程。只有當(dāng)前線程放棄對該同步監(jiān)視器的鎖定后癞松,才可以執(zhí)行被喚醒的線程爽撒。

使用Condition控制線程通信

如果程序不使用synchronized 關(guān)鍵字來保證同步,而是直接便用Lock對象采保證同步响蓉,則系統(tǒng)中下存在隱式的同步監(jiān)視器硕勿,也就不能使用wait()notify()枫甲、notifyAll()方法進(jìn)行線程通信了源武。

當(dāng)使用Lock 對象來保證同步時(shí),Java提供了一個(gè)Condition類來保持協(xié)調(diào)想幻,使用Condition可以讓那些已經(jīng)得到Lock對象卻無法繼續(xù)執(zhí)行的線程釋放Lock對象粱栖,Condition對象也可以喚醒其他處于等待的線程。

Condition實(shí)例被綁定在一個(gè)Lock對象上脏毯。要獲得特定Lock實(shí)例的Condition實(shí)例闹究,調(diào)用Lock對象的newCondition()方法即可。Condition類提供了如下三個(gè)方法:

  • await():類似于隱式同步監(jiān)視器上的wait()方法食店,導(dǎo)致當(dāng)前線程等待渣淤,直到其他線程調(diào)用該Conditionsignal()方法或signalAll()方法來喚醒該線程。

  • signal():喚醒在此Lock對象上等待的單個(gè)線程叛买。如果所有線程都在該Lock對象上等待砂代,則會(huì)選擇喚醒其中一個(gè)線程。選擇是任意性的率挣。只有當(dāng)前線程放棄對該Lock對象的鎖定后(使用await()方法),才可以執(zhí)行被喚醒的線程露戒。

  • signalAIl():喚醒在此Lock對象上等待的所有線程椒功。只有當(dāng)前線程放棄對該Lock對象的鎖定后捶箱,才可以執(zhí)行被喚醒的線程。

public class Account
{
    // 顯式定義Lock對象
    private final Lock lock = new ReentrantLock();
    // 獲得指定Lock對象對應(yīng)的Condition
    private final Condition cond  = lock.newCondition();
    // 封裝賬戶編號动漾、賬戶余額的兩個(gè)成員變量
    private String accountNo;
    private double balance;
    // 標(biāo)識(shí)賬戶中是否已有存款的旗標(biāo)
    private boolean flag = false;

    public Account(){}
    // 構(gòu)造器
    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
        return this.accountNo;
    }
    // 因此賬戶余額不允許隨便修改丁屎,所以只為balance提供getter方法,
    public double getBalance()
    {
        return this.balance;
    }

    public void draw(double drawAmount)
    {
        // 加鎖
        lock.lock();
        try
        {
            // 如果flag為假旱眯,表明賬戶中還沒有人存錢進(jìn)去晨川,取錢方法阻塞
            if (!flag)
            {
                cond.await();
            }
            else
            {
                // 執(zhí)行取錢
                System.out.println(Thread.currentThread().getName()
                    + " 取錢:" +  drawAmount);
                balance -= drawAmount;
                System.out.println("賬戶余額為:" + balance);
                // 將標(biāo)識(shí)賬戶是否已有存款的旗標(biāo)設(shè)為false。
                flag = false;
                // 喚醒其他線程
                cond.signalAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        // 使用finally塊來釋放鎖
        finally
        {
            lock.unlock();
        }
    }
    public void deposit(double depositAmount)
    {
        lock.lock();
        try
        {
            // 如果flag為真删豺,表明賬戶中已有人存錢進(jìn)去共虑,則存錢方法阻塞
            if (flag)             // ①
            {
                cond.await();
            }
            else
            {
                // 執(zhí)行存款
                System.out.println(Thread.currentThread().getName()
                    + " 存款:" +  depositAmount);
                balance += depositAmount;
                System.out.println("賬戶余額為:" + balance);
                // 將表示賬戶是否已有存款的旗標(biāo)設(shè)為true
                flag = true;
                // 喚醒其他線程
                cond.signalAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        // 使用finally塊來釋放鎖
        finally
        {
            lock.unlock();
        }
    }

    // 下面兩個(gè)方法根據(jù)accountNo來重寫hashCode()和equals()方法
    public int hashCode()
    {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if (obj !=null
            && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

??

使用阻塞隊(duì)列(BlockingQueue)控制線程通信

Java5提供了一個(gè)BlockingQueue接口,雖然BlockingQueue也是Queue的子接口呀页,但它的主要用途并不是作為容器妈拌,而是作為線程同步的工具。

BlockingQueue具有一個(gè)特征:

當(dāng)生產(chǎn)者線程試圖向BlockingOueue中放入元素時(shí)蓬蝶,如果該隊(duì)列已滿尘分,則該線程被阻塞;

當(dāng)消費(fèi)者線程試圖從BlockingQueue中取出元素時(shí)丸氛,如果該隊(duì)列已空培愁,則該線程被阻塞。

BlockingQueue提供如下兩個(gè)支持阻塞的方法缓窜。

  • put(E e):嘗試把E元素放入BlockingQueue中竭钝,如果該隊(duì)列的元素已滿,則阻塞該線程雹洗。

  • take():嘗試從BlockingQueue的頭部取出元素香罐,如果該隊(duì)列的元素已空,則阻塞該線程时肿。

BlockingQueue繼承了Queue接口庇茫,當(dāng)然也可使用Queue接口中的方法。這些方法歸納起來可分為如下三組螃成。

  • 在隊(duì)列尾部插入元素旦签。包括add(E e)offer(E e)put(Ee)方法寸宏,當(dāng)該隊(duì)列已滿時(shí)宁炫,這三個(gè)方法分別會(huì)拋出異常、返回false氮凝、阻塞隊(duì)列羔巢。

  • 在隊(duì)列頭部刪除并返回刪除的元素。包括remove()poll()take()方法竿秆。當(dāng)該隊(duì)列已空時(shí)启摄,這三個(gè)方法分別會(huì)拋出異常、返回false幽钢、阻塞隊(duì)列歉备。

  • 在隊(duì)列頭部取出但不刪除元素。包括element()peek()方法匪燕,當(dāng)隊(duì)列已空時(shí)蕾羊,這兩個(gè)方法分別拋出異常、返回false帽驯。

使用阻塞隊(duì)列(BlockingQueue)來實(shí)現(xiàn)線程通信龟再,以消費(fèi)者生產(chǎn)者為例:

//生產(chǎn)者類
class Producer extends Thread
{
    private BlockingQueue<String> bq;
    public Producer(BlockingQueue<String> bq)
    {
        this.bq = bq;
    }
    public void run()
    {
        String[] strArr = new String[]
        {
            "Java",
            "Struts",
            "Spring"
        };
        for (int i = 0 ; i < 999999999 ; i++ )
        {
            System.out.println(getName() + "生產(chǎn)者準(zhǔn)備生產(chǎn)集合元素!");
            try
            {
                Thread.sleep(200);
                // 嘗試放入元素界拦,如果隊(duì)列已滿吸申,線程被阻塞
                bq.put(strArr[i % 3]);
            }
            catch (Exception ex){ex.printStackTrace();}
            System.out.println(getName() + "生產(chǎn)完成:" + bq);
        }
    }
}

//消費(fèi)者類
class Consumer extends Thread
{
    private BlockingQueue<String> bq;
    public Consumer(BlockingQueue<String> bq)
    {
        this.bq = bq;
    }
    public void run()
    {
        while(true)
        {
            System.out.println(getName() + "消費(fèi)者準(zhǔn)備消費(fèi)集合元素!");
            try
            {
                Thread.sleep(200);
                // 嘗試取出元素享甸,如果隊(duì)列已空截碴,線程被阻塞
                bq.take();
            }
            catch (Exception ex){ex.printStackTrace();}
            System.out.println(getName() + "消費(fèi)完成:" + bq);
        }
    }
}

//主程序
public class BlockingQueueTest2
{
    public static void main(String[] args)
    {
        // 創(chuàng)建一個(gè)容量為1的BlockingQueue
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
        // 啟動(dòng)3條生產(chǎn)者線程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        // 啟動(dòng)一條消費(fèi)者線程
        new Consumer(bq).start();
    }
}

??

線程池


系統(tǒng)啟動(dòng)一個(gè)新線程的成本是比較高的,因?yàn)樗婕芭c操作系統(tǒng)交互蛉威。在這種情形下斋否,使用線程池可以很好地提高性能浙宜,尤其是當(dāng)程序中需要?jiǎng)?chuàng)建大量生存期很短暫的線程時(shí),更應(yīng)該考慮使用線程池。

與數(shù)據(jù)庫連接池類似的是是目,線程池在系統(tǒng)啟動(dòng)時(shí)即創(chuàng)建大量空閑的線程谚攒,程序?qū)⒁粋€(gè)Runnable對象或Callable對象傳給線程池令境,線程池就會(huì)啟動(dòng)一個(gè)線程來執(zhí)行它們的run()call()方法挣菲。

當(dāng)run()call()方法執(zhí)行結(jié)束后,該線程并不會(huì)死亡栅盲,而是再次返回線程池中成為空閑狀態(tài)汪诉,等待執(zhí)行下一個(gè)Runnable對象的run()call()方法。

除此之外谈秫,使用線程池可以有效地控制系統(tǒng)中并發(fā)線程的數(shù)量扒寄,當(dāng)系統(tǒng)中包含大量并發(fā)線程時(shí),會(huì)導(dǎo)致系統(tǒng)性能劇烈下降拟烫,甚至導(dǎo)致JVM崩潰该编,而線程池的最大線程數(shù)參數(shù)可以控制系統(tǒng)中并發(fā)線程數(shù)不超過此數(shù)。

創(chuàng)建線程池

在Java5以前硕淑,開發(fā)者必須手動(dòng)實(shí)現(xiàn)自己的線程池课竣;從Java5開始嘉赎,Java內(nèi)建支持線程池。

Java5新增了一個(gè)Executors工廠類來產(chǎn)生線程池稠氮,該工廠類包含如下幾個(gè)靜態(tài)工廠方法來創(chuàng)建線程池曹阔。

  • newCachedThreadPool():創(chuàng)建一個(gè)具有緩存功能的線程池半开,系統(tǒng)根據(jù)需要?jiǎng)?chuàng)建線程隔披,這些線程將會(huì)被緩存在線程池中。

  • newFixedThreadPool(int nThreads):創(chuàng)建一個(gè)可重用的寂拆、具有固定線程數(shù)的線程池奢米。

  • newSingle ThreadExecutor():創(chuàng)建一個(gè)只有單線程的線程池,它相當(dāng)于調(diào)用newFixedThread Pool()方法時(shí)傳入?yún)?shù)為1纠永。

  • newScheduledThreadPool(int corePoolSize):創(chuàng)建具有指定線程數(shù)的線程池鬓长,它可以在指定延遲后執(zhí)行線程任務(wù)。corePoolSize指池中所保存的線程數(shù)尝江,即使線程是空閑的也被保存在線程池內(nèi)涉波。

  • newSingle ThreadScheduledExecutor):創(chuàng)建只有一個(gè)線程的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)炭序。

  • ExecutorService new WorkStealingPool(int parallelism):創(chuàng)建持有足夠的線程的線程池來支持給定的并行級別啤覆,該方法還會(huì)使用多個(gè)隊(duì)列來減少競爭。

  • ExecutorService new WorkStealingPool):該方法是前一個(gè)方法的簡化版本惭聂。如果當(dāng)前機(jī)器有4個(gè)CPU窗声,則目標(biāo)并行級別被設(shè)置為4,也就是相當(dāng)于為前一個(gè)方法傳入4作為參數(shù)辜纲。

上面7個(gè)方法中的前三個(gè)方法返回一個(gè)ExecutorService對象笨觅,該對象代表一個(gè)線程池,它可以執(zhí)行Runnable對象或Callable對象所代表的線程耕腾;

而中間兩個(gè)方法返回一個(gè)ScheduledExecutorService線程池见剩,它是ExecutorService的子類,它可以在指定延遲后執(zhí)行線程任務(wù)扫俺;

最后兩個(gè)方法則是Java8新增的苍苞,這兩個(gè)方法可充分利用多CPU并行的能力。這兩個(gè)方法生成的work stealing池牵舵,都相當(dāng)于后臺(tái)線程池柒啤,如果所有的前臺(tái)線程都死亡了,work stealing池中的線程會(huì)自動(dòng)死亡畸颅。

ExecutorService代表盡快執(zhí)行線程的線程池(只要線程池中有空閑線程担巩,就立即執(zhí)行線程任務(wù))

程序只要將一個(gè)Runnable對象或Callable對象(代表線程任務(wù))提交給該線程池,該線程池就會(huì)盡快執(zhí)行該任務(wù)没炒。

ExecutorService里提供了如下三個(gè)方法涛癌。

  • Future<?>submit(Runnable task):將一個(gè)Runnable對象提交給指定的線程池,線程池將在有空閑線程時(shí)執(zhí)行Runnable對象代表的任務(wù)。其中Future對象代表Runnable任務(wù)的返回值拳话,但run()方法沒有返回值先匪,所以Future對象將在run()方法執(zhí)行結(jié)束后返回null

但可以調(diào)用FutureisDone()弃衍、isCancelled()方法來獲得Runnable對象的執(zhí)行狀態(tài)呀非。

  • <T>Future-T>submit(Runnable task,T result):將一個(gè)Runnable對象提交給指定的線程池,線程池將在有空閑線程時(shí)執(zhí)行Runnable對象代表的任務(wù)镜盯。其中result顯式指定線程執(zhí)行結(jié)束后的返回值岸裙,所以Future對象將在run()方法執(zhí)行結(jié)束后返回result

  • <T>Future-T>submit(Callable<T>task):將一個(gè)Callable對象提交給指定的線程池速缆,線程池將在有空閑線程時(shí)執(zhí)行Callable對象代表的任務(wù)降允。其中Future代表Callable對象里call()方法的返回值。

ScheduledExecutorService代表可在指定延遲后或周期性地執(zhí)行線程任務(wù)的線程池艺糜,它提供了如下4個(gè)方法剧董。

  • ScheduledFuture<V> schedule(Callable-V> callable,long delay,TimeUnit unit):指定callable任務(wù)將在delay延遲后執(zhí)行。

  • ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit):指定command任務(wù)將在delay延遲后執(zhí)行破停。

  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit):指定command任務(wù)將在delay延遲后執(zhí)行翅楼,而且以設(shè)定頻率重復(fù)執(zhí)行。也就是說辱挥,在initialDelay后開始執(zhí)行犁嗅,依次在initialDelay+period、initialDelay+2*period…處重復(fù)執(zhí)行晤碘,依此類推褂微。

  • ScheduledFuture<?>scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):創(chuàng)建并執(zhí)行一個(gè)在給定初始延遲后首次啟用的定期操作,隨后在每一次執(zhí)行終止和下一次執(zhí)行開始之間都存在給定的延遲园爷。如果任務(wù)在任一次執(zhí)行時(shí)遇到異常宠蚂,就會(huì)取消后續(xù)執(zhí)行;否則童社,只能通過程序來顯式取消或終止該任務(wù)求厕。

用完一個(gè)線程池后,應(yīng)該調(diào)用該線程池的shutdown0方法扰楼,該方法將啟動(dòng)線程池的關(guān)閉序列呀癣,調(diào)用shutdown()方法后的線程池不再接收新任務(wù),但會(huì)將以前所有已提交任務(wù)執(zhí)行完成弦赖。當(dāng)線程池中的所有任務(wù)都執(zhí)行完成后项栏,池中的所有線程都會(huì)死亡;

另外也可以調(diào)用線程池的shutdownNow()方法來關(guān)閉線程池蹬竖,該方法試圖停止所有正在執(zhí)行的活動(dòng)任務(wù)沼沈,暫停處理正在等待的任務(wù)流酬,并返回等待執(zhí)行的任務(wù)列
表。

使用線程池來執(zhí)行線程任務(wù)的步驟如下列另。

①調(diào)用Executors類的靜態(tài)工廠方法創(chuàng)建一個(gè)ExecutorService對象芽腾,該對象代表一個(gè)線程池。

②創(chuàng)建Runnable 實(shí)現(xiàn)類或Callable實(shí)現(xiàn)類的實(shí)例页衙,作為線程執(zhí)行任務(wù)摊滔。

③調(diào)用ExecutorService對象的submit()方法來提交Runnable實(shí)例或Callable實(shí)例。

④當(dāng)不想提交任何任務(wù)時(shí)拷姿,調(diào)用ExecutorService對象的shutdown()方法來關(guān)閉線程池惭载。

代碼示例:

public class ThreadPoolTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 創(chuàng)建足夠的線程來支持4個(gè)CPU并行的線程池
        // 創(chuàng)建一個(gè)具有固定線程數(shù)(6)的線程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        // 使用Lambda表達(dá)式創(chuàng)建Runnable對象
        Runnable target = () -> {
            for (int i = 0; i < 100 ; i++ )
            {
                System.out.println(Thread.currentThread().getName() + "的i值為:" + i);
            }
        };
        // 向線程池中提交兩個(gè)線程
        pool.submit(target);
        pool.submit(target);
        // 關(guān)閉線程池
        pool.shutdown();
    }
}

??

Java8增強(qiáng)的ForkJoinPool

Java7提供了ForkJoinPool來支持將一個(gè)任務(wù)拆分成多個(gè)“小任務(wù)”并行計(jì)算旱函,再把多個(gè)“小任務(wù)”的結(jié)果合并成總的計(jì)算結(jié)果响巢。ForkJoinPoolExecutorService的實(shí)現(xiàn)類,因此是一種特殊的線程池棒妨。

ForkJoinPool提供了如下兩個(gè)常用的構(gòu)造器踪古。

  • ForkJoinPool(int parallelism):創(chuàng)建一個(gè)包含parallelism個(gè)并行線程的ForkJoinPool

  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作為parallelism參數(shù)來創(chuàng)建ForkJoinPool券腔。

Java8進(jìn)一步擴(kuò)展了ForkJoinPool的功能伏穆,Java8為ForkJoinPool增加了通用池功能。

ForkJoinPool通過如下兩個(gè)靜態(tài)方法提供通用池功能纷纫。

  • ForkJoinPool commonPool():該方法返回一個(gè)通用池枕扫。

通用池的運(yùn)行狀態(tài)不會(huì)受shutdown()shutdownNow()方法的影響。當(dāng)然辱魁,如果程序直接執(zhí)行System.exit(0)烟瞧;來終止虛擬機(jī),通用池以及通用池中正在執(zhí)行的任務(wù)都會(huì)被自動(dòng)終止染簇。

  • int getCommonPoolParallelism():該方法返回通用池的并行級別参滴。

創(chuàng)建了ForkJoinPool實(shí)例之后,就可調(diào)用ForkJoinPoolsubmit(ForkJoin Task task)invoke(ForkJoinTask task)方法來執(zhí)行指定任務(wù)了锻弓。

其中ForkJoinTask代表一個(gè)可以并行砾赔、合并的任務(wù)。

ForkJoinTask是一個(gè)抽象類青灼,它還有兩個(gè)抽象子類:RecursiveActionRecursive Task暴心。

其中Recursive Task代表有返回值的任務(wù),而RecursiveAction代表沒有返回值的任務(wù)杂拨。

下面以執(zhí)行沒有返回值的“大任務(wù)”(簡單地打印0-300的數(shù)值)為例专普,程序?qū)⒁粋€(gè)“大任務(wù)”拆分成多個(gè)“小任務(wù)”,并將任務(wù)交給ForkJoinPool來執(zhí)行扳躬。

// 繼承RecursiveAction來實(shí)現(xiàn)"可分解"的任務(wù)
class PrintTask extends RecursiveAction
{
    // 每個(gè)“小任務(wù)”只最多只打印50個(gè)數(shù)
    private static final int THRESHOLD = 50;
    private int start;
    private int end;
    // 打印從start到end的任務(wù)
    public PrintTask(int start, int end)
    {
        this.start = start;
        this.end = end;
    }
    @Override
    protected void compute()
    {
        // 當(dāng)end與start之間的差小于THRESHOLD時(shí)脆诉,開始打印
        if(end - start < THRESHOLD)
        {
            for (int i = start ; i < end ; i++ )
            {
                System.out.println(Thread.currentThread().getName() + "的i值:" + i);
            }
        }
        else
        {
            // 如果當(dāng)end與start之間的差大于THRESHOLD時(shí)甚亭,即要打印的數(shù)超過50個(gè)
            // 將大任務(wù)分解成兩個(gè)小任務(wù)。
            int middle = (start + end) / 2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            // 并行執(zhí)行兩個(gè)“小任務(wù)”
            left.fork();
            right.fork();
        }
    }
}

/**
 * description: 主函數(shù)
 **/
public class ForkJoinPoolTest
{
    public static void main(String[] args)
        throws Exception
    {
        ForkJoinPool pool = new ForkJoinPool();
        // 提交可分解的PrintTask任務(wù)
        pool.submit(new PrintTask(0 , 300));
        pool.awaitTermination(2, TimeUnit.SECONDS);
        // 關(guān)閉線程池
        pool.shutdown();
    }
}

上面定義的任務(wù)是一個(gè)沒有返回值的打印任務(wù)击胜,如果大任務(wù)是有返回值的任務(wù)亏狰,則可以讓任務(wù)繼承Recursive Task<T>,其中泛型參數(shù)T就代表了該任務(wù)的返回值類型偶摔。下面程序示范了使用Recursive Task對一個(gè)長度為100的數(shù)組的元素值進(jìn)行累加暇唾。

// 繼承RecursiveTask來實(shí)現(xiàn)"可分解"的任務(wù)
class CalTask extends RecursiveTask<Integer>
{
    // 每個(gè)“小任務(wù)”只最多只累加20個(gè)數(shù)
    private static final int THRESHOLD = 20;
    private int arr[];
    private int start;
    private int end;
    // 累加從start到end的數(shù)組元素
    public CalTask(int[] arr , int start, int end)
    {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }
    @Override
    protected Integer compute()
    {
        int sum = 0;
        // 當(dāng)end與start之間的差小于THRESHOLD時(shí),開始進(jìn)行實(shí)際累加
        if(end - start < THRESHOLD)
        {
            for (int i = start ; i < end ; i++ )
            {
                sum += arr[i];
            }
            return sum;
        }
        else
        {
            // 如果當(dāng)end與start之間的差大于THRESHOLD時(shí)辰斋,即要累加的數(shù)超過20個(gè)時(shí)
            // 將大任務(wù)分解成兩個(gè)小任務(wù)策州。
            int middle = (start + end) / 2;
            CalTask left = new CalTask(arr , start, middle);
            CalTask right = new CalTask(arr , middle, end);
            // 并行執(zhí)行兩個(gè)“小任務(wù)”
            left.fork();
            right.fork();
            // 把兩個(gè)“小任務(wù)”累加的結(jié)果合并起來
            return left.join() + right.join();    // ①
        }
    }
}


/**
 * description: 主函數(shù)
 **/
public class Sum
{
    public static void main(String[] args)
        throws Exception
    {
        int[] arr = new int[100];
        Random rand = new Random();
        int total = 0;
        // 初始化100個(gè)數(shù)字元素
        for (int i = 0 , len = arr.length; i < len ; i++ )
        {
            int tmp = rand.nextInt(20);
            // 對數(shù)組元素賦值,并將數(shù)組元素的值添加到sum總和中宫仗。
            total += (arr[i] = tmp);
        }
        System.out.println(total);
        // 創(chuàng)建一個(gè)通用池
        ForkJoinPool pool = ForkJoinPool.commonPool();
        // 提交可分解的CalTask任務(wù)
        Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
        System.out.println(future.get());
        // 關(guān)閉線程池
        pool.shutdown();
    }
}

??

線程相關(guān)的類


ThreadLocal

ThreadLocal,是Thread Local Variable(線程局部變量)的意思够挂,它就是為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本,使每一個(gè)線程都可以獨(dú)立地改變自己的副本藕夫,而不會(huì)和其他線程的副本沖突孽糖。從線程的角度看,就好像每一個(gè)線程都完全擁有該變量一樣毅贮。

它只提供了如下三個(gè)public方法办悟。

  • T get():返回此線程局部變量中當(dāng)前線程副本中的值。

  • void remove():刪除此線程局部變量中當(dāng)前線程的值滩褥。

  • void set(T value):設(shè)置此線程局部變量中當(dāng)前線程副本中的值病蛉。

代碼示例:


/**
 * description: 賬戶類
 **/
class Account
{
    /* 定義一個(gè)ThreadLocal類型的變量,該變量將是一個(gè)線程局部變量
    每個(gè)線程都會(huì)保留該變量的一個(gè)副本 */
    private ThreadLocal<String> name = new ThreadLocal<>();
    // 定義一個(gè)初始化name成員變量的構(gòu)造器
    public Account(String str)
    {
        this.name.set(str);
        // 下面代碼用于訪問當(dāng)前線程的name副本的值
        System.out.println("---" + this.name.get());
    }
    // name的setter和getter方法
    public String getName()
    {
        return name.get();
    }
    public void setName(String str)
    {
        this.name.set(str);
    }
}


/**
 * description: 線程類
 **/
class MyTest extends Thread
{
    // 定義一個(gè)Account類型的成員變量
    private Account account;
    public MyTest(Account account, String name)
    {
        super(name);
        this.account = account;
    }
    public void run()
    {
        // 循環(huán)10次
        for (int i = 0 ; i < 10 ; i++)
        {
            // 當(dāng)i == 6時(shí)輸出將賬戶名替換成當(dāng)前線程名
            if (i == 6)
            {
                account.setName(getName());
            }
            // 輸出同一個(gè)賬戶的賬戶名和循環(huán)變量
            System.out.println(account.getName() + " 賬戶的i值:" + i);
        }
    }
}



/**
 * description: 主程序
 **/
public class ThreadLocalTest
{
    public static void main(String[] args)
    {
        // 啟動(dòng)兩條線程瑰煎,兩條線程共享同一個(gè)Account
        Account at = new Account("初始名");
        /*
        雖然兩條線程共享同一個(gè)賬戶铺然,即只有一個(gè)賬戶名
        但由于賬戶名是ThreadLocal類型的,所以每條線程
        都完全擁有各自的賬戶名副本丢间,所以從i == 6之后探熔,將看到兩條
        線程訪問同一個(gè)賬戶時(shí)看到不同的賬戶名。
        */
        new MyTest(at , "線程甲").start();
        new MyTest(at , "線程乙").start ();
    }
}

??
程序結(jié)果如圖:


線程局部變量互不干擾的情形

分析:

上面Account類中的三行粗體字代碼分別完成了創(chuàng)建ThreadLocal對象烘挫、從ThreadLocal中取出線程局部變量诀艰、修改線程局部變量的操作。

由于程序中的賬戶名是一個(gè)ThreadLocal變量饮六,所以雖然程序中只有一個(gè)Account對象其垄,但兩個(gè)子線程將會(huì)產(chǎn)生兩個(gè)賬戶名(主線程也持有一個(gè)賬戶名的副本)。

兩個(gè)線程進(jìn)行循環(huán)時(shí)都會(huì)在i=6時(shí)將賬戶名改為與線程名相同卤橄,這樣就可以看到兩個(gè)線程擁有兩個(gè)賬戶名的情形绿满,如圖所示。

從上面程序可以看出窟扑,實(shí)際上賬戶名有三個(gè)副本喇颁,主線程一個(gè)漏健,另外啟動(dòng)的兩個(gè)線程各一個(gè),它們的值互不干擾橘霎,每個(gè)線程完全擁有自己的ThreadLocal變量蔫浆,這就是ThreadLocal的用途。

ThreadLocal和其他所有的同步機(jī)制一樣姐叁,都是為了解決多線程中對同一變量的訪問沖突瓦盛。

在普通的同步機(jī)制中,是通過對象加鎖來實(shí)現(xiàn)多個(gè)線程對同一變量的安全訪問的外潜。該變量是多個(gè)線程共享的原环,所以要使用這種同步機(jī)制,需要很細(xì)致地分析在什么時(shí)候?qū)ψ兞窟M(jìn)行讀寫处窥,什么時(shí)候需要鎖定某個(gè)對象嘱吗,什么時(shí)候釋放該對象的鎖等。在這種情況下碧库,系統(tǒng)并沒有將這份資源復(fù)制多份柜与,只是采用了安全機(jī)制來控制對這份資源的訪問而已。

ThreadLocal從另一個(gè)角度來解決多線程的并發(fā)訪問嵌灰,ThreadLocal將需要并發(fā)訪問的資源復(fù)制多份,每個(gè)線程擁有一份資源颅悉,每個(gè)線程都擁有自己的資源副本沽瞭,從而也就沒有必要對該變量進(jìn)行同步了。

ThreadLocal提供了線程安全的共享對象剩瓶,在編寫多線程代碼時(shí)驹溃,可以把不安全的整個(gè)變量封裝進(jìn)ThreadLocal,或者把該對象與線程相關(guān)的狀態(tài)使用ThreadLocal保存。

ThreadLocal并不能替代同步機(jī)制延曙,兩者面向的問題領(lǐng)域不同豌鹤。同步機(jī)制是為了同步多個(gè)線程對相同資源的并發(fā)訪問,是多個(gè)線程之間進(jìn)行通信的有效方式枝缔;

ThreadLocal是為了隔離多個(gè)線程的數(shù)據(jù)共享布疙,從根本上避免多個(gè)線程之間對共享資源(變量)的競爭,也就不需要對多個(gè)線程進(jìn)行同步了愿卸。

通常建議:
如果多個(gè)線程之間需要共享資源灵临,以達(dá)到線程之間的通信功能,就使用同步機(jī)制趴荸;如果僅僅需要隔離多個(gè)線程之間的共享沖突儒溉,則可以使用ThreadLocal

??

包裝線程不安全的集合

ArrayList发钝、LinkedList顿涣、HashSet波闹、TreeSetHashMap涛碑、TreeMap等都是線程不安全的舔痪,也就是說,當(dāng)多個(gè)并發(fā)線程向這些集合中存锌唾、取元素時(shí)锄码,就可能會(huì)破壞這些集合的數(shù)據(jù)完整性。

如果程序中有多個(gè)線程可能訪問以上這些集合晌涕,就可以使用Collections提供的類方法把這些集合包裝成線程安全的集合滋捶。Collections提供了如下幾個(gè)靜態(tài)方法。

  • <T>Collection<T>synchronizedCollection(Collection<T>c):返回指定collection對應(yīng)的線程安全的collection余黎。

  • static<T>List<T>synchronizedList(List<T>list):返回指定List對象對應(yīng)的線程安全的List對象重窟。

  • static<K,V>Map<K,V> synchronizedMap(Map<K,V>m):返回指定Map對象對應(yīng)的線程安全的Map對象。

  • static<T>Set<T>synchronizedSet(Set<T>s):返回指定Set對象對應(yīng)的線程安全的Set對象惧财。

  • static<K,V>SortedMap<K,V>synchronizedSortedMap(SortedMap<K,V>m):返回指定SortedMap對象對應(yīng)的線程安全的SortedMap對象巡扇。

  • static<T>SortedSet-T>synchronizedSortedSet(SortedSet<T>s):返回指定SortedSet對象對應(yīng)的線程安全的SortedSet對象。

例如需要在多線程中使用線程安全的HashMap對象垮衷,則可以采用如下代碼:

//使用Collections的synchronizedMap方法將一個(gè)普通的HashMap包裝成線程安全的類
HashMap m=Collections.synchronizedMap(new HashMap());

??
tips:
如果需要把某個(gè)集合包裝成線程安全的集合厅翔,則應(yīng)該在創(chuàng)建之后立即包裝,如上程序所示搀突,當(dāng)HashMap對象創(chuàng)建后立即被包裝成線程安全的HashMap對象刀闷。

線程安全的集合類

線程安全的集合類可分為如下兩類:

  • Concurrent開頭的集合類,如ConcurrentHashMap仰迁、ConcurrentSkipListMap甸昏、ConcurrentSkip ListSet
    ConcurrentLinkedQueueConcurrentLinkedDeque

  • CopyOnWrite開頭的集合類徐许,如CopyOnWriteArrayList施蜜、CopyOnWriteArraySet

其中以Concurrent開頭的集合類代表了支持并發(fā)訪問的集合,它們可以支持多個(gè)線程并發(fā)寫入訪問雌隅,這些寫入線程的所有操作都是線程安全的翻默,但讀取操作不必鎖定。

Concurrent開頭的集合類采用了更復(fù)雜的算法來保證永遠(yuǎn)不會(huì)鎖住整個(gè)集合澄步,因此在并發(fā)寫入時(shí)有較好的性能冰蘑。

在默認(rèn)情況下,ConcurrentHashMap支持16個(gè)線程并發(fā)寫入村缸,當(dāng)有超過16個(gè)線程并發(fā)向該Map中寫入數(shù)據(jù)時(shí)祠肥,可能有一些線程需要等待。實(shí)際上,程序通過設(shè)置concurrencyLevel構(gòu)造參數(shù)(默認(rèn)值為16)來支持更多的并發(fā)寫入線程仇箱。

與前面介紹的HashMap和普通集合不同的是县恕,因?yàn)?code>ConcurrentLinkedQueue和ConcurrentHashMap支持多線程并發(fā)訪問,所以當(dāng)使用迭代器來遍歷集合元素時(shí)剂桥,該迭代器可能不能反映出創(chuàng)建迭代器之后所做的修改忠烛,但程序不會(huì)拋出任何異常。

Java8擴(kuò)展了ConcurrentHashMap的功能权逗,Java8為該類新增了30多個(gè)新方法美尸,這些方法可借助于StreamLambda表達(dá)式支持執(zhí)行聚集操作。ConcurrentHashMap新增的方法大致可分為如下三類:

  • forEach系列 (forEach,forEachKey,forEach Value,forEachEntry)

  • search系列 (search,searchKeys,search Values,searchEntries)

  • reduce系列 (reduce,reduce ToDouble,reduce ToLong,reduceKeys,reduceValues)

除此之外斟薇,ConcurrentHashMap還新增了mappingCount()师坎、newKeySet()等方法,增強(qiáng)后的ConcurrentHashMap更適合作為緩存實(shí)現(xiàn)類使用堪滨。

??
CopyOnWriteAtraySet

由于CopyOnWriteAtraySet的底層封裝了CopyOnWriteArmayList胯陋,因此它的實(shí)現(xiàn)機(jī)制完全類似于CopyOnWriteArrayList集合。

對于CopyOnWriteArrayList集合袱箱,遏乔,它采用復(fù)制底層數(shù)組的方式來實(shí)現(xiàn)寫操作。

當(dāng)線程對CopyOnWriteArrayList集合執(zhí)行讀取操作時(shí)发笔,線程將會(huì)直接讀取集合本身盟萨,無須加鎖與阻塞。

當(dāng)線程對CopyOnWriteArrayList集合執(zhí)行寫入操作時(shí)(包括調(diào)用add()筐咧、remove()鸯旁、set()`等方法)該集合會(huì)在底層復(fù)制一份新的數(shù)組,接下來對新的數(shù)組執(zhí)行寫入操作量蕊。

由于對 CopyOnWriteArmayList集合的寫入操作都是對數(shù)組的副本執(zhí)行操作,因此它是線程安全的艇挨。

需要指出的是残炮,由于CopyOnWriteArrayList執(zhí)行寫入操作時(shí)需要頻繁地復(fù)制數(shù)組,性能比較差缩滨。

但由于讀操作與寫操作不是操作同一個(gè)數(shù)組势就,而且讀操作也不需要加鎖,因此讀操作就很快脉漏、很安全苞冯。由此可見,CopyOnWriteArayList適合用在讀取操作遠(yuǎn)遠(yuǎn)大于寫入操作的場景中侧巨,例如緩存等舅锄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市司忱,隨后出現(xiàn)的幾起案子皇忿,更是在濱河造成了極大的恐慌畴蹭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳍烁,死亡現(xiàn)場離奇詭異叨襟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)幔荒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門糊闽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人爹梁,你說我怎么就攤上這事右犹。” “怎么了卫键?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵傀履,是天一觀的道長。 經(jīng)常有香客問我莉炉,道長钓账,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任絮宁,我火速辦了婚禮梆暮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绍昂。我一直安慰自己啦粹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布窘游。 她就那樣靜靜地躺著唠椭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪忍饰。 梳的紋絲不亂的頭發(fā)上贪嫂,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音艾蓝,去河邊找鬼力崇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛赢织,可吹牛的內(nèi)容都是我干的亮靴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼于置,長吁一口氣:“原來是場噩夢啊……” “哼茧吊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤饱狂,失蹤者是張志新(化名)和其女友劉穎曹步,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體休讳,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡讲婚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俊柔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筹麸。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖雏婶,靈堂內(nèi)的尸體忽然破棺而出物赶,到底是詐尸還是另有隱情,我是刑警寧澤留晚,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布酵紫,位于F島的核電站,受9級特大地震影響错维,放射性物質(zhì)發(fā)生泄漏奖地。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一赋焕、第九天 我趴在偏房一處隱蔽的房頂上張望参歹。 院中可真熱鬧,春花似錦隆判、人聲如沸犬庇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臭挽。三九已至,卻和暖如春咬腕,著一層夾襖步出監(jiān)牢的瞬間埋哟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工郎汪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人闯狱。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓煞赢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哄孤。 傳聞我的和親對象是個(gè)殘疾皇子照筑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

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