Java多線程編程核心技術(三)多線程通訊

線程是操做系統中獨立的個體,但這些個體若是不通過特殊的處理就不能成爲一個總體。線程間的通訊就是成爲總體的必用方案之一,能夠說,使線程間進行通訊後,系統之間的交互性會更強大,在大大提升CPU利用率的同時還會使程序員對各線程任務在處理的過程當中進行有效的把控與監督。html

在本章中須要着重掌握的技術點以下:java

  • 使用wait/notify實現線程間的通訊
  • 生產者/消費者模式的實現
  • 方法join的使用
  • ThreadLocal類的使用

1.等待 / 通知機制

經過本節能夠學習到,線程與線程之間不是獨立的個體,它們彼此之間能夠互相通訊和協做。程序員

1.1 不使用等待 / 通知機制實現線程間通訊

下面的示例,是sleep()結合while(true)死循環來實現多個線程間通訊。算法

public class MyService {
    volatile private List<Integer> list = new ArrayList<>();
    public void add(){
        list.add(1);
    }
    public int size(){
        return list.size();
    }

    public static void main(String[] args) {
        MyService myService = new MyService();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i= 0;i<10;i++) {
                    myService.add();
                    System.out.println("添加了"+myService.size()+"個元素");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true){
                        if (myService.size() == 5){
                            System.out.println(" == 5 ,我要退出了");
                            throw new InterruptedException();
                        }

                    }
                } catch (InterruptedException e) {
                    System.out.println(myService.size());
                    e.printStackTrace();
                }
            }
        }).start();

    }
}

打印結果:編程

添加了1個元素
添加了2個元素
添加了3個元素
添加了4個元素
添加了5個元素
 == 5 ,我要退出了
5
java.lang.InterruptedException
    at cn.zyzpp.thread3_1.MyService$2.run(MyService.java:42)
    at java.lang.Thread.run(Thread.java:745)
添加了6個元素
添加了7個元素
添加了8個元素
添加了9個元素
添加了10個元素

雖然兩個線程間實現了通訊,但有一個弊端就是,線程ThreadB.java不停地經過while語句輪詢機制來檢測某一個條件,這樣會浪費CPU資源。若是輪詢的時間間隔很小,更浪費CPU資源;若是輪詢的時間間隔很大,有可能會取不到想要獲得的數據。因此就須要一種機制來實現減小CPU的資源浪費,並且還能夠實如今多個線程間通訊,它就是「wait / notify」機制。數組

1.2 什麼是等待 / 通知機制

等待 / 通知機制在生活中比比皆是,好比你去餐廳點餐,服務員去取菜,菜暫時尚未作出來,這時候服務員就進入」等待「的狀態,等到廚師把菜放在菜品傳遞臺上,其實就至關於一種」通知「,這時服務員才能夠拿到菜並交給就餐者。多線程

須要說明的是,上節多個線程間也能夠實現通訊,緣由是多個線程共同訪問同一個變量,但那種通訊不是「等待/通知」,兩個線程徹底是主動式地讀取一個共享變量,在花費讀取時間的基礎上,讀到的值是否是想要的,並不能徹底肯定。因此如今迫切須要一種「等待 / 通知」機制來知足上面的要求。併發

1.3 等待 / 通知機制的實現

方法 wait() 的做用是使當前執行代碼的線程進行等待,wait()方法是object類的方法,該方法用來將當前線程置於「預執行隊列」中,而且在wait()所在的代碼行處中止執行,直到接到通知或被中斷爲止。在調用wait()方法以前,線程必須拿到該對象的對象級別鎖。在從wait()返回前,線程與其餘線程競爭從新得到鎖。若是調用wait()時沒有持有適當的鎖,則拋出 java.lang.IllegalMonitorStateException 異常,它是RuntimeException 的一個子類,所以,不須要try-catch語句進行捕捉異常。ide

