四. Java并發(fā)基礎知識(二)

Java自誕生開始就明智地選擇了內(nèi)置對多線程的支持挑秉,這使得Java語言相比同一時期的其他語言具有明顯的優(yōu)勢法梯。線程作為操作系統(tǒng)調(diào)度的最小單元,多個線程能夠同時執(zhí)行犀概,這將顯著提升程序性能立哑,在多核環(huán)境中表現(xiàn)得更加明顯。但是姻灶,過多的創(chuàng)建線程和對線程的不當管理也容易造成問題铛绰。本章將著重介紹Java并發(fā)編程的基礎知識,從啟動一個線程到線程間不同的通信方式产喉,最后通過簡單的線程池示例以及應用來串聯(lián)本章介紹的內(nèi)容捂掰。

線程簡介

什么是線程

現(xiàn)代操作系統(tǒng)在運行一個程序時,會為其創(chuàng)建一個進程曾沈。例如这嚣,啟動一個Java程序,操作系統(tǒng)為它創(chuàng)建一個Java進程∪悖現(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程姐帚,也叫輕量級進程(Light Weight Process),在一個進程中可以創(chuàng)建多個線程障涯,這些線程擁有自己的計數(shù)器罐旗,堆棧和局部變量等屬性,并且能夠訪問共享的內(nèi)存變量唯蝶。處理器在這些線程上高速切換九秀,讓使用者感覺這些線程在同時執(zhí)行。

一個Java程序從main()方法開始執(zhí)行粘我,然后按照既定的邏輯代碼執(zhí)行鼓蜒,看似沒有其他線程參與,但實際上Java程序天生就是多線程程序涂滴,因為執(zhí)行main()方法的是一個名稱為main的線程。下面使用JMX來查看一個普通的Java程序包含哪些線程晴音。

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

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

輸出如下所示(輸出內(nèi)容可能不同):

可以看出,一個Java程序的運行并不僅僅是main()方法的運行锤躁,而是main線程和多個其他線程的同時運行搁料。

線程優(yōu)先級

現(xiàn)代操作系統(tǒng)基本采用時分的形式調(diào)度運行的線程,操作系統(tǒng)會分出一個個時間片,線程會分配到若干時間片郭计,當線程的時間片用完就會發(fā)生線程調(diào)度霸琴,并等待下次分配。線程分配到的時間片多少也決定了線程使用處理器資源的多少昭伸,而線程優(yōu)先級就是決定線程需要多或者少分配一些處理器資源的線程屬性梧乘。

在Java線程中,通過一個整型成員變量priority來控制優(yōu)先級庐杨,優(yōu)先級的范圍從1-10选调,在線程構(gòu)建的時候可以通過setPriority(int)方法來修改優(yōu)先級,默認優(yōu)先級為5灵份,優(yōu)先級高的線程分配時間片的數(shù)量要多于優(yōu)先級低的線程仁堪。在設置優(yōu)先級時,針對頻繁阻塞(休眠或IO操作)的線程需要設置較高的優(yōu)先級填渠,而偏重計算的線程則設置較低的優(yōu)先級弦聂,確保處理器不會被獨占。在不同JVM以及操作系統(tǒng)上氛什,線程規(guī)劃會存在差異莺葫,有些操作系統(tǒng)甚至會忽略對線程優(yōu)先級的設定。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Priority {
    private static volatile boolean notStart = true;
    private static volatile boolean notEnd = true;

    public static void main(String[] args) throws Exception {
        List<Job> jobs = new ArrayList<>(16);
        for(int i = 0; i < 10; ++i) {
            int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
            Job job = new Job(priority);
            jobs.add(job);
            Thread thread = new Thread(job, "Thread:" + i);
            thread.setPriority(priority);
            thread.start();
        }
        notStart = false;
        TimeUnit.SECONDS.sleep(10);
        notEnd = false;

        for(Job job : jobs) {
            System.out.println("Job Priority: " + job.priority + ", Count: " + job.jobCount);
        }
    }

    private static class Job implements Runnable {
        private int priority;
        private long jobCount;

        Job(int priority) {
            this.priority = priority;
        }

        @Override
        public void run() {
            while (notStart) {
                Thread.yield();
            }
            while (notEnd) {
                Thread.yield();
                jobCount++;
            }
        }
    }
}

運行該實例屉更,輸出如下:

Job Priority: 1, Count: 328139
Job Priority: 1, Count: 328468
Job Priority: 1, Count: 328122
Job Priority: 1, Count: 328002
Job Priority: 1, Count: 328111
Job Priority: 10, Count: 2763533
Job Priority: 10, Count: 2776199
Job Priority: 10, Count: 2770687
Job Priority: 10, Count: 2776199
Job Priority: 10, Count: 2765352

此結(jié)果是在win10, jdk10下的輸出徙融,可以看出優(yōu)先級生效了。
Job Priority: 1, Count: 3976833
Job Priority: 1, Count: 4058807
Job Priority: 1, Count: 5225471
Job Priority: 1, Count: 4941778
Job Priority: 1, Count: 4190309
Job Priority: 10, Count: 4257979
Job Priority: 10, Count: 3897556
Job Priority: 10, Count: 4107731
Job Priority: 10, Count: 4068744
Job Priority: 10, Count: 4164109

此結(jié)果是在debian9, jdk8下的輸出瑰谜,可以看出優(yōu)先級沒有生效欺冀。

從上面兩個輸出來看,程序正確性不能依賴線程的優(yōu)先級高低萨脑。

線程的狀態(tài)

Java線程在運行的生命周期中可能處于如下所示的6種不同狀態(tài)隐轩,在給定的一個時刻,線程只能處理其中的一個狀態(tài):

狀態(tài)名稱 說明
NEW 初始狀態(tài)渤早,線程被構(gòu)建职车,但還沒有調(diào)用start()方法
RUNNABLE 運行狀態(tài),Java線程將操作系統(tǒng)中的就緒和運行兩種狀態(tài)籠統(tǒng)地稱作“運行中”
BLOCKED 阻塞狀態(tài)鹊杖,表示線程阻塞于鎖
WAITING 等待狀態(tài)悴灵,表示線程進入等待狀態(tài),進入該狀態(tài)表示當前線程需要等待其他線程做一些特定動作(通知或中斷)
TIME_WAITING 超時等待狀態(tài)骂蓖,不同于WAITING积瞒,他可以在指定時間自行返回
TERMINATED 終止狀態(tài),表示當前線程已經(jīng)執(zhí)行完畢

下面我們使用jstack(位于JDK安裝目錄的bin目錄下)工具登下,查看示例代碼運行時的線程信息茫孔,更加深入了解線程狀態(tài)叮喳。

import util.SleepUtils;

public class ThreadState {
    public static void main(String[] args) {
        new Thread(new TimeWaiting(), "TimeWaitingThread").start();
        new Thread(new Waiting(), "WaitingThread").start();
        new Thread(new Blocked(), "BlockedThread-1").start();
        new Thread(new Blocked(), "BlockedThread-2").start();
    }

    private static class TimeWaiting implements Runnable {
        @Override
        public void run() {
            //該線程不斷進行睡眠
            while (true) {
                SleepUtils.sleep(100);
            }
        }
    }

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

    private static class Blocked implements Runnable {
        @Override
        public void run() {
            //該線程在Blocked.class上加鎖后,不會釋放鎖
            synchronized (Blocked.class) {
                while (true) {
                    SleepUtils.sleep(100);
                }
            }
        }
    }
}

------------------------------------------------------------------------

package util;

import java.util.concurrent.TimeUnit;

public class SleepUtils {
    public static void sleep(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運行該實例缰贝,打開終端馍悟,輸入jps,輸出如下:

PS C:\Program Files\Java\jdk-10.0.2\bin> jps
29120 Launcher
42148
49112 Jps
50220 ThreadState

可以看到運行實例對應的進程ID為50220剩晴,接著鍵入 jstack 50220(此處進程ID需要和你自己輸入jps得到的ID一致)锣咒,部分輸出如下:

"TimeWaitingThread" #13 prio=5 os_prio=0 tid=0x00000242c87c9000 nid=0x27d4 waiting on condition [0x000000e411efe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)

"WaitingThread" #14 prio=5 os_prio=0 tid=0x00000242c87cb800 nid=0x7d34 in Object.wait() [0x000000e411fff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@10.0.2/Native Method)
- waiting on <0x00000006d11a0a88> (a java.lang.Class for p4.ThreadState$Waiting)

"BlockedThread-1" #15 prio=5 os_prio=0 tid=0x00000242c87cc000 nid=0xc398 waiting on condition [0x000000e4120fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)

"BlockedThread-2" #16 prio=5 os_prio=0 tid=0x00000242c87cd000 nid=0xa124 waiting for monitor entry [0x000000e4121ff000]
java.lang.Thread.State: BLOCKED (on object monitor)
- waiting to lock <0x00000006d11a1db0> (a java.lang.Class for p4.ThreadState$Blocked)

從上個例子中我們可以了解到,線程在自身的生命周期中并不是固定的處于某個狀態(tài)李破,而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進行切換宠哄,Java線程狀態(tài)變遷如下所示:

Daemon線程

Daemon線程是一種支持型線程,因為它主要被用作程序中后臺調(diào)度以及支持性工具嗤攻。這意味著毛嫉,當Java虛擬機不存在非Daemon線程時,Java虛擬機將會退出妇菱〕性粒可以通過調(diào)用Thread.setDaemon(true)將線程設置為Daemon線程。

Daemon屬性需要在啟動線程前設置闯团,不能在啟動后設置辛臊。

Daemon線程被用作完成支持性工作,但在Java虛擬機退出時Daemon線程中的finally塊不一定會執(zhí)行房交。

public class DaemonTest {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            try {
                SleepUtils.sleep(5);
            } finally {
                System.out.println("DaemonThread finally run.");
            }
        }, "DaemonThread");
        thread.setDaemon(true);
        thread.start();
    }
}

