并發(fā)編程基礎(chǔ)

??Java從誕生開始就明智地選擇了內(nèi)置對(duì)多線程的支持,這使得Java語言相比同一時(shí)期的其他語言具有顯示的優(yōu)勢(shì)。線程作為操作系統(tǒng)調(diào)度的最小單元,多個(gè)線程能夠同時(shí)執(zhí)行,這將顯著提升程序性能,在多核環(huán)境中表現(xiàn)得更加明顯。但是,過多地創(chuàng)建線程和對(duì)線程的不當(dāng)管理也容易造成問題。

4.1線程簡(jiǎn)介

4.1.1 什么是線程
??現(xiàn)代操作系統(tǒng)在運(yùn)行一個(gè)程序時(shí),會(huì)為其創(chuàng)建一個(gè)進(jìn)程。例如,啟動(dòng)一個(gè)Java程序,操作系統(tǒng)就會(huì)創(chuàng)建一個(gè)Java進(jìn)程?,F(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程,也叫輕量級(jí)進(jìn)程(Light Weight Process),在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,這些線程都擁有各自的計(jì)數(shù)器、堆棧和局部變量等屬性,并且能夠訪問共享的內(nèi)存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時(shí)執(zhí)行。
??一個(gè)Java程序從main()方法開始執(zhí)行,然后按照既定的代碼邏輯執(zhí)行,看似沒有其他線程參與,但實(shí)際上Java程序天生就是多線程程序,因?yàn)閳?zhí)行main()是一個(gè)名稱為main的線程。

public class MultiThread {
    public static void main(String[] args) {
        //獲取 Java 線程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //不需要獲取同步的monitor和synchronized信息,僅獲取線程和線程堆棧信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍歷線程信息,僅打印線程ID和線程名稱信息
        for(ThreadInfo threadInfo : threadInfos){
            System.out.println("[" + threadInfo.getThreadId()+"] "+ threadInfo.getThreadName());
        }
    }
}

運(yùn)行結(jié)果:

[6] Monitor Ctrl-Break
[5] Attach Listener
[4] Signal Dispatcher
[3] Finalizer
[2] Reference Handler
[1] main

Process finished with exit code 0

