《Java 多執行緒程式設計核心技術》筆記——第3章 執行緒間通訊(三)

bm1998發表於2020-12-13

宣告:

本部落格是本人在學習《Java 多執行緒程式設計核心技術》後整理的筆記,旨在方便複習和回顧,並非用作商業用途。

本部落格已標明出處,如有侵權請告知,馬上刪除。

3.2 方法 join 的使用

在很多情況下,主執行緒建立並啟動子執行緒,如果子執行緒中要進行大量的耗時計算,主執行緒往往將早於子執行緒結束之前結束。這時,如果主執行緒想等待子執行緒執行完了再結束。比如子執行緒處理一個資料,主執行緒要取到這個資料中的值,就要用到 join() 方法了。方法 join() 的作用是等待執行緒物件銷燬

3.2.1 學習 join 方法前的鋪墊

在介紹 join 方法之前,先來看一個實驗。

  1. 建立一個自定義的執行緒類

    public class MyThread extends Thread {
        @Override
        public void run() {
            try {
                int secondValue = (int) (Math.random() * 10000);
                System.out.println(secondValue);
                Thread.sleep(secondValue);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 測試類

    public class Run {
        public static void main(String[] args) {
         MyThread myThread = new MyThread();
            myThread.start();
         //Thread.sleep(?);
            System.out.println("當myThread物件執行完畢後再執行");
            System.out.println("但上面程式碼的sleep的值寫多少");
            System.out.println("答案是不確定");
     }
    }
    

    執行結果:

    當myThread物件執行完畢後再執行
    但上面程式碼的sleep的值寫多少
    答案是不確定
    5900
    

3.2.2 用 join() 方法來解決

  1. 建立一個自定義的執行緒類

    public class MyThread extends Thread {
        @Override
        public void run() {
            try {
                int secondValue = (int) (Math.random() * 10000);
                System.out.println(secondValue);
                Thread.sleep(secondValue);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 測試類

    public class Run {
        public static void main(String[] args) {
            try {
                MyThread myThread = new MyThread();
                myThread.start();
                myThread.join();
                System.out.println("當物件 myThread 執行完畢後再執行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    執行結果:

    5482
    當物件 myThread 執行完畢後再執行
    

方法 join 的作用是使所屬的執行緒物件 x 正常執行 run() 方法中的任務,而使當前執行緒 z 進行無限期阻塞,等待執行緒 x 銷燬後再繼續執行執行緒 z 後面的程式碼

方法 join 具有使執行緒排隊執行的作用,有些類似同步的效果。join 與 synchronized 的區別是:join 內部是使用 wait() 方法進行等待的,而 synchronized 關鍵字是使用的是 “物件監視器” 原理做為同步。

3.2.3 方法 join 與異常

在 join 過程中,如果當前執行緒物件被中斷,則當前執行緒出現異常。

示例如下:

  1. 建立三個自定義的執行緒類

    public class ThreadA extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                String newString = new String();
                Math.random();
            }
        }
    }
    
    public class ThreadB extends Thread {
        @Override
        public void run() {
            try {
                ThreadA threadA = new ThreadA();
                threadA.start();
                threadA.join();
                System.out.println("執行緒B在run end處列印了");
            } catch (InterruptedException e) {
                System.out.println("執行緒B在catch處列印了");
                e.printStackTrace();
            }
        }
    }
    
    public class ThreadC extends Thread {
        private ThreadB threadb;
    
        public ThreadC(ThreadB threadb) {
            this.threadb = threadb;
        }
    
        @Override
        public void run() {
            threadb.interrupt();
        }
    }
    
    
  2. 測試類

    public class Run {
        public static void main(String[] args) {
            try {
                ThreadB b = new ThreadB();
                b.start();
                Thread.sleep(500);
                ThreadC c = new ThreadC(b);
                c.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    執行結果:

    執行緒B在catch處列印了
    java.lang.InterruptedException
    	at java.lang.Object.wait(Native Method)
    	at java.lang.Thread.join(Thread.java:1249)
    	at java.lang.Thread.join(Thread.java:1323)
    	at joinexception.ThreadB.run(ThreadB.java:9)
    

說明方法 join() 與 interrupt() 方法如果彼此相遇,則會出現異常。但程式按鈕還是呈紅色狀態,原因是執行緒 ThreadA 還在繼續執行,執行緒 ThreadA 並未出現異常,是正常執行的狀態。

3.2.4 方法 join(long) 的使用

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

示例如下:

  1. 建立自定義的執行緒類

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            try {
                System.out.println("begin timer=" + System.currentTimeMillis());
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 測試類

    public class Test {
        public static void main(String[] args) {
            try {
                MyThread myThread = new MyThread();
                myThread.start();
                myThread.join(2000);
    //            Thread.sleep(2000);
                System.out.println("end timer=  " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    執行結果:

    begin timer=1607782653815
    end timer=  1607782655815
    

但將 main 方法中的程式碼改成使用 sleep(2000) 方法時,執行的效果還是等待了 2 秒,執行結果如下所示。

begin timer=1607783051218
end timer=  1607783053217

那使用 join(2000) 和使用 sleep(2000) 有什麼區別呢?上面的示例中在執行效果上並沒有區別,其實區別主要還是來自於這 2 個方法對同步的處理上。

3.2.5 方法 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(long) 方法卻不釋放鎖

在下面的示例中將實驗 Thread.sleep(long) 方法具有不釋放鎖的特點。

  1. 建立三個自定義的執行緒類

    public class ThreadA extends Thread {
    
        private ThreadB b;
    
        public ThreadA(ThreadB b) {
            super();
            this.b = b;
        }
    
        @Override
        public void run() {
            try {
                synchronized (b) {
                    b.start();
                    Thread.sleep(6000);
                    // Thread.sleep()不釋放鎖
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public class ThreadB extends Thread {
    
        @Override
        public void run() {
            try {
                System.out.println("   b run begin timer=" + System.currentTimeMillis());
                Thread.sleep(5000);
                System.out.println("   b run   end timer=" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        synchronized public void bService() {
            System.out.println("列印 b Service timer=" + System.currentTimeMillis());
        }
    
    }
    
    public class ThreadC extends Thread {
    
        private ThreadB threadB;
    
        public ThreadC(ThreadB threadB) {
            super();
            this.threadB = threadB;
        }
    
        @Override
        public void run() {
            threadB.bService();
        }
    
    }
    
  2. 測試類

    public class Run {
    
        public static void main(String[] args) {
    
            try {
                ThreadB b = new ThreadB();
    
                ThreadA a = new ThreadA(b);
    
                a.start();
    
                Thread.sleep(1000);
    
                ThreadC c = new ThreadC(b);
    
                c.start();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    執行結果:

       b run begin timer=1607788792039
       b run   end timer=1607788797040
    列印 b Service timer=1607788798039
    

由於執行緒 ThreadA 使用 Thread.sleep(long) 方法一直持有 ThreadB 物件的鎖,時間達到 6 秒,所以執行緒 ThreadC 只有在 ThreadA 時間到達 6 秒後釋放 ThreadB 的鎖時,才可以呼叫 ThreadB 中的同步方法 synchronized public void bService()。

下面繼續實驗,驗證 join() 方法釋放鎖的特點。

  1. 更改 ThreadA.java 類程式碼如下:

    public class ThreadA extends Thread {
    
        private ThreadB b;
    
        public ThreadA(ThreadB b) {
            super();
            this.b = b;
        }
    
        @Override
        public void run() {
            try {
                synchronized (b) {
                    b.start();
                    b.join();// 說明join釋放鎖了
                    for (int i = 0; i < Integer.MAX_VALUE; i++) {
                        String newString = new String();
                        Math.random();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 再次執行,結果如下

       b run begin timer=1607790117470
    列印 b Service timer=1607790118472
       b run   end timer=1607790122472
    

由於執行緒 ThreadA 釋放了 ThreadB 的鎖,所以執行緒 ThreadC 可以呼叫 ThreadB 中的同步方法 synchronized public void bService()。

此實驗也再次說明 join(long) 方法具有釋放鎖的特點

3.2.6 方法 join() 後面的程式碼提前執行:出現意外

針對前面章節中的程式碼進行測試的過程中,還可以延伸出 “陷阱式” 的結果,如果稍加不注意,就會掉進 “陷阱” 裡。

示例如下:

  1. 建立兩個自定義的執行緒類

    public class ThreadA extends Thread {
        private ThreadB b;
    
        public ThreadA(ThreadB b) {
            super();
            this.b = b;
        }
    
        @Override
        public void run() {
            try {
                synchronized (b) {
                    System.out.println("begin A ThreadName="
                            + Thread.currentThread().getName() + "  "
                            + System.currentTimeMillis());
                    Thread.sleep(5000);
                    System.out.println("  end A ThreadName="
                            + Thread.currentThread().getName() + "  "
                            + System.currentTimeMillis());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public class ThreadB extends Thread {
        @Override
        synchronized public void run() {
            try {
                System.out.println("begin B ThreadName="
                        + Thread.currentThread().getName() + "  "
                        + System.currentTimeMillis());
                Thread.sleep(5000);
                System.out.println("  end B ThreadName="
                        + Thread.currentThread().getName() + "  "
                        + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 測試類

    public class Run1 {
        public static void main(String[] args) {
            try {
                ThreadB b = new ThreadB();
                ThreadA a = new ThreadA(b);
                a.start();
                b.start();
                b.join(2000);
                System.out.println("                    main end "
                        + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

程式執行後,在控制檯列印結果有以下兩種情況:

begin A ThreadName=Thread-1  1607792596530
  end A ThreadName=Thread-1  1607792601532
                    main end 1607792601532
begin B ThreadName=Thread-0  1607792601532
  end B ThreadName=Thread-0  1607792606532
begin A ThreadName=Thread-1  1607793705864
  end A ThreadName=Thread-1  1607793710866
begin B ThreadName=Thread-0  1607793710866
  end B ThreadName=Thread-0  1607793715866
  					main end 1607793715866

為什麼出現截然不同的執行結果呢?

3.2.7 方法 join() 後面的程式碼提前執行:解釋意外

為了檢視 join() 方法在 Run1.java 類中執行的時機,建立 RunFirst.java 類檔案,程式碼如下:

public class RunFirst {
    public static void main(String[] args) {
        ThreadB b = new ThreadB();
        ThreadA a = new ThreadA(b);
        a.start();
        b.start();
        System.out.println("   main end=" + System.currentTimeMillis());
    }
}

程式多次執行結果,如下所示:

   main end=1607794763236
begin A ThreadName=Thread-1  1607794763236
  end A ThreadName=Thread-1  1607794768237
begin B ThreadName=Thread-0  1607794768237
  end B ThreadName=Thread-0  1607794773238

通過多次執行 RunFirst.java 檔案後,可以發現一個規律:main end 往往都是第一個列印的。所以可以完全確定地得出一個結論:方法 join(2000) 大部分是先執行的,也就是先搶到 ThreadB 的鎖,然後快速進行釋放。

而執行 Run1.java 檔案後就會出現一些不同的執行結果:

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

相關文章