運行此程序彻舰,可以看到?jīng)]有任何輸出。main線程(非Daemon線程)在啟動了DaemonRunner線程后main方法執(zhí)行完畢候味,main線程終止刃唤,因此Java虛擬機中已沒有非Daemon線程,虛擬機退出白群。Java虛擬機中所有Daemon線程都需要立即終止尚胞,因此DaemonRunner立即停止,finally塊并未執(zhí)行帜慢。

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

線程間通信

線程開始運行粱玲,擁有自己的椆恚空間,就如同一個腳本一樣抽减,按照既定的代碼一步一步執(zhí)行允青,直到終止。但是胯甩,每個運行中的線程昧廷,如果僅僅孤立地運行,那么沒有什么太大的價值偎箫,如果多個線程能夠相互配合完成工作木柬,這將帶來巨大的價值。

volatile和synchronized關鍵字

Java支持多個線程同時訪問一個對象或?qū)ο蟮某蓡T變量淹办,由于每個線程可以擁有這個變量的拷貝(雖然對象以及其成員變量在共享內(nèi)存中分配內(nèi)存眉枕,但是每個執(zhí)行的線程還是可以擁有一份拷貝,加速程序的執(zhí)行怜森,這是現(xiàn)代多核處理器的一個顯著特性)速挑,所以程序在執(zhí)行過程中,一個線程看到的變量并不一定是最新的副硅。