??可以看到,一個(gè)Java程序的運(yùn)行不僅僅是main()的運(yùn)行,而是main線程和多個(gè)其他線程的同時(shí)運(yùn)行。
4.1.2 為什么要使用多線程
??執(zhí)行一個(gè)簡(jiǎn)單的"Hello,World!",卻啟動(dòng)了那么多的“無關(guān)”線程,是不是把簡(jiǎn)單的問題復(fù)雜化了?當(dāng)然不是,因?yàn)檎_使用多線程,總是能夠給開發(fā)人員帶來顯著的好處,而使用多線程的原因主要有以下幾點(diǎn)。
??(1)更多的處理器核心
??隨著處理器上的核心數(shù)量越來越多,以及超線程技術(shù)的廣泛運(yùn)用,現(xiàn)在大多數(shù)計(jì)算機(jī)都比以往更加擅長并行計(jì)算,而處理器性能的提升方式,也從更高的主頻向更多的核心發(fā)展。如何利用好處理器上的多個(gè)核心也成了現(xiàn)在的主要問題。
??線程是大多數(shù)操作系統(tǒng)調(diào)度的基本單元,一個(gè)程序作為一個(gè)進(jìn)程來運(yùn)行,程序運(yùn)行過程中能夠創(chuàng)建多個(gè)線程,而一個(gè)線程在一個(gè)時(shí)刻只能運(yùn)行在一個(gè)處理器核心上。試想一下,一個(gè)單線程程序在運(yùn)行時(shí)只能使用一個(gè)處理器核心,那么再多的處理器核心加入也無法顯著提升該程序的執(zhí)行效率。相反,如果該程序使用多線程技術(shù),將計(jì)算邏輯分配到多個(gè)處理器核心上,就會(huì)顯著減少程序的處理時(shí)間,并且隨著更多處理器核心的加入而變得更有效率。
??(2)更快的響應(yīng)時(shí)間
??有時(shí)我們會(huì)編寫一些較為復(fù)雜的代碼(這里的復(fù)雜不是說復(fù)雜的算法,而是復(fù)雜的業(yè)務(wù)邏輯),例如,一筆訂單的創(chuàng)建,它包括插入訂單數(shù)據(jù)、生成訂單快照、發(fā)送郵件通知記錄貨品銷售數(shù)量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結(jié)果。但是這么多業(yè)務(wù)操作,如何能夠讓其更快地完成呢?
??在上面的場(chǎng)景中,可以使用多線程技術(shù),即將數(shù)據(jù)一致性不強(qiáng)的操作派發(fā)給其他線程處理(也可以使用消息隊(duì)列),如生成訂單快照、發(fā)送郵件等。這樣做的好處是響應(yīng)用戶請(qǐng)求的線程能夠盡可能快地處理完成,縮短了響應(yīng)時(shí)間,提升了用戶體驗(yàn)。
??(3)更好的編程模型
??Java為多線程提供了良好、考究并且一致的編程模型,使開發(fā)人員能夠更加專注于問題的解決,即為所遇到的問題建立合適的模型,而不是絞盡腦汁地考慮如何將其多線程化。一旦開發(fā)人員建立好了模型,稍做修改總是能夠方便地映射到Java提供的多線程編程模型上。
4.1.3 線程優(yōu)先級(jí)
??現(xiàn)代操作系統(tǒng)基本采用時(shí)分的形式調(diào)度運(yùn)行的線程,操作系統(tǒng)會(huì)分出一個(gè)個(gè)時(shí)間片,線程會(huì)分配到若干時(shí)間片,當(dāng)線程的時(shí)間片用完了就會(huì)發(fā)生線程調(diào)度,并等待著下次分配。線程分配到的時(shí)間片多少也就決定了線程使用處理器資源的多少,而線程優(yōu)先級(jí)就是決定線程需要多或者少分配一些處理器資源的線程屬性。
??在Java線程中,通過一個(gè)整型成員變量priority來控制優(yōu)先級(jí),優(yōu)先級(jí)的范圍從1~10,在線程構(gòu)建的時(shí)候可以通過setPriority(int)方法來修改優(yōu)先級(jí),默認(rèn)優(yōu)先級(jí)是5,優(yōu)先級(jí)高的線程分配時(shí)間片的數(shù)量要多于優(yōu)先級(jí)低的線程。設(shè)置線程優(yōu)先級(jí)時(shí),針對(duì)頻繁阻塞(休眠或者I/O操作)的線程需要設(shè)置較高的優(yōu)先級(jí),確保處理器不會(huì)被獨(dú)占。在不同的JVM以及操作系統(tǒng)上,線程規(guī)劃會(huì)存在差異,有些操作系統(tǒng)甚至?xí)雎詫?duì)線程優(yōu)先級(jí)的設(shè)定。
4.1.4 線程的狀態(tài)
??Java線程在運(yùn)行的生命周期中可能處于如下圖所示的6種不同的狀態(tài),在給定的一個(gè)時(shí)刻,線程只能處于其中的一個(gè)狀態(tài)。

Java線程狀態(tài).jpg

??線程在自身的生命周期中,并不是固定地處于某個(gè)狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換,Java線程狀態(tài)變遷如圖:
線程狀態(tài).jpg

??由上圖可以看出,線程創(chuàng)建后,調(diào)用start()方法開始運(yùn)行。當(dāng)線程執(zhí)行wait()方法之后,線程進(jìn)行等待狀態(tài)。進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài),而超時(shí)等待狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,也就是超時(shí)時(shí)間到達(dá)時(shí)將會(huì)返回到運(yùn)行狀態(tài)。當(dāng)線程調(diào)用同步方法時(shí),在沒有獲取到鎖的情況下,線程將會(huì)進(jìn)入到阻塞狀態(tài)。線程在執(zhí)行Runnable的run()方法之后將會(huì)進(jìn)入到終止?fàn)顟B(tài)。