方法notify()也要在同步方法或同步塊中調用,即在調用前,線程也必須得到該對象的對象級別鎖。若是調用notify時沒有適當的鎖,也會拋出 java.lang.IllegalMonitorStateException 異常。該方法用來通知那些可能等待該對象的對象鎖的其餘線程,若是有多個線程等待,則由線程規劃器隨機挑選出其中一個呈 wait 狀態的線程,對其發出通知 notify,並使它等待獲取該對象的對象鎖。須要說明的是,在執行 notify 方法後,當前線程不會立刻釋放該對象鎖,呈 wait 狀態的線程也並不能立刻獲取該對象鎖,要等到執行 notify() 方法的線程將程序執行完,也就是退出 synchronized 代碼塊後,當前線程纔會釋放鎖,而呈wait狀態所在的線程才能夠獲取該對象鎖。當第一個得到了該對象鎖的 wait 線程運行完畢之後,它會釋放掉該對象鎖,此時若是該對象沒有再次使用 notify 語句,則該對象以及空閒,其它 wait 狀態等待的線程因爲沒有獲得該對象的通知,還會繼續阻塞在 wait 狀態,知道直到這個對象發出一個 notify 或 notifyAll。學習

用一句話來總結一下 wait 和 notify :wait 使線程中止運行,而 notify 使中止的線程繼續運行

示例代碼:

public class MyServiceTwo extends Thread {
    private Object lock;
    
    public MyServiceTwo(Object object) {
        this.lock = object;
    }
    
