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

轉(zhuǎn):《Java并發(fā)編程的藝術(shù)》

1 線程簡(jiǎn)介

現(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)程。現(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程,也叫輕量級(jí)進(jìn)程(Light Weight Process),在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,這些線程都擁有各自的計(jì)數(shù)器、堆棧和局部變量等屬性,并且能夠訪問共享的內(nèi)存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時(shí)執(zhí)行。

2 線程優(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í), 而偏重計(jì)算(需要較多CPU時(shí)間或者偏運(yùn)算) 的線程則設(shè)置較低的優(yōu)先級(jí), 確保處理器不會(huì)被獨(dú)占。 在不同的JVM以及操作系統(tǒng)上, 線程規(guī)劃會(huì)存在差異,有些操作系統(tǒng)甚至?xí)雎詫?duì)線程優(yōu)先級(jí)的設(shè)定(因此,在程序中設(shè)置線程優(yōu)先級(jí)的實(shí)踐意義并不大,因?yàn)榫€程優(yōu)先級(jí)的最終解釋權(quán)在底層操作系統(tǒng))。

3 并發(fā)與并行的區(qū)別

如果某個(gè)系統(tǒng)支持兩個(gè)或者多個(gè)操作(Action)同時(shí)存在,那么這個(gè)系統(tǒng)就是一個(gè)并發(fā)系統(tǒng)。如果某個(gè)系統(tǒng)支持兩個(gè)或者多個(gè)動(dòng)作同時(shí)執(zhí)行,那么這個(gè)系統(tǒng)就是一個(gè)并行系統(tǒng)。并發(fā)系統(tǒng)與并行系統(tǒng)這兩個(gè)定義之間的關(guān)鍵差異在于“存在”這個(gè)詞。

在并發(fā)程序中可以同時(shí)擁有兩個(gè)或者多個(gè)線程。這意味著,如果程序在單核處理器上運(yùn)行,那么這兩個(gè)線程將交替地?fù)Q入或者換出內(nèi)存。這些線程是同時(shí)“存在”的——每個(gè)線程都處于執(zhí)行過程中的某個(gè)狀態(tài)。如果程序能夠并行執(zhí)行,那么就一定是運(yùn)行在多核處理器上。此時(shí),程序中的每個(gè)線程都將分配到一個(gè)獨(dú)立的處理器核上,因此可以同時(shí)運(yùn)行。

“并行”概念是“并發(fā)”概念的一個(gè)子集。也就是說,你可以編寫一個(gè)擁有多個(gè)線程或者進(jìn)程的并發(fā)程序,但如果沒有多核處理器來執(zhí)行這個(gè)程序,那么就不能以并行方式來運(yùn)行代碼。因此,凡是在求解單個(gè)問題時(shí)涉及多個(gè)執(zhí)行流程的編程模式或者執(zhí)行行為,都屬于并發(fā)編程的范疇。

用一個(gè)極其簡(jiǎn)單的生活實(shí)例來解釋如下:
  你吃飯吃到一半,電話來了,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行。
  你吃飯吃到一半,電話來了,你停了下來接了電話,接完后繼續(xù)吃飯,這說明你支持并發(fā)。
  你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支持并行。
  并發(fā)的關(guān)鍵是你有處理多個(gè)任務(wù)的能力,不一定要同時(shí)。并行的關(guān)鍵是你有同時(shí)處理多個(gè)任務(wù)的能力。

4 線程狀態(tài)及其切換

下面的這個(gè)圖非常重要!你如果看懂了這個(gè)圖,那么對(duì)于多線程的理解將會(huì)更加深刻。


  1. 新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象。
  2. 就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后,其他線程調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線程位于可運(yùn)行線程隊(duì)列中,變得可運(yùn)行,等待獲取CPU的使用權(quán)。
  3. 運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼。
  4. 阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行。直到線程進(jìn)入就緒狀態(tài),才有機(jī)會(huì)再次轉(zhuǎn)到運(yùn)行狀態(tài)。

阻塞的情況分三種:
等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會(huì)把該線程放入等待池中(wait會(huì)釋放持有的鎖)。
同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí),若該同步鎖被別的線程占用,則JVM會(huì)把該線程放入鎖池中
其他阻塞運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請(qǐng)求時(shí),JVM會(huì)把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)(注意,sleep不會(huì)釋放線程所持有的鎖)

  1. 死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。

