1. 程式人生 > >Java多線程編程核心技術(三)多線程通信

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

臨時 -c 線程 visa service ade 出現異常 並發訪問 try

線程是操作系統中獨立的個體,但這些個體如果不經過特殊的處理就不能成為一個整體。線程間的通信就是成為整體的必用方案之一,可以說,使線程間進行通信後,系統之間的交互性會更強大,在大大提高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 狀態,程序最後也就呈“假死”的狀態,不能繼續運行下去了。

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

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多線程編程核心技術》高洪巖著

擴展

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

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

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