    @Override
    public void run() {
        try {
            synchronized (lock){
                System.out.println("開始等待"+System.currentTimeMillis());
                lock.wait();
                System.out.println("結束等待"+System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

public class MyServiceThree extends Thread {
    private Object lock;

    public MyServiceThree(Object object) {
        this.lock = object;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("開始通知" + System.currentTimeMillis());
            lock.notify();
            System.out.println("結束通知" + System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyServiceTwo serviceTwo = new MyServiceTwo(lock);
        serviceTwo.start();
        Thread.sleep(100);
        MyServiceThree serviceThree = new MyServiceThree(lock);
        serviceThree.start();
    }

}

打印結果:

開始等待1537185132949
開始通知1537185133048
結束通知1537185133048
結束等待1537185133048

從控制檯的打印來看,100ms後線程被 notify 通知喚醒。

下面咱們使用 wait / notify 來實現剛開始的實驗:

public class MyService {
    volatile private List<Integer> list = new ArrayList<>();

    public void add() {
        list.add(1);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyService myService = new MyService();
        Object lock = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (lock) {
                        if (myService.size() != 5) {
                            System.out.println("等待 "+System.currentTimeMillis());
                            lock.wait();
                            System.out.println("等待結束 "+System.currentTimeMillis());
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 10; i++) {
                        if (myService.size() == 5){
                            lock.notify();
                            System.out.println("已發出通知!");
                        }
                        myService.add();
                        System.out.println("添加了" + myService.size() + "個元素");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

    }
}

打印結果:

等待 1537186277023
添加了1個元素
添加了2個元素
添加了3個元素
添加了4個元素
添加了5個元素
已發出通知!
添加了6個元素
添加了7個元素
添加了8個元素
添加了9個元素
添加了10個元素
等待結束 1537186287034

日誌信息 wait end 在最後輸出,這也說明 notify 方法執行後並不當即釋放鎖。

關鍵字 synchronized 能夠將任何一個 Object 對象做爲同步對象來看待,而 Java 爲每一個 Object 都實現了 wait 和 notify 方法,它們必須用在被 synchronized 同步的 object 的臨界區內。經過調用 wait() 方法可使處於臨界區內的線程進入等待狀態,同時釋放被同步對象對象的鎖。而 notify 操做能夠喚醒一個因調用了 wait 操做而處於阻塞狀態中的線程,使其進入就緒狀態。被從新換醒的線程會試圖從新得到臨界區的控制權,也就是鎖,並繼續執行臨界區內 wait 以後的代碼。若是發出 notify 操做時沒有處於阻塞狀態中的線程,那麼該命令會被忽略。

wait 方法可使調用該方法的線程釋放共享資源的鎖,而後從運行狀態退出,進入等待隊列,直到被再次喚醒。

notify 方法能夠隨機喚醒等待隊列中等待同一共享資源的「一個」線程,並使該線程退出等待隊列,進入可運行狀態,也就是 notify() 方法僅通知「一個」線程。

notifyAll() 方法可使全部正在等待隊列中等待同一共享資源的「所有」線程從等待狀態退出,進入可運行狀態。並使該線程退出等待隊列,進入可運行狀態。此時,優先級最高的那個線程最早執行,但也有多是隨機執行,由於這要取決於JVM虛擬機的實現。

在《Java多線程編程核心技術(一)Java多線程技能》中,已經介紹了與Thread有關的大部分 API ,這些 API 能夠改變線程對象的狀態。

  1. 新建立一個新的線程對象後,再調用它的 start() 方法,系統會爲此線程分配CPU資源,使其處於 Runnable(可運行)狀態,這是一個準備運行的階段。若是線程搶佔到CPU資源,此線程就處於 Running(運行)狀態。

  2. Runnable 狀態和 Running 狀態可相互切換,由於有可能線程運行一段時間後,有其餘高優先級的線程搶佔了CPU資源,這時此線程就從 Running 狀態變成 Runnable 狀態。

    線程進入Runable 狀態大致分爲以下3中狀況:

    • 調用 sleep方法後通過的時間超過了指定的休眠時間。
    • 線程調用的阻塞IO已經返回,阻塞方法執行完畢。
    • 線程成功地得到了試圖同步的監視器。
    • 線程正在等待某個通知,其餘線程發出了通知。
    • 處於掛起狀態的線程調用了 resurne恢復方法。
  3. Blocked是阻寒的意思, 例如遇到了一個IO操做, 此時CPU處於空閒狀態, 可能會轉而把CPU時間片分配給其餘線程, 這時也能夠稱爲「暫停」狀態。 Blocked 狀態結束後,進入 Runnable狀態, 等待系統從新分配資源。

    出現阻塞的狀況大致分爲以下5種:

    • 線程調用 sleep方法, 主動放棄佔用的處理器資源。
    • 線程調用了阻塞式IO方法,在該方法返回前,該線程被阻塞。
    • 線程試圖得到一個同步監視器,但該同步監視器正被其餘線程所持有。
    • 線程等待某個通知。
    • 程序調用了 suspend方法將該線程掛起。此方法容易致使死鎖,儘可能避免使用該方法。
  4. main() 方法運行結束後進人銷燬階段,整個線程執行完畢。

每一個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列。就緒隊列存儲了將要得到鎖的線程,阻塞隊列存儲了被阻塞的線程。一個線程被喚醒後,纔會進入就緒隊列,等待CPU的調度;反之,一個線程被 wait 後,就會進入阻塞隊列,等待下一次被喚醒。

1.4 方法wait()鎖釋放與notify()鎖不釋放

當方法 wait() 被執行後,鎖自動釋放,但執行完 notify() 方法,鎖卻不自動釋放。

1.5 當interrupt方法遇到wait方法

當線程呈 wait() 方法時,調用線程對象的 interrupt() 方法會出現 InterruptedException 異常。

下面咱們作一個實驗:

public class MyServiceTwo extends Thread {
    private Object lock;

    public MyServiceTwo(Object object) {
        this.lock = object;
    }


    @Override
    public void run() {
        try {
            synchronized (lock){
                System.out.println("開始等待"+System.currentTimeMillis());
                lock.wait();
                System.out.println("結束等待"+System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("出現異常了");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyServiceTwo service = new MyServiceTwo(lock);
        service.start();
        Thread.sleep(5000);
        service.interrupt();
    }
    
}

運行結果:

開始等待1537194007598
java.lang.InterruptedException
出現異常了
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at cn.zyzpp.thread3_1.MyServiceTwo.run(MyServiceTwo.java:19)

經過上面的實驗能夠總結以下三點:

  1. 執行完同步代碼塊就會釋放對象的鎖。
  2. 在執行同步代碼塊的過程當中,遇到異常而致使線程終止,鎖也會被釋放。
  3. 在執行同步代碼塊的過程當中,執行了鎖所屬對象的 wait() 方法,這個線程會釋放對象鎖,而此線程對象會進入線程等待池中,等待被喚醒。

1.6 notify()和notifyAll()

調用方法 notify() 一次只隨機通知一個線程進行喚醒。

當屢次調用 notify() 方法會隨機將等待 wait 狀態的線程進行喚醒。

notifyAll() 方法會喚醒所有線程。

1.7 方法 wait(long) 的使用

帶一個參數的 wait(long) 方法的功能是等待某一時間內是否有線程對鎖進行喚醒,若是超過這個時間則自動喚醒。

1.8 等待/通知之交叉備份

假設咱們建立了20個線程,咱們須要這20個線程的運行效果變成有序的,咱們能夠在 等待 / 通知的基礎上,利用以下代碼做爲標記:

volatile private boolean prevIsA = false;

再使用while()循環:

while(prevIsA){
    wait();
}

實現交替打印。

2.生產者 / 消費者模式

等待 / 通知模式最經典的案列就是」生產者 / 消費者「模式。但此模式在使用上有幾種」變形「,還有一些小的注意事項,但原理都是基於 wait/notify 的。

1.一輩子產與一消費:操做值

生產者:

public class P {
    private String lock;

    public P(String lock) {
        super();
        this.lock = lock;
    }

    public void setValue(){
        try {
            synchronized (lock){
                if (!ValueObject.value.equals("")){
                    lock.wait();
                }
                String value = System.currentTimeMillis() + "_" + System.nanoTime();
                System.out.println("set的值是 "+value);
                ValueObject.value =  value;
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

消費者:

public class C {
    private String lock;

    public C(String lock) {
        super();
        this.lock = lock;
    }

    public void getVlue() {
        try {
            synchronized (lock) {
                if (ValueObject.value.equals("")) {
                    lock.wait();
                }
                System.out.println("get的值是 " + ValueObject.value);
                ValueObject.value = "";
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

操做值:

public class ValueObject {
    public static String value = "";
}

main方法:

public class Run {
    public static void main(String[] args) {
        String lock = new String();
        P p = new P(lock);
        C c = new C(lock);
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    p.setValue();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    c.getVlue();
                }
            }
        }).start();
    }
}

打印結果:

set的值是 1537253968947_1379616037064493
get的值是 1537253968947_1379616037064493
set的值是 1537253968947_1379616037099625
get的值是 1537253968947_1379616037099625
set的值是 1537253968947_1379616037136730
get的值是 1537253968947_1379616037136730
set的值是 1537253968947_1379616037173047
.....

本實例是1個生產者與消費者進行數據的交互,在控制檯中打印的日誌get和set是交替運行的。

但若是在此實驗的基礎上,設計出多個生產者與消費者,那麼在運行的過程當中極有可能出現「假死」的狀況,也就是全部的線程都呈 WAITING 等待狀態。

2.多生產與多消費:操做值

生產者:

public class P {
    private String lock;

    public P(String lock) {
        super();
        this.lock = lock;
    }

    public void setValue(){
        try {
            synchronized (lock){
                while (!ValueObject.value.equals("")){
                    System.out.println("生產者"+Thread.currentThread().getName()+"WAITING");
                    lock.wait();
                }
                String value = System.currentTimeMillis() + "_" + System.nanoTime();
                System.out.println("生產者"+Thread.currentThread().getName()+"set的值是 "+value);
                ValueObject.value =  value;
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

消費者:

public class C {
    private String lock;

    public C(String lock) {
        super();
        this.lock = lock;
    }

    public void getVlue() {
        try {
            synchronized (lock) {
                while (ValueObject.value.equals("")) {
                    System.out.println("消費者"+Thread.currentThread().getName()+"WAITING");
                    lock.wait();
                }
                System.out.println("消費者"+Thread.currentThread().getName()+"get的值是 " + ValueObject.value);
                ValueObject.value = "";
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

操做值:

public class ValueObject {
    public static String value = "";
}

main方法:

public class Run {
    public static void main(String[] args) {
        String lock = new String();
        P p = new P(lock);
        C c = new C(lock);
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        p.setValue();
                    }
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        c.getVlue();
                    }
                }
            }).start();
        }
    }
}

運行結果:

...
消費者Thread-1WAITING
消費者Thread-3WAITING
生產者Thread-0set的值是 1537255325047_1380972136738280
生產者Thread-0WAITING
消費者Thread-1get的值是 1537255325047_1380972136738280
消費者Thread-1WAITING
消費者Thread-3WAITING
生產者Thread-2set的值是 1537255325048_1380972137330390
生產者Thread-2WAITING
生產者Thread-0WAITING

運行結果顯示,最後全部的線程都呈WAITING狀態。爲何會出現這樣的狀況呢?在代碼中已經 wait/notify 啊?

在代碼中確實已經經過 wait / notify 進行呈通訊了,但不保證 notify 喚醒的是異類,也許是同類,好比「生產者」喚醒「生產者」,或「消費者」喚醒「消費者」這樣的狀況。若是按這樣狀況運行的比率聚沙成塔,就會致使全部的線程都不能繼續運行下去,你們都在等待,都呈 WAITING 狀態,程序最後也就呈「假死」的狀態,不能繼續運行下去了。

解決「假死」的狀況很簡單,將P.java和C.Java文件中的 notify() 改爲 notifyAll() 方法便可,它的原理就是不光通知同類線程,也包括異類。這樣就不至於出現假死的狀態了,程序會一直運行下去。

3.經過管道進行線程間通訊

字節流

在 Java 語言中提供了各類各樣的輸入 / 輸出流Stream,使咱們可以很方便地對數據進行操做,其中管道流(pipeStream)是一種特殊的流,用於在不一樣線程間直接傳送數據。一個線程發送數據到輸出管道,另外一個線程從輸入管道中讀數據。經過使用管道,實現不一樣線程間的通訊,而無須藉助於相似臨時文件之類的東西。

在 Java 的JDK中的IO包提供了4個類來使線程間能夠進行通訊:

  1. PipedInputStream 和 PipedOutputStream
  2. PipedReader 和 PipedWriter

下面來演示字節流的使用。

讀線程:

public class ReadThread extends Thread{
    PipedInputStream inputStream;

    public ReadThread(PipedInputStream inputStream) {
        this.inputStream = inputStream;
    }

    @Override
    public void run() {
        readMethod();
    }

    private void readMethod(){
        try {
            System.out.println("Read :");
            byte[] bytes = new byte[20];
            int readLength = inputStream.read(bytes);
            while (readLength != -1){
                String data = new String(bytes,0,readLength);
                System.out.print(data);
                readLength = inputStream.read(bytes);
            }
            System.out.println();
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

寫線程:

public class WriteThread extends Thread{
    PipedOutputStream outputStream;

    public WriteThread(PipedOutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public void run() {
        readMethod();
    }

    private void readMethod(){
        try {
            System.out.println("write :");
            for (int i=0;i<300;i++){
                String data = ""+(i+1);
                outputStream.write(data.getBytes());
                System.out.print(data);
            }
            System.out.println();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

運行類:

public class Run {

    public static void main(String[] args) throws InterruptedException, IOException {
        PipedOutputStream outputStream = new PipedOutputStream();
        PipedInputStream inputStream = new PipedInputStream();
//        inputStream.connect(outputStream);
        outputStream.connect(inputStream);
        ReadThread readThread =  new ReadThread(inputStream);
        WriteThread writeThread = new WriteThread(outputStream);
        readThread.start();
        Thread.sleep(2000);
        writeThread.start();
    }

}

打印結果:

Read :
write :
123456789101112131415161718192021222324...
123456789101112131415161718192021222324...

使用代碼inputStream.connect(outputStream) 或 outputStream.connect(inputStream) 的做用使兩個 Stream 之間產生通訊連接,這樣才能夠將數據進行輸入與輸出。

但在此實驗中,首先是讀取線程啓動,因爲當時沒有數據被寫入。因此線程阻塞在 int readLength = inputStream.read(bytes) 代碼中,直到有數據被寫入,才繼續向下運行。

字符流

寫線程:

public class WriteThread extends Thread{
    PipedWriter outputStream;

    public WriteThread(PipedWriter outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public void run() {
        readMethod();
    }

    private void readMethod(){
        try {
            System.out.println("write :");
            for (int i=0;i<300;i++){
                String data = ""+(i+1);
                outputStream.write(data);
                System.out.print(data);
            }
            System.out.println();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

讀線程:

public class ReadThread extends Thread{
    PipedReader inputStream;

    public ReadThread(PipedReader inputStream) {
        this.inputStream = inputStream;
    }

    @Override
    public void run() {
        readMethod();
    }

    private void readMethod(){
        try {
            System.out.println("Read :");
            char[] chars = new char[20];
            int readLength = inputStream.read(chars);
            while (readLength != -1){
                String data = new String(chars);
                System.out.print(data);
                readLength = inputStream.read(chars);
            }
            System.out.println();
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

運行類:

public class Run {

    public static void main(String[] args) throws InterruptedException, IOException {
        PipedWriter outputStream = new PipedWriter();
        PipedReader inputStream = new PipedReader();
//        inputStream.connect(outputStream);
        outputStream.connect(inputStream);
        ReadThread readThread =  new ReadThread(inputStream);
        WriteThread writeThread = new WriteThread(outputStream);
        readThread.start();
        Thread.sleep(2000);
        writeThread.start();

    }

}

運行結果:

Read :
write :
123456789101112131415161718...
123456789101112131415161718...

打印的結果基本和前一個基本同樣,此實驗是在兩個線程中經過管道流進行字符數據的傳輸。

4.方法join的使用

在不少狀況下,主線程建立並啓動了子線程,若是子線程中要進行大量的耗時運算,主線程每每將早於子線程以前結束。這時,若是主線程想等待子線程執行完成以後再結束,好比子線程處理一個數據,主線程要取得這個數據中的值,就要用到 join() 方法了。方法 join() 的做用是等待線程對象銷燬。

示例代碼:

public class MyThread extends Thread{
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"執行完畢");
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.start();
        thread.join();
        System.out.println("我想在thread執行完以後執行,我作到了");
    }

}

打印結果:

Thread-0執行完畢
我想在thread執行完以後執行,我作到了

方法join() 的做用是使所屬的線程對象 x 正常執行 run() 方法中的任務,而使當前線程 z 進行無限期的阻塞,等待線程x 銷燬後再繼續執行線程z 後面的代碼。

join與synchronized的區別是:join 在內部使用 wait() 方法進行等待,而synchronize 關鍵字使用的是「對象監視器」原理作爲同步。

在前面已經講到:當線程呈 wait() 方法時,調用線程對象的 interrupt() 方法會出現 InterruptedException 異常。說明方法 join() 和 interrupt() 方法若是彼此遇到,則會出現異常。

4.1 方法 join(long) 的使用

方法 join(long) 中的參數是設定等待的時間。

4.2 join(long) 和 sleep(long) 的區別

方法 join(long) 的功能在內部是使用 wait(long) 方法來實現的,因此 join(long) 方法具備釋放鎖的特色。

方法 join(long) 的源代碼以下:

public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

從源代碼能夠了解到,當執行 wait(long) 方法後,當前線程的鎖被釋放,那麼其餘線程就能夠調用此線程中的同步方法了。而 Thread.sleep() 方法卻不釋放鎖。

5.類 ThreadLocal 的使用

變量值的共享可使用 public static 變量的形式,全部的線程都使用同一個 public static 變量。若是想實現每個線程都有本身的共享變量該如何解決呢?JDK中提供的類ThreadLocal正是爲了解決這樣的問題。

類ThreadLocal 主要解決的就是每一個線程綁定本身的值,能夠將 ThreadLocal 類比喻成全局存放數據的盒子,盒子中能夠存儲每一個線程的私有數據。

示例代碼:

public class LocalThread extends Thread {
    private static ThreadLocal local = new ThreadLocal();

    @Override
    public void run() {
        local.set("線程的值");
        System.out.println("thread線程:"+ local.get());
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println(local.get());
        local.set("main的值");
        LocalThread t = new LocalThread();
        t.start();
        Thread.sleep(1000);
        System.out.println("main線程:"+ local.get());
    }

}

打印結果:

null
thread線程:線程的值
main線程:main的值

在第一次調用get()方法返回的是null,怎麼樣能實現第一次調用get()不返回 null 呢?也就是具備默認值的效果。

答案是繼承 LocalThread 類重寫 initialValue() 方法:

public class Local extends ThreadLocal {

    @Override
    protected Object initialValue() {
        return new Date();
    }
    
}

ThreadLocal原理

ThreadLocal內部使用了ThreadLocalMap,ThreadLocal的set方法內部經過當前線程對象獲取ThreadLocalMap對象,而後將當前ThreadLocal對象做爲Key與Value一塊兒保存到ThreadLocalMap中。

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

ThreadLocal的get方法內部也是經過當前線程對象獲取ThreadLocalMap對象,把當前ThreadLocal對象做爲Key,獲取Value。

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();
    }

6.類 InheritableThreadLocal 的使用

使用類 InheritableThreadLocal 能夠在子線程中取得父線程繼承下來的值。

示例代碼:

public class LocalThread extends Thread {
    private static InheritableThreadLocal local = new InheritableThreadLocal();

    @Override
    public void run() {
        System.out.println("thread線程:"+ local.get());
    }

    public static void main(String[] args) throws InterruptedException {
        local.set("main的值");
        LocalThread t = new LocalThread();
        t.start();
        System.out.println("main線程:"+ local.get());
    }

}

若是想要自定義 get() 方法默認值,具體操做也和 ThreadLocal 是同樣的。

public class Local extends InheritableThreadLocal {

    @Override
    protected Object initialValue() {
        return new Date();
    }
}

InheritableThreadLocal 提供繼承的同時還能夠進行進一步的處理。代碼以下:

public class Local extends InheritableThreadLocal {

    @Override
    protected Object initialValue() {
        return new Date();
    }

    @Override
    protected Object childValue(Object parentValue) {
        return parentValue+"[子線程加強版]";
    }
}

InheritableThreadLocal 如何作到繼承父線程的值的呢?從下面的代碼中咱們能夠看到詳細的邏輯:

/**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

在構造方法的完整源代碼算法中能夠發現,子線程將父線程中的 table 對象以複製的方式賦值給子線程的 table 數組,這個過程是在建立 Thread 類對象時發生的,也就說明當子線程對象建立完畢後,子線程中的數據就是主線程中舊的數據,主線程使用新的數據時,子線程仍是使用舊的數據,由於主子線程使用兩個 Entry[] 對象數組各自存儲本身的值。

7.文末總結

通過本文的學習,能夠將之前分散的線程對象進行彼此的通訊與協做,線程任務再也不是單打獨鬥,更具備團結性,由於它們之間能夠相互通訊。

參考

《Java多線程編程核心技術》高洪巖著

擴展

Java多線程編程核心技術(一)Java多線程技能

Java多線程編程核心技術(二)對象及變量的併發訪問

Java多線程核心技術(四)Lock的使用

Java多線程核心技術(五)單例模式與多線程

Java多線程核心技術(六)線程組與線程異常

相關文章
相關標籤/搜索