關鍵詞volatile可以用來修飾字段(成員變量)姥宝,就是告知程序任何對該變量的訪問均需要從共享內(nèi)存中獲取,而對他的改變必須同步刷新回共享內(nèi)存恐疲,他能保證所有線程對變量訪問的可見性腊满。但是,過多的使用volatille是不必要的培己,因為它會降低程序執(zhí)行的效率碳蛋。

關鍵詞synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻省咨,只能有一個線程處于方法或同步塊中肃弟,他保證了線程對變量訪問的排他性和可見性。

如下的代碼使用了同步塊和同步方法零蓉,通過javap工具查看生成的class文件信息來分析synchronized關鍵字的實現(xiàn)細節(jié)笤受。

public class Synchronized {
    public static void main(String[] args) {
        synchronized (Synchronized.class) {
        }
        m();
    }

    private static synchronized void m() {

    }
}

鍵入javap -v Synchronized.class

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class p4/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

在上面的class信息中,對于同步塊實現(xiàn)使用了monitorentermonitorexit指令壁公,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED(上面未出現(xiàn))來完成的感论。無論采取哪種方式,其本質(zhì)是對一個對象的監(jiān)視器進行獲取紊册,這個獲取的過程是排他的比肄,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監(jiān)視器。

任意一個對象都擁有自己的監(jiān)視器囊陡,當這個對象由同步塊或者這個對象的同步方法調(diào)用時芳绩,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進入同步塊或者同步方法,而沒有獲取到監(jiān)視器的線程將會阻塞在同步塊和同步方法入口處撞反,進入BLOCKED狀態(tài)妥色。