注意:Java將操作系統(tǒng)中的運(yùn)行和就緒兩個(gè)狀態(tài)合并稱為運(yùn)行狀態(tài)。阻塞狀態(tài)是線程阻塞在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時(shí)的狀態(tài),但是阻塞在java.concurrent包中Lock接口的線程狀態(tài)卻是等待狀態(tài),因?yàn)閖ava.concurrent包中Lock接口對(duì)于阻塞的實(shí)現(xiàn)均使用了LockSupport類中的相關(guān)方法。


4.1.5 Daemon 線程
??Daemon線程被用作完成支持性工作,但是在Java虛擬機(jī)退出時(shí)Daemon線程中的finally塊并不一定會(huì)執(zhí)行,示例代碼如下:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
        thread.setDaemon(true);
        thread.start();
    }
    
    static class DaemonRunner implements Runnable {
        @Override
        public void run() {
            try{
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("DaemonThread finally run.");
            }
        }
    }
}

??運(yùn)行Daemon程序,可以看到在終端或者命令提示符上沒有任何輸出。main線程(非Daemon線程)在啟動(dòng)了線程DaemonRunner之后隨著main方法執(zhí)行完畢而終止,而此時(shí)Java虛擬機(jī)中已經(jīng)沒有非Daemon線程,虛擬機(jī)需要退出。Java虛擬機(jī)中的所有Daemon線程都需要立即終止,因?yàn)镈aemonRunner立即終止,但是DaemonRunner中的finally塊并沒有執(zhí)行。


注意:在構(gòu)建Daemon線程時(shí),不能依靠finally塊中的內(nèi)容來確保執(zhí)行關(guān)閉或清理資源的邏輯。


4.2啟動(dòng)和終止線程

4.2.1 構(gòu)造線程
??在運(yùn)行線程之前首先要構(gòu)造一個(gè)線程對(duì)象,線程對(duì)象在構(gòu)造的時(shí)候需要提供線程所需要的屬性,如線程所屬的線程組、線程優(yōu)先級(jí)、是否是Daemon 線程等信息。以下代碼摘自Thread中對(duì)線程初始化的部分。

   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

??在上述過程中,一個(gè)新構(gòu)造的線程對(duì)象是由其parent線程來進(jìn)行空間分配的,而child線程繼承了parent是否為Daemon、優(yōu)先級(jí)和加載資源的contextClassLoader以及可繼承的ThreadLocal,同時(shí)還會(huì)分配一個(gè)唯一的ID來標(biāo)識(shí)這個(gè)child線程。至此,一個(gè)能夠運(yùn)行的線程對(duì)象就初始化好了,在堆內(nèi)存中等待著運(yùn)行。
4.2.2 啟動(dòng)線程
??線程對(duì)象初始化完成之后,調(diào)用start()方法就可以啟動(dòng)這個(gè)線程。線程start()方法的含義是:當(dāng)前線程(即parent線程)同步告知Java虛擬機(jī),只要線程規(guī)劃器空閑,應(yīng)立即啟動(dòng)調(diào)用start()方法的線程。


注意:?jiǎn)?dòng)一個(gè)線程前,最好為這個(gè)線程設(shè)置線程名稱,因?yàn)檫@樣在使用jstack分析程序或者進(jìn)行問題排查時(shí),就會(huì)給開發(fā)人員提供一些提示,自定義的線程最好能夠起個(gè)名字。


4.2.3 理解中斷
??中斷可以理解為線程的一個(gè)標(biāo)識(shí)位屬性,它表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作。中斷好比其他線程對(duì)該線程打了個(gè)招呼,其他線程通過調(diào)用該線程的interrupt()方法對(duì)其進(jìn)行中斷操作。
??線程通過檢查自身是否被中斷來進(jìn)行響應(yīng),線程對(duì)過方法isInterrupted()來進(jìn)行判斷是否被中斷,也可以調(diào)用靜態(tài)方法Thread.interrupted()對(duì)當(dāng)前線程的中斷標(biāo)識(shí)位進(jìn)行復(fù)位。如果該線程已經(jīng)處于終結(jié)狀態(tài),即使該線程被中斷過,在調(diào)用該線程對(duì)象的isInterrupted()時(shí)依舊會(huì)返回false。
??從Java的API中可以看到,許多聲明拋出InterruptedException的方法(例如Thread.sleep(long millis)方法)這些方法在拋出InterruptedException之前,Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)識(shí)位清除,然后拋出InterruptedException,此時(shí)調(diào)用isInterrupted()方法將會(huì)返回false。
??在以下所示的例子中,首先創(chuàng)建了兩個(gè)線程,SleepThread和BusyThread,前者不停地睡眠,后者一直運(yùn)行,然后對(duì)這兩個(gè)線程分別進(jìn)行中斷操作,觀察二者的中斷標(biāo)識(shí)位。