Java線程在運(yùn)行的生命周期中可能處于下圖表中所示的6種不同的狀態(tài), 在給定的一個(gè)時(shí)刻,線程只能處于其中的一個(gè)狀態(tài)。

下面使用jstack工具(可以選擇打開終端, 鍵入jstack或者到JDK安裝目錄的bin目錄下執(zhí)行命令), 嘗試查看示例代碼運(yùn)行時(shí)的線程信息, 更加深入地理解線程狀態(tài)。測(cè)試代碼如下:

public class ThreadState {
    // 該線程不斷地進(jìn)行睡眠
    static class TimeWaiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils.second(100);
            }
        }
    }

    // 該線程在Waiting.class實(shí)例上等待
    static class Waiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (Waiting.class) {
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    // 該線程在Blocked.class實(shí)例上加鎖后, 不會(huì)釋放該鎖
    static class Blocked implements Runnable {
        @Override
        public void run() {
            synchronized (Blocked.class) {
                while (true) {
                    SleepUtils.second(100);
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TimeWaiting (), "TimeWaitingThread").start();
        new Thread(new Waiting(), "WaitingThread").start();
        // 使用兩個(gè)Blocked線程, 一個(gè)獲取鎖成功, 另一個(gè)被阻塞
        new Thread(new Blocked(), "BlockedThread-1").start();
        new Thread(new Blocked(), "BlockedThread-2").start();
    }
}

上述示例中使用的SleepUtils代碼如下:

public class SleepUtils {

    public static final void second(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds) ;
        } catch (InterruptedException e) {
        }
    }
}

運(yùn)行該示例, 打開終端或者命令提示符, 鍵入“jps”, 輸出如下:

16544 Jps
13700
10156 ThreadState

可以看到運(yùn)行示例對(duì)應(yīng)的進(jìn)程ID是10156,接著再輸入“jstack 10156”,部分輸出如下:

//BlockedThread-2線程阻塞在獲取Blocked.class示例的鎖上
"BlockedThread-2" #13 prio=5 os_prio=0 tid=0x00000000180ad800 nid=0x108c waiting for monitor entry [0x0000000018e8f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at ThreadState$Blocked.run(ThreadState.java:35)
        - waiting to lock <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
        at java.lang.Thread.run(Thread.java:745)
//BlockedThread-1線程獲取到了Blocked.class的鎖,處于睡眠狀態(tài)
"BlockedThread-1" #12 prio=5 os_prio=0 tid=0x00000000180ad000 nid=0x2740 waiting on condition [0x0000000018d8e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at SleepUtils.second(SleepUtils.java:6)
        at ThreadState$Blocked.run(ThreadState.java:35)
        - locked <0x00000000e01e4db0> (a java.lang.Class for ThreadState$Blocked)
        at java.lang.Thread.run(Thread.java:745)
//WaitingThread線程在Waitting實(shí)例上等待
"WaitingThread" #11 prio=5 os_prio=0 tid=0x00000000180a6000 nid=0x93c in Object.wait() [0x0000000018c8f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
        at java.lang.Object.wait(Object.java:502)
        at ThreadState$Waiting.run(ThreadState.java:21)
        - locked <0x00000000e01e37c0> (a java.lang.Class for ThreadState$Waiting)
        at java.lang.Thread.run(Thread.java:745)
//TimeWaitingThread線程處于超時(shí)等待
"TimeWaitingThread" #10 prio=5 os_prio=0 tid=0x00000000180a5000 nid=0x8c4 waiting on condition [0x0000000018b8f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at SleepUtils.second(SleepUtils.java:6)
        at ThreadState$TimeWaiting.run(ThreadState.java:8)
        at java.lang.Thread.run(Thread.java:745)

通過示例, 我們了解到Java程序運(yùn)行中線程狀態(tài)的具體含義。 線程在自身的生命周期中,并不是固定地處于某個(gè)狀態(tài), 而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換。

下面是一張更詳細(xì)的線程狀態(tài)遷移圖:

從圖中可以看到, 線程創(chuàng)建之后, 調(diào)用start()方法開始運(yùn)行(這里的運(yùn)行狀態(tài)其實(shí)是就緒態(tài)和運(yùn)行態(tài)的合集)。 當(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)。

注意:阻塞狀態(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)方法。

5 線程的啟動(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è)名字。