下圖描述了對象,監(jiān)視器遏片,同步隊列和執(zhí)行線程之間的關系嘹害。

從圖中可以看出撮竿,任意線程對Object(Object由synchronized保護)的訪問,首先要獲取Object的監(jiān)視器笔呀。如果獲取失敗幢踏,線程進入同步隊列,線程狀態(tài)變?yōu)?code>BLOCKED许师。當訪問Object的前驅(qū)線程(獲得了鎖的線程)釋放了鎖房蝉,則該操作會喚醒阻塞在同步隊列中的線程,使其重新嘗試對監(jiān)視器的獲取微渠。

等待/通知機制

一個線程修改了一個對象的值搭幻,而另一個線程感知到了變化,然后進行相應的操作逞盆,整個過程開始于一個線程檀蹋,而最終執(zhí)行又是另一個線程。前者是生產(chǎn)者云芦,后者就是消費者续扔,這種模式隔離了“做什么”和“怎么做”,在功能層面實現(xiàn)了解耦焕数,體系結(jié)構(gòu)上具備了良好了伸縮性纱昧,但在Java語言中如何實現(xiàn)類似的功能呢?

簡單的方法是讓消費者線程不斷循環(huán)檢查變量是否符合預期堡赔,如下面代碼所示识脆,在while循環(huán)中設置不滿足的條件,如果條件滿足則退出while循環(huán)善已,從而完成消費者的工作灼捂。

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

上面這段偽代碼在條件不滿足時就睡眠一段時間,這樣做的目的是防止過快地“無效”嘗試换团,這種方式看似能夠?qū)崿F(xiàn)所需的功能悉稠,但是卻存在以下問題:

  1. 難以確保及時性。在睡眠時艘包,基本不消耗處理器資源的猛,但是如果睡的太久,就不能及時發(fā)現(xiàn)條件已經(jīng)變化想虎,也就是及時性難以保證卦尊。
  2. 難以降低開銷。如果降低睡眠的時間舌厨,比如休眠1ms岂却,這樣消費者就能更快速的發(fā)現(xiàn)條件變化,但也消耗了更多的處理器資源,造成了無端的浪費躏哩。

以上兩個問題看似矛盾難以調(diào)和署浩,但Java通過內(nèi)置的等待/通知機制能夠很好的解決這個矛盾并實現(xiàn)所需的功能。等待/通知的相關方法是任意Java對象都具備的扫尺,因為這些方法被定義在所有對象的超類java.lang.Object上瑰抵。

  • notify()
  • notifyAll()
  • wait()
  • wait(long)
  • wait(long, int)

等待/通知機制,是指一個線程A調(diào)用了對象O的wait()方法進入等待狀態(tài)器联,而另一個線程B調(diào)用了對象O的notify()方法或者notifyAll()方法,線程A收到通知后從對象的wait()方法返回婿崭,進而執(zhí)行后續(xù)操作拨拓。上面兩個線程通過對象O實現(xiàn)交互,而對象上的wait()和notify()/notifyAll()的關系就如同開關信號一樣氓栈,用來完成等待方和通知方之間的交互工作渣磷。

下面代碼創(chuàng)建了兩個線程——WaitThread和NotifyThread,前者檢查flag是否為false授瘦,如果是就進行后續(xù)操作醋界,否則在LOCK上等待,后者睡眠一段時間后對LOCK進行通知提完。

public class WaitNotifyTest {
    private static boolean flag = true;
    private static final Object LOCK = new Object();

    @Test
    public void test() {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();

        SleepUtils.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();

        //使用JUnit測試時形纺,如果test線程結(jié)束,其他線程也會結(jié)束徒欣,所以等待waitThread和notifyThread執(zhí)行
        SleepUtils.sleep(10);
    }

    private static class Wait implements Runnable {
        @Override
        public void run() {
            synchronized (LOCK) {
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread() + " flag is true. wait @ " + new Date());
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread() + " flag is false. running @ " + new Date());
            }
        }
    }

    private static class Notify implements Runnable {
        @Override
        public void run() {
            synchronized (LOCK) {
                System.out.println(Thread.currentThread() + " hold lock. notify @ " + new Date());
                flag = false;
                LOCK.notifyAll();
                SleepUtils.sleep(5);
            }
            synchronized (LOCK) {
                System.out.println(Thread.currentThread() + " hold lock again. sleep @ " + new Date());
                SleepUtils.sleep(5);
            }
        }
    }
}

輸出如下:

Thread[WaitThread,5,main] flag is true. wait @ Wed Dec 26 16:02:03 CST 2018
Thread[NotifyThread,5,main] hold lock. notify @ Wed Dec 26 16:02:04 CST 2018
Thread[WaitThread,5,main] flag is false. running @ Wed Dec 26 16:02:09 CST 2018
Thread[NotifyThread,5,main] hold lock again. sleep @ Wed Dec 26 16:02:09 CST 2018

第三行和第四行的輸出可能會互換逐样,上述例子主要說明了調(diào)用wait()和notify()/notifyAll()時需要注意的細節(jié):

  1. 使用wait(), notify()以及notifyAll()時需要先對調(diào)用對象加鎖
  2. 調(diào)用wait()方法后,線程狀態(tài)從RUNNING變?yōu)?code>WAITING打肝,并將線程放置到對象的等待隊列
  3. notify()或notifyAll()方法調(diào)用后脂新,等待線程依然不會從wait()返回,需要調(diào)用notify()或notifyAll()的線程釋放鎖之后粗梭,等待線程才有機會從wait()返回
  4. notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中争便,而notifyAll()方法則將等待隊列中所有的線程都移到同步隊列,被移動的線程狀態(tài)從WAITING變?yōu)?code>BLOCKED
  5. 從wait()方法返回的前提是獲得了調(diào)用對象的鎖

從上面細節(jié)可以看出断医,等待/通知機制依托于同步機制滞乙,其目的就是確保等待線程從wait()方法返回時能夠感知到通知線程對變量做出的修改。

下圖描述了上述示例的過程:

WaitThread首先獲取了對象的鎖鉴嗤,然后調(diào)用對象的wait()方法酷宵,從而放棄鎖進入對象的等待隊列中,進入等待狀態(tài)躬窜。由于WaitThread釋放了對象的鎖浇垦,NotifyThread便能獲取對象的鎖,然后調(diào)用對象的notify()方法荣挨,將WaitThread從等待隊列轉(zhuǎn)移到同步隊列男韧。NotifyThread釋放鎖后朴摊,WaitThread再次獲取鎖并從wait()方法返回繼續(xù)執(zhí)行。

等待/通知的經(jīng)典范式