public class Interrupted {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread 不停的嘗試睡眠
        Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
        sleepThread.setDaemon(true);
        //busyThread 不停的運(yùn)行
        Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();

        //休眠5秒,讓sleepThread 和busyThread充分運(yùn)行
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread  is "+ sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is "+ busyThread.isInterrupted());
        // 防止sleepThread和busyThread立即退出
        TimeUnit.SECONDS.sleep(2);
    }

    static class SleepRunner implements Runnable {
        @Override
        public void run() {
            while(true){
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class BusyRunner implements Runnable{
        @Override
        public void run() {
            while (true){

            }
        }
    }

}

輸出結(jié)果如下:

SleepThread  is false
BusyThread interrupted is true

??從結(jié)果可以看出,拋出InterruptedException的線程SleepThread,其中斷標(biāo)識(shí)位被清除了,而一直忙碌運(yùn)行的線程BusyThread,中斷標(biāo)識(shí)位沒有被清除。
**4.2.4 過期的suspend()、resume()和stop()
??我們對(duì)于CD機(jī)應(yīng)該不會(huì)陌生,如果把它播放音樂比作一個(gè)線程的運(yùn)作,那么對(duì)音樂播放做出的暫停、恢復(fù)和停止操作對(duì)應(yīng)在線程Thread的API就是suspend()、resume()和stop()。

public class Deprecated {
    public static void main(String[] args) throws InterruptedException {
        DateFormat format = new SimpleDateFormat("HH:mm:ss");
        Thread printThread = new Thread(() -> {
            DateFormat format1 = new SimpleDateFormat("HH:mm:ss");
            while (true){
                System.out.println(Thread.currentThread().getName()+" Run at "+format1.format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        printThread.setDaemon(true);
        printThread.start();
        TimeUnit.SECONDS.sleep(3);
        // 將PrintThread進(jìn)行暫停,輸出內(nèi)容工作停止
        printThread.suspend();
        System.out.println("main suspend PrintThread at "+ format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);

        // 將PrintThread 進(jìn)行恢復(fù),輸出內(nèi)容繼續(xù)
        printThread.resume();
        System.out.println("main resume PrintThread at "+ format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);

        // 將PrintThread進(jìn)行終止,輸出內(nèi)容停止
        printThread.stop();
        System.out.println("main stop PrintThread at "+ format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);
    }
}

輸出結(jié)果如下(輸出內(nèi)容中的時(shí)間與示例執(zhí)行的具體時(shí)間相關(guān))。

Thread-0 Run at 19:46:16
Thread-0 Run at 19:46:17
Thread-0 Run at 19:46:18
main suspend PrintThread at 19:46:19
main resume PrintThread at 19:46:22
Thread-0 Run at 19:46:22
Thread-0 Run at 19:46:23
Thread-0 Run at 19:46:24
main stop PrintThread at 19:46:25

??在執(zhí)行過程中,PrintThread運(yùn)行了3秒,隨后被暫停,3秒后恢復(fù),最后經(jīng)過3秒被終止。
??通過示例的輸出可以看到,suspend()、resume()和stop()方法完成了線程的暫停、恢復(fù)和終止工作,而且非?!叭诵曰薄5沁@些API是過期的,也就是不建議使用的。
??不建議使用的原因主要有:以suspend()方法為例,在調(diào)用后,線程不會(huì)釋放已經(jīng)占有的資源(比如鎖),而是占用著資源進(jìn)入睡眠狀態(tài),這樣容易引發(fā)死鎖問題。同樣,stop()方法在終結(jié)一個(gè)線程時(shí)不會(huì)保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機(jī)會(huì),因此導(dǎo)致程序可能工作在不確定狀態(tài)下。


注意:正是因?yàn)閟uspend()、resume()和stop()方法帶來的副作用,這些方法才被標(biāo)注為不建議使用的過期方法,而暫停和恢復(fù)操作可以用后面的等待/通知機(jī)制來替代。


4.2.5 安全地終止線程
??在4.2.3中提到的中斷狀態(tài)是線程的一個(gè)標(biāo)識(shí)位,而中斷操作是一種簡(jiǎn)便的線程間交互方式,而這種交互方式最適合用來取消或停止任務(wù)。除了中斷以外,還可以利用一個(gè)boolean變量來控制是否需要停止任務(wù)并終止該線程。
??在以下代碼里,創(chuàng)建一個(gè)線程CountThread,它不斷地進(jìn)行變量累加,而主線程嘗試對(duì)其進(jìn)行中斷操作和停止操作。

public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        //睡眠1秒,main線程對(duì)CountThread進(jìn)行中斷,使CountThread能夠感知中斷而結(jié)束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        // 睡眠1秒,main線程對(duì)Runner two進(jìn)行取消,使CountThread能夠感知on 為false而結(jié)束
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;
        @Override
        public void run() {
            while(on && !Thread.currentThread().isInterrupted()){
                i++;
            }
            System.out.println("Count i = "+i);
        }

        private void cancel() {
            on = false;
        }
    }

}

輸出結(jié)果如下:

Count i = 511158113
Count i = 513235580

??示例在執(zhí)行過程中,main線程通過中斷操作和cancel()方法均可使CountThread得以終止。這種通過標(biāo)識(shí)位或者中斷操作的方式能夠使線程在終止時(shí)有機(jī)會(huì)去清理資源,而不是武斷地將線程停止,因此這種終止線程的做法顯得更加安全和優(yōu)雅。

4.3 線程間通信

??線程開始運(yùn)行,擁有自己的??臻g,就如同一個(gè)腳本一樣,按照既定的代碼一步一步地執(zhí)行,直到終止。但是,每個(gè)運(yùn)行中的線程,如果僅僅是孤立地運(yùn)行,那么沒有一點(diǎn)兒價(jià)值,或者說價(jià)值很少,如果多個(gè)線程能夠相互配合完成工作,這將會(huì)帶來巨大的價(jià)值。
**4.3.1 volatile 和synchronized 關(guān)鍵字
??Java支持多個(gè)線程同時(shí)訪問一個(gè)對(duì)象或者對(duì)象或者對(duì)象的成員變量,由于每個(gè)線程可以擁有這個(gè)變量的拷貝(雖然對(duì)象以及成員變量分配的內(nèi)存在共享內(nèi)存中的,但是每個(gè)執(zhí)行的線程還是可以擁有一份拷貝,這樣做的目的是加速程序的執(zhí)行,這是現(xiàn)代多核處理器的一個(gè)顯著特性),所以程序在執(zhí)行過程中,一個(gè)線程看到的變量并不一定是最新的。
??關(guān)鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對(duì)該訪問均需要從共享內(nèi)存中獲取,而對(duì)它的改變必須同步刷新回共享內(nèi)存,它能保證所有線程對(duì)變量可見性。
??舉個(gè)例子,定義一個(gè)表示程序是否運(yùn)行的成員變量 boolean on = true,那么另一個(gè)線程可能對(duì)它執(zhí)行關(guān)閉操作(on=false)這里涉及多個(gè)線程對(duì)變量的訪問,因此需要將其定義成為volatile boolean on = true,這樣其他線程對(duì)它進(jìn)行改變時(shí),可以讓所有線程感知到變化,因?yàn)樗袑?duì)on變量的訪問和修改都需要以共享內(nèi)存為準(zhǔn)。但是,過多地使用volatile是不必要的,因?yàn)樗鼤?huì)降低程序執(zhí)行的效率。
??關(guān)鍵字synchronized可以修飾方法或者以同步塊的形式來進(jìn)行使用,它主要確保多個(gè)線程在同一個(gè)時(shí)刻,只能有一個(gè)線程處于方法或者同步塊中,它保證了線程對(duì)變量訪問的可見性和排他性。
??在以下代碼的例子中,使用了同步塊和同步方法,但通過使用javap工具查看生成的class文件信息來分析synchronized關(guān)鍵字的實(shí)現(xiàn)細(xì)節(jié),示例如下。