中斷可以理解為線程的一個(gè)標(biāo)識(shí)位屬性, 它表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作。 中斷好比其他線程對(duì)該線程打了個(gè)招呼, 其他線程通過調(diào)用該線程的interrupt()方法對(duì)其進(jìn)行中斷操作。

線程通過檢查自身是否被中斷來進(jìn)行響應(yīng), 線程通過方法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。

調(diào)用某個(gè)線程的interrupt()方法,將會(huì)設(shè)置該線程為中斷狀態(tài),即設(shè)置為true。線程中斷后的結(jié)果是死亡、還是等待新的任務(wù)或是繼續(xù)運(yùn)行至下一步,取決于這個(gè)程序本身。線程可以不時(shí)地檢測(cè)這個(gè)中斷標(biāo)識(shí)位,以判斷線程是否應(yīng)該被中斷(中斷標(biāo)志是否為true)。它并不像stop方法那樣會(huì)真的會(huì)粗暴地打斷一個(gè)正在運(yùn)行的線程。

從Java的API中可以看到, 有許多聲明拋出InterruptedException的方法(例如Thread.sleep(long millis)方法), 這些方法在拋出InterruptedException之前, Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)識(shí)位清除, 然后拋出InterruptedException, 此時(shí)調(diào)用isInterrupted()方法將會(huì)返回false。

測(cè)試代碼如下,首先創(chuàng)建了兩個(gè)線程, SleepThread和BusyThread, 前者不停地睡眠, 后者一直運(yùn)行, 然后對(duì)這兩個(gè)線程分別進(jìn)行中斷操作, 觀察二者的中斷標(biāo)識(shí)位。

public class Interrupted {

    public static void main(String[ ] args) throws Exception {
        // 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 interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());

        // 防止sleepThread和busyThread立刻退出
        SleepUtils. second(2);

    }

    static class SleepRunner implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils. second(10) ;
            }
        }
    }

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

            }
        }   
    }
}

輸出如下:

SleepThread interrupted is false
BusyThread interrupted is true

從結(jié)果可以看出, 拋出InterruptedException的線程SleepThread, 其中斷標(biāo)識(shí)位被清除了,而一直忙碌運(yùn)作的線程BusyThread, 中斷標(biāo)識(shí)位沒有被清除。

中斷狀態(tài)是線程的一個(gè)標(biāo)識(shí)位, 而中斷操作是一種簡(jiǎn)便的線程間交互方式, 而這種交互方式最適合用來取消或停止任務(wù)。 除了中斷以外, 還可以利用一個(gè)boolean共享變量來控制是否需要停止任務(wù)并終止該線程,這是最受推薦的終止一個(gè)線程(就是讓一個(gè)線程徹底停止運(yùn)行)的方式,使用共享變量(shared variable)來發(fā)出信號(hào),告訴線程必須停止正在運(yùn)行的任務(wù)。線程必須周期性的核查這一變量,然后有秩序地停止任務(wù)。測(cè)試代碼如下:

public class Shutdown {

    public static void main(String[ ] args) throws Exception {
        Runner one = new Runner() ;
        Thread countThread = new Thread(one, "CountThread") ;
        countThread.start() ;

        // 睡眠1秒,main線程對(duì)Runner one進(jìn)行中斷, 使CountThread能夠感知中斷標(biāo)識(shí)位的置位而結(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);
        }

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

輸出結(jié)果如下(多次運(yùn)行結(jié)果可能不同):

Count i = 543487324
Count i = 540898082

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

注意:suspend()、resume()和stop()方法也可以完成線程的暫停、恢復(fù)和終止工作,而且非常“人性化”。但是這些API是過期的,也就是不建議使用的。
不建議使用的原因主要有:以suspend()方法為例,在調(diào)用后,線程不會(huì)釋放已經(jīng)占有的資源(比如鎖),而是占有著資源進(jìn)入睡眠狀態(tài),這樣容易引發(fā)死鎖問題。同樣,stop()方法在終結(jié)一個(gè)線程時(shí)不會(huì)保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機(jī)會(huì),因此會(huì)導(dǎo)致程序可能工作在不確定狀態(tài)下。
正因?yàn)閟uspend()、resume()和stop()方法帶來的副作用,這些方法才被標(biāo)注為不建議使用的過期方法,而暫停和恢復(fù)操作可以用后面提到的等待/通知機(jī)制來替代。

?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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