從前面的示例中可以提煉出等待/通知的經(jīng)典范式此虑,分為兩部分甚纲,分別針對等待方(消費者)和通知方(生產(chǎn)者)。

等待方:

  1. 獲取對象的鎖
  2. 如果條件不滿足朦前,調(diào)用對象的wait()方法介杆,被通知后仍要檢查條件
  3. 條件滿足執(zhí)行對應邏輯
synchronized(對象) {
    while(條件不滿足) {
        對象.wait();
    }
    對應邏輯
}

通知方:

  1. 獲取對象的鎖
  2. 改變條件
  3. 通知所有等待在對象上的線程
synchronized(對象) {
    改變條件
    對象.notifyAll();
}

管道輸入/輸出流

管道輸入/輸出流和普通文件的輸入/輸出流或者網(wǎng)絡輸入/輸出流的不同之處在于,它主要用于線程之間的數(shù)據(jù)傳輸韭寸,傳輸媒介為內(nèi)存春哨。

管道輸入/輸出流主要包括了4種具體實現(xiàn):PipedOutputStreamPipedInputStream恩伺,PipedReader赴背,PipedWriter,前兩種面向字節(jié)晶渠,后兩種面向字符凰荚。下面是一個簡單的例子:

/**
 * 管道輸入主要用于線程之間的數(shù)據(jù)傳輸
 */
public class Piped {
    public static void main(String[] args) throws Exception {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        //將輸入輸出流進行連接,否則使用時會拋出IOException
        writer.connect(reader);

        Thread printThread = new Thread(new Print(reader), "PrintThread");
        printThread.start();
        int receive;
        try {
            while ((receive = System.in.read()) != -1) {
                writer.write(receive);
            }
        } finally {
            writer.close();
        }
    }


    private static class Print implements Runnable {
        private PipedReader reader;