public class Synchronized {
    public static void main(String[] args) {
        //對(duì) Synchronized Class 對(duì)象進(jìn)行加鎖
        synchronized (Synchronized.class){

        }
        // 靜態(tài)同步方法, 對(duì) Synchronized Class對(duì)象進(jìn)行加鎖
        m();
    }

    public static synchronized void m() {

    }
}

部分輸出如下所示:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class yisu/Synchronized
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: invokestatic  #3                  // Method m:()V
        18: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any
      LineNumberTable:
        line 6: 0
        line 8: 5
        line 10: 15
        line 11: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 10
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

??上面Class信息中,對(duì)于同步塊的實(shí)現(xiàn)使用了monitorenter 和 monitorexit指令,而同步方法則是依靠方法上的ACC_SYNCHRONIZED來完成的,無論采用哪種方式,其本質(zhì)是對(duì)一個(gè)對(duì)象的監(jiān)視器(monitor)進(jìn)行獲取,而這個(gè)獲取過程是排他的,也就是同一時(shí)刻只能有一個(gè)線程獲取到由synchronized所保護(hù)對(duì)象的監(jiān)視器。
??任意一個(gè)對(duì)象擁有都擁有自己的監(jiān)視器,當(dāng)這個(gè)對(duì)象由同步塊或者這個(gè)對(duì)象的同步方法調(diào)用時(shí),執(zhí)行方法的線程必須先獲取到該對(duì)象的監(jiān)視器才能進(jìn)入同步塊或者同步方法,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會(huì)被阻塞在同步塊和同步方法的入口處,進(jìn)入BLOCKED狀態(tài)。
??下圖描述了對(duì)象、對(duì)象監(jiān)視器、同步隊(duì)列和執(zhí)行線程之間的關(guān)系。


