1. 程式人生 > >Java多執行緒程式設計核心技術(三)多執行緒通訊

Java多執行緒程式設計核心技術(三)多執行緒通訊

執行緒是作業系統中獨立的個體,但這些個體如果不經過特殊的處理就不能成為一個整體。執行緒間的通訊就是成為整體的必用方案之一,可以說,使執行緒間進行通訊後,系統之間的互動性會更強大,在大大提高CPU利用率的同時還會使程式設計師對各執行緒任務在處理的過程中進行有效的把控與監督。在本章中需要著重掌握的技術點如下:

  • 使用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語句進行捕捉異常。

方法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 狀態,程式最後也就呈“假死”的狀態,不能繼續執行下去了。

解決“假死”的情況很簡單,將Pjava和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();
    }
    
}

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 類需要注意一點的是,如果子執行緒在取得值的同時,主執行緒將 InheritableThreadLocal 中的值進行更改,那麼子執行緒取到的值還是舊值。

7.文末總結

經過本文的學習,可以將以前分散的執行緒物件進行彼此的通訊與協作,執行緒任務不再是單打獨鬥,更具有團結性,因為它們之間可以相互通訊。

參考

《Java多執行緒程式設計核心技術》高洪巖著