        Print(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            int receive;
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

結(jié)果如下褒脯,輸入一組字符串便瑟,被原樣輸出:

13465
13465

Thread.join()的使用

如果一個線程A執(zhí)行了thread.join()語句,其含義是:當前線程A等待thread線程終止之后才從thread.join()返回番川。線程Thread除了提供join()方法外胳徽,還提供了join(long mills)和join(long mills, int nanos)兩個具備超時性質(zhì)的方法。這兩個方法表示爽彤,如果線程thread在給定的時間內(nèi)沒有終止养盗,那么將會從該超時方法中返回。

下面這個例子在前面已經(jīng)提及過:

import util.SleepUtils;

public class Join {
    public static void main(String[] args) throws Exception {
        Thread previous = Thread.currentThread();
        for(int i=0;i<10;++i) {
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous=thread;
        }
        SleepUtils.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    private static class Domino implements Runnable {
        private Thread thread;
        Domino(Thread thread) {
            this.thread=thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

輸出如下:

main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.

從上述輸出可以看出适篙,每個線程終止的前提是前驅(qū)線程的終止往核,每個線程等待前驅(qū)線程終止后,才從join()方法返回嚷节,這里其實也涉及到了等待/通知機制聂儒。

ThreadLocal的使用

ThreadLocal即線程變量,是一個以ThreadLocal對象為鍵硫痰,任意對象為值的存儲結(jié)構(gòu)衩婚。這個結(jié)構(gòu)被附帶在線程上,也就是一個線程可以根據(jù)一個ThreadLocal對象查詢到綁定在這個線程上的一個值效斑。

Java中的ThreadLocal類使您能夠創(chuàng)建只能由同一線程讀取和寫入的變量非春。因此,即使兩個線程正在執(zhí)行相同的代碼,并且代碼具有對ThreadLocal變量的引用 奇昙,那么這兩個線程也看不到彼此的ThreadLocal變量护侮。

創(chuàng)建ThreadLocal

這是一個代碼示例,演示如何創(chuàng)建 ThreadLocal 變量:

private ThreadLocal myThreadLocal = new ThreadLocal();

如你所見储耐,你實例化了一個新 ThreadLocal 對象羊初,這只需要每個線程完成一次。即使不同的線程執(zhí)行相同獲取 ThreadLocal 的代碼什湘,每個線程也只能看到自己的 ThreadLocal 實例长赞。即使兩個不同的線程在同一個 ThreadLocal 對象上設置不同的值,它們也看不到彼此的值闽撤。

訪問ThreadLocal

一旦 ThreadLocal 被創(chuàng)建得哆,你可以像這樣設置值:

myThreadLocal.set("一個線程本地值");

你可以這樣讀取存儲在 ThreadLocal 內(nèi)容中的值:

String threadLocalValue = (String) myThreadLocal.get();

get() 方法返回一個Object,該 set() 方法采用Object作為參數(shù)腹尖。

通用ThreadLocal

您可以創(chuàng)建一個泛型 ThreadLocal 這樣就不必對 get() 返回的值進行類型轉(zhuǎn)換 。這是一個通用的 ThreadLocal 例子:

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

現(xiàn)在伐脖,你只能在該 ThreadLocal 實例中存儲字符串热幔。此外,你也不需要對從該 ThreadLocal 獲得的值進行類型轉(zhuǎn)換:

myThreadLocal.set(“Hello ThreadLocal”);
String threadLocalValue = myThreadLocal.get();

初始ThreadLocal值

由于在 ThreadLocal 對象上設置的值僅對設置值的線程可見讼庇,因此沒有線程可以在 ThreadLocal 上使用 set() 設置初始值绎巨,使其對所有線程可見。

相反蠕啄,您可以通過子類化 ThreadLocal 和重寫 initialValue() 方法為 ThreadLocal 對象指定初始值场勤。這是看起來如何使用的:

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override protected String initialValue() {
        return "This is the initial value";
    }
}; 

現(xiàn)在所有線程在調(diào)用set()之前調(diào)用get()都會看到相同的初始值。

InheritableThreadLocal

InheritableThreadLocal 類是 ThreadLocal 的子類歼跟。比起每個線程在 ThreadLocal 中都有自己的值和媳, InheritableThreadLocal 的值對創(chuàng)建它的線程以及該線程創(chuàng)建的所有子線程來說都能訪問芒篷。

下面是一個ThreadLocal完整的例子:

public class Profiler {
    private static final ThreadLocal<Long> TIME_THREAD_LOCAL = ThreadLocal.withInitial(System::currentTimeMillis);

    public static void begin() {
        TIME_THREAD_LOCAL.set(System.currentTimeMillis());
    }

    public static long end() {
        return System.currentTimeMillis()-TIME_THREAD_LOCAL.get();
    }

    public static void main(String[] args) throws Exception {
        Profiler.begin();
        SleepUtils.sleep(1);
        System.out.println("Cost: " + Profiler.end() + " mills");

        //result:1546423458054
        //       main: 1546423457053
        //可以看出ThreadLocal變量是依附于一個線程的都许,不像普通變量被共享
        begin();
        Thread thread1 = new Thread(() -> System.out.println(TIME_THREAD_LOCAL.get()));
        thread1.start();
        System.out.println("main: " + TIME_THREAD_LOCAL.get());
    }
}

輸出如下:

Cost: 1002 mills
1546423458054
main: 1546423457053

此類可以被復用在耗時統(tǒng)計的功能上蘸鲸,在方法入口執(zhí)行begin()方法寓调,方法調(diào)用后執(zhí)行end()方法拢军,好處是這兩個方法的調(diào)用不需要在同一個類或方法中往声,比如在AOP編程中柠座,可以在方法調(diào)用前的切入點執(zhí)行begin()方法铅祸,而在方法調(diào)用后的切入點執(zhí)行end()方法作箍,這樣依舊可以獲得方法的執(zhí)行耗時硬梁。

下面是ThreadLocal幾個常用方法的源碼:

/**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市胞得,隨后出現(xiàn)的幾起案子荧止,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,207評論 6 521
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罩息,死亡現(xiàn)場離奇詭異嗤详,居然都是意外死亡,警方通過查閱死者的電腦和手機瓷炮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,455評論 3 400
  • 文/潘曉璐 我一進店門葱色,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娘香,你說我怎么就攤上這事苍狰。” “怎么了烘绽?”我有些...
    開封第一講書人閱讀 170,031評論 0 366
  • 文/不壞的土叔 我叫張陵淋昭,是天一觀的道長。 經(jīng)常有香客問我安接,道長翔忽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,334評論 1 300
  • 正文 為了忘掉前任盏檐,我火速辦了婚禮歇式,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胡野。我一直安慰自己材失,他們只是感情好,可當我...
    茶點故事閱讀 69,322評論 6 398
  • 文/花漫 我一把揭開白布硫豆。 她就那樣靜靜地躺著龙巨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熊响。 梳的紋絲不亂的頭發(fā)上旨别,一...
    開封第一講書人閱讀 52,895評論 1 314
  • 那天,我揣著相機與錄音汗茄,去河邊找鬼昼榛。 笑死,一個胖子當著我的面吹牛剔难,可吹牛的內(nèi)容都是我干的胆屿。 我是一名探鬼主播,決...
    沈念sama閱讀 41,300評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼偶宫,長吁一口氣:“原來是場噩夢啊……” “哼非迹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纯趋,我...
    開封第一講書人閱讀 40,264評論 0 277
  • 序言:老撾萬榮一對情侶失蹤憎兽,失蹤者是張志新(化名)和其女友劉穎冷离,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纯命,經(jīng)...
    沈念sama閱讀 46,784評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡西剥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,870評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了亿汞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞭空。...
    茶點故事閱讀 40,989評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖疗我,靈堂內(nèi)的尸體忽然破棺而出咆畏,到底是詐尸還是另有隱情,我是刑警寧澤吴裤,帶...
    沈念sama閱讀 36,649評論 5 351
  • 正文 年R本政府宣布旧找,位于F島的核電站,受9級特大地震影響麦牺,放射性物質(zhì)發(fā)生泄漏钮蛛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,331評論 3 336
  • 文/蒙蒙 一剖膳、第九天 我趴在偏房一處隱蔽的房頂上張望魏颓。 院中可真熱鬧,春花似錦潮秘、人聲如沸琼开。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,814評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搞动,卻和暖如春躏精,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹦肿。 一陣腳步聲響...
    開封第一講書人閱讀 33,940評論 1 275
  • 我被黑心中介騙來泰國打工矗烛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人箩溃。 一個月前我還...
    沈念sama閱讀 49,452評論 3 379
  • 正文 我出身青樓瞭吃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涣旨。 傳聞我的和親對象是個殘疾皇子歪架,可洞房花燭夜當晚...
    茶點故事閱讀 45,995評論 2 361

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

  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內(nèi)存運行時,即變成一個進程.進程是處于運行過程中...
    勝浩_ae28閱讀 5,120評論 0 23
  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內(nèi)存運行時,即變成一個進程.進程是處于運行過程中...
    小徐andorid閱讀 2,814評論 3 53
  • 1. cpu通過時間片分配算法來循環(huán)執(zhí)行任務,當前任務執(zhí)行一個時間片后會切換到下一任務霹陡。但是和蚪,再切換之前會保存上一...
    冰與河豚魚閱讀 671評論 0 0
  • 一止状、基本寫法 a.上面定義了一個類,包含著一個構(gòu)造函數(shù)和一個say方法攒霹。構(gòu)造函數(shù)內(nèi)的this指向?qū)嵗龑ο笄影蹋以摵瘮?shù)...
    www_ye閱讀 206評論 0 0
  • 據(jù)科學研究,養(yǎng)成一個習慣的平均時間是66天催束,而不同的習慣養(yǎng)成的時間相差很大集峦,從18天到254天不等。 很多人都喜歡...
    纖陌顏閱讀 478評論 0 3