對(duì)象、線程之間的關(guān)系.jpg

??從上圖可以看到,任意線程對(duì)Object(Object 由synchronized保護(hù))的訪問,首先要獲取Object的監(jiān)視器。如果獲取失敗,線程進(jìn)入同步隊(duì)列,線程狀態(tài)變?yōu)锽LOCKED。當(dāng)訪問Object的前驅(qū)(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊(duì)列中的線程,使其重新嘗試對(duì)監(jiān)視器的獲取。
4.3.2 等待/通知機(jī)制
??一個(gè)線程修改了一個(gè)對(duì)象的值,而另一個(gè)線程感知到了變化,然后進(jìn)行相應(yīng)的操作,整個(gè)過程開始于一個(gè)線程,而最終執(zhí)行又是另一個(gè)線程。前者是生產(chǎn)者,后者就是消費(fèi)者,這種模式隔離了“做什么”(what)和“怎么做”(How),在功能層面上實(shí)現(xiàn)了解耦,體系結(jié)構(gòu)上具備了良好的伸縮性,但是在Java語言中如何實(shí)現(xiàn)類似的功能呢?
??簡(jiǎn)單的辦法是讓消費(fèi)者線程不斷地循環(huán)檢查變量是否符合預(yù)期,如下面代碼所示,在while循環(huán)中設(shè)置不滿足的條件,如果條件滿足則退出while循環(huán),從而完成消費(fèi)者的工作。

while (value != desire){
        Thread.sleep(1000);
}
doSomething();

??上面這段偽代碼在條件不滿足就睡眠一段時(shí)間,這樣做的目的是防止過快的“無效”嘗試,這種方式看似能夠解實(shí)現(xiàn)所需的功能,但是卻存在如下問題。
??1)難以確保及時(shí)性。在睡眠時(shí),基本不消耗處理器資源,但是如果睡得過久,就不能及時(shí)發(fā)現(xiàn)條件已經(jīng)變化,也就是及時(shí)性難以保證。
??2)難以降低開銷。如果降低睡眠的時(shí)間,比如休眠1毫秒,這樣消費(fèi)者能更加迅速地發(fā)現(xiàn)條件變化,但是卻可能消耗更多的處理器資源,造成了無端的浪費(fèi)。
??以上兩個(gè)問題,看似矛盾難以調(diào)和,但是Java通過內(nèi)置的等待/通知機(jī)制能夠很好地解決這個(gè)矛盾并實(shí)現(xiàn)所需的功能。
??等待/通知的相關(guān)方法是任意Java對(duì)象都具備的,因?yàn)檫@些方法被定義在所有對(duì)象的超類java.lang.Object上,方法和描述如下:


等待和通知的相關(guān)方法.jpg

??等待/通知機(jī)制,是指一個(gè)線程A調(diào)用了對(duì)象O的wait()方法進(jìn)入等待狀態(tài),而另一個(gè)線程B調(diào)用了對(duì)象O的notify()或者notifyAll()方法,線程A收到通知后從對(duì)象O的wait()和notify/notifyAll()的關(guān)系就如同開關(guān)信號(hào)一樣,用來完成等待方和通知方之間的交互工作。
??在下面的例子中,創(chuàng)建了兩個(gè)線程-WaitThread和NotifyThread,前者檢查flag值是否為false,如果符合要求,進(jìn)行后續(xù)操作,否則在lock上等待,后者在睡眠了一段時(shí)間后對(duì)lock進(jìn)行通知,示例如下。

package yisu;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }

    static class Wait implements Runnable{
        @Override
        public void run() {
            //加鎖,擁有l(wèi)ock的Monitor
            synchronized (lock){
                // 當(dāng)條件不滿足時(shí),繼續(xù)wait,同時(shí)釋放了lock的鎖
                while (flag){
                    try{
                        System.out.println(Thread.currentThread()+" flag is true. wait @ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        lock.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
                // 條件滿足時(shí),完成工作
                System.out.println(Thread.currentThread() + " flag is false. running @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            }
        }
    }

    static  class Notify implements Runnable{
        @Override
        public void run() {
            //加鎖,擁有 lock的Monitor
            synchronized (lock){
                //獲取lock的鎖,然后進(jìn)行通知,通知時(shí)不會(huì)釋放lock的鎖。
                //直到當(dāng)前線程釋放了lock后,WaitThread才能從wait方法中返回
                System.out.println(Thread.currentThread() + " hold lock. notify @ "+
                        new SimpleDateFormat("HH:mm:ss").format(new Date()));
                lock.notifyAll();
                flag = false;
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //再次加鎖
            synchronized (lock){
                System.out.println(Thread.currentThread() + " hold lock again. sleep @ "+
                        new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

輸出結(jié)果:

Thread[WaitThread,5,main] flag is true. wait @ 23:17:46
Thread[NotifyThread,5,main] hold lock. notify @ 23:17:47
Thread[NotifyThread,5,main] hold lock again. sleep @ 23:17:52
Thread[WaitThread,5,main] flag is false. running @ 23:17:57

??上述第3行和第4行輸出的順序可能會(huì)互換,而上述例子主要說明了調(diào)用wait()、notify()以及notifyAll()時(shí)需要注意的細(xì)節(jié),如下。
??1)使用wait()、notify()和notifyAll()時(shí)需要先調(diào)用對(duì)象加鎖。
??2)調(diào)用wait()方法后,線程狀態(tài)由RUNNINg變?yōu)閃AITING,并將當(dāng)前線程放置到對(duì)象的等待隊(duì)列。
??3)notify()或notifyAll()方法調(diào)用后,等待線程依舊不會(huì)從wait()返回,需要調(diào)用notify()或notifyAll()的線程釋放鎖之后,等待線程才有機(jī)會(huì)從wait()返回。
??4)notify()方法將等待隊(duì)列中的一個(gè)等待線程從等待隊(duì)列中移動(dòng)到同步隊(duì)列中,而notifyAll()方法則是將等待隊(duì)列中所有的線程全部移到同步隊(duì)列,被移動(dòng)的線程狀態(tài)由WAITING變?yōu)锽LOCKED。
??5)從wait()方法返回的前提是獲得了調(diào)用對(duì)象的鎖。
??從上述細(xì)節(jié)中可以看到,等待/通知機(jī)制依托于同步機(jī)制,其目的就是確保等待線程從wait()方法返回時(shí)能夠感知到通知線程對(duì)變量做出的修改。
??下圖描述了上述示例的過程:

WaitNotify的運(yùn)行.jpg

??在上圖中,WaitThread首先獲取了對(duì)象的鎖,然后調(diào)用對(duì)象的wait()方法,從而放棄了鎖并進(jìn)入了對(duì)象的等待隊(duì)列WaitQueue中,進(jìn)入等待狀態(tài)。由于WaitThread釋放了對(duì)象的鎖,NotifyThread隨后獲取了對(duì)象的鎖,并調(diào)用對(duì)象的nofity()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時(shí)WaitThread的狀態(tài)變?yōu)樽枞麪顟B(tài)。NotifyThread釋放了鎖之后,WaitThread再次獲取到鎖并從wait()方法返回繼續(xù)執(zhí)行。
4.3.3 等待/通知的經(jīng)典范式
??等待方遵循如下原則:
??1)獲取對(duì)象的鎖。
??2)如果條件不滿足,那么調(diào)用對(duì)象的wait()方法,被通知后仍要檢查條件。
??3)條件滿足則執(zhí)行對(duì)應(yīng)的邏輯。
??對(duì)應(yīng)的偽代碼如下:

synchronized (對(duì)象){
         while(條件不滿足){
             對(duì)象的.wait();
        }
        對(duì)應(yīng)的處理邏輯。
}

??通知方遵循如下原則。
??1)獲得對(duì)象的鎖。
??2)改變條件。
??3)通知所有等待在對(duì)象上的線程。
??對(duì)應(yīng)的偽代碼如下.

synchronized(對(duì)象){
    改變條件
    對(duì)象.notifyAll();
}

4.3.4 管道輸入/輸出流
??它主要用于線程數(shù)據(jù)傳輸,而傳輸?shù)拿浇闉閮?nèi)存。
??主要包括下面4種實(shí)現(xiàn):PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前面面向字節(jié),后面兩種面向字符。
??對(duì)于Piped類型的流,必須先要進(jìn)行綁定,也就是調(diào)用connect()方法,如果沒有將輸入/輸出流綁定起來,對(duì)于該流的訪問將會(huì)拋出異常。
** 4.3.5 Thread.join()的使用**
??如果一個(gè)線程 A 執(zhí)行了 thread. join()語句, 其含義是: 當(dāng)前線程 A 等待 thread 線程終止之后才從 thread.join()返回。 線程 Thread 除了提供 join()方法之外, 還提供了 join(long millis)和 join(long millis, int nanos)兩個(gè)具備超時(shí)特性的方法。 這兩個(gè)超時(shí)方法表示, 如果線 程 thread 在給定的超時(shí)時(shí)間里沒有終止, 那么將會(huì)從該超時(shí)方法中返回。
??每個(gè)線程終止的前提是前驅(qū)線程的終止, 每個(gè)線程等待前驅(qū)線程終止后, 才從 join()方法返回, 這里涉及了等待/通知機(jī)制(等待前驅(qū)線程結(jié)束, 接收前驅(qū)線程結(jié)束通知)。
??當(dāng)線程終止時(shí), 會(huì)調(diào)用線程自身的notifyAll()方法, 會(huì)通知所有等待在該線程對(duì)象上的線程。加鎖、循環(huán)和處理邏輯3個(gè)步驟。
4.3.6 ThreadLocal的使用
??ThreadLocal , 即線程變量, 是一個(gè)以ThreadLocal對(duì)象為鍵、任意對(duì)象為值的存儲(chǔ)結(jié)婚。這個(gè)結(jié)構(gòu)被附帶在線程上,也就是說一個(gè)線程可以根據(jù)一個(gè)ThreadLocal對(duì)象查詢到綁定在這個(gè)線程上的一個(gè)值。
??可以通過set(T)方法來設(shè)置一個(gè)值,在當(dāng)前線程下再通過get()方法獲取到原先設(shè)置的值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容