天天看點

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

目錄

一、等待通知機制(wait/notify)

1、等待方法/wait()

2、通知方法/notify()

3、通知所有/notifyAll()

4、interrupt()方法遇到wait()方法和sleep()方法

5、wait(long)和sleep(time)

6、notify()通知過早

二、join()方法——線程排隊

2.1join()方法的執行效果

2.2join(long)和sleep(long)的差別

2.3join()方法和中斷異常

三、ThreadLocal類和InheritableThreadLocal類

1、ThreadLocal類的使用

1.1ThreadLocal的預設值和唯一鍵值對

2、InheritableThreadLocal類

四、yied()的使用

線程間通信使線程成為一個整體,提高系統之間的互動性,在提高CPU使用率的同時可以對線程任務進行有效的把控與監督。

一、等待通知機制(wait/notify)

wait使線程停止運作,notify使停止的線程繼續執行。

1、等待方法/wait()

1.1使目前執行代碼的線程進行等待

該方法将目前線程置入“預執行隊列”中,并在wait()所在的代碼行處停止執行,直到接到通知或被中斷為止。

1.2wait()方法隻能在同步方法或同部塊中調用

1.3執行wait()方法後,目前線程立即釋放鎖

1.4調用wait()方法,線程需要擷取該對象的對象級别鎖,如果沒有持鎖,将會抛出IllegalMonitorStateException

2、通知方法/notify()

2.1notify()方法隻能在同步方法或同步塊中調用

2.2在執行notify()方法後,目前線程不會立即釋放鎖

一個線程執行notify()方法後,需要等到執行notify()方法的線程将程式執行完,也就是退出synchronized代碼塊後,目前線程才會釋放鎖,而成wait狀态所在的線程才可以擷取該對象的鎖。

關于通知/等待機制的完整代碼示範:

建立線程——線程A

public class MyThreadA extends Thread {

    private Object lock;

    public MyThreadA(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ":開始執行wait()方法,執行後釋放鎖...");
                lock.wait();
                System.out.println(Thread.currentThread().getName() + ":wait()方法被喚醒,得到鎖後繼續執行...");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThreadA myThreadA = new MyThreadA(lock);
        myThreadA.start();
        Thread.sleep(1000);
        MyThreadB myThreadB = new MyThreadB(lock);
        myThreadB.start();
    }
}
           

建立線程——線程B

public class MyThreadB extends Thread {

    private Object lock;

    public MyThreadB(Object lock) {
        super();
        this.lock = lock;
    }

    public void run() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ":得到鎖,開始執行notify()方法...");
                lock.notify();
                System.out.println(Thread.currentThread().getName() + ":執行notify()方法結束,沒有立即釋放鎖...");
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName() + ":還沒有釋放鎖...繼續執行鎖内循環" + i);
                }
            }
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + ":synchronized外方法繼續運作,此時鎖已經釋放...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

代碼邏輯解釋:

1、建立兩個線程:等待線程A(執行wait()方法)和通知線程B(執行notify()方法,用來喚醒等待線程)

2、線程A先擷取鎖,執行wait()方法,釋放鎖并進行等待。

3、1秒鐘後,線程B啟動,并擷取鎖,執行notify()方法,喚醒線程A。

但此時線程B并沒有馬上釋放鎖,因為同步代碼塊中還有一個循環沒有執行完,線程B繼續執行循環。

4、線程B循環結束,同步代碼塊執行完成,釋放鎖。等待線程A拿到鎖,繼續執行。

然後,我們看到線程B還有一個同步代碼塊外的執行邏輯,這個邏輯加在這裡的原因是證明,同步代碼塊外的邏輯并不影響等待/通知機制,notify()隻有在該所在同步代碼塊代碼執行完才會釋放鎖。

3、通知所有/notifyAll()

notify()方法可以随機喚醒等待隊列中等待同一共享資源的一個(随機的,僅僅一個)線程,并使該線程退出等待隊列,進入可運作狀态。

notifyAll()方法可以使所有正在等待隊列中等待同一資源的全部線程從等待狀态進入可運作狀态。

4、interrupt()方法遇到wait()方法和sleep()方法

wait()方法執行完會自動釋放鎖,sleep()方法不會釋放鎖。

當線程調用wait()方法和sleep()方法時,如果程式在執行過程中再調用interrupt()方法中斷線程,程式會抛出中斷異常。

測試代碼:

建立MyService類

public class MyService {

    public void testMethod(Object lock){
        try {
            synchronized (lock) {
                System.out.println("執行wait()方法開始....");
                lock.wait();
                System.out.println("wait狀态被喚醒....");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("程式在執行wait()方法中被中斷了...");
        }
    }
}
           

建立線程

package demo.otherdemo.lock;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Administrator
 * @date 2019/8/17/017 14:26
 * @Version 1.0
 */

public class MyThread extends Thread {

    private Object lock;

    public MyThread(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        MyService myService = new MyService();
        myService.testMethod(lock);
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThread myThread = new MyThread(lock);
        myThread.start();
        Thread.sleep(5000);
        // 調用中斷方法,打上中斷标記
        myThread.interrupt();
    }
}
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

上邊代碼示範,一個線程執行完等待wait()方法後,一直沒被喚醒,此時該線程調用interrupt()方法,程式抛出了異常。那麼如果是sleep()方法呢?

接下來修改一下程式,把wait()方法修改成sleep()方法

public class MyService {

    public void testMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("執行sleep()方法開始....");
                Thread.sleep(30000);
                System.out.println("執行sleep()方法結束...");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("程式在執行sleep()方法中被中斷了...");
        }
    }
}
           

執行結果如下:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

上述示範結果,都證明了結論的正确性。

5、wait(long)和sleep(time)

wait(long)方法,如果線程在等待的一段之間内沒有被喚醒,那麼超過這個時間,線程将自動喚醒。

sleep(time)方法,線程在睡眠期無法被正常喚醒,隻能等到睡眠時間結束,線程才會繼續執行。

6、notify()通知過早

如果通知notify()先執行,等待wait()後執行,那麼,執行wait()的線程将永遠的等待下去,出現類似死鎖的情況。

這個問題出現的原因,是等待和通知是分别由兩個不同線程去實作的,在并發量大的情況下,兩者的執行順序是得不到保證的,也就是說,等待并不總是出現在通知前。

怎麼去解決過早通知問題呢?

解決方案:

在等待和通知方法中添加條件!

比如:

等待線程A需要在條件true下才會執行等待,

通知線程B在執行通知後會把條件更改為false。

這樣做的好處是,如果通知線程B先執行,那麼等待線程A拿不到等待的條件,就不會進入等待狀态,進而避免出現死鎖。

通俗說法:

就好比你去火車站乘火車,如果火車已經開走了(通知已經執行),那麼車站會便提示你本次列車已經開出(修改條件),等你到車站一看,火車已經開走了(條件發生變化),那你就無需再去等此次列車了(不執行等待)。

測試代碼:

public class MyService {

    private Object lock = new Object();

    private boolean condition = true;

    public Runnable runnableA = new Runnable() {
        @Override
        public void run() {
            try {
                synchronized (lock) {
                    // 等待是需要條件的
                    while (condition) {
                        System.out.println(Thread.currentThread().getName()
                                +":開始執行wait()方法...");
                        lock.wait();
                        System.out.println(Thread.currentThread().getName()
                                +":被喚醒...");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    public Runnable runnableB = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName()
                        +":開始執行notify()方法...");
                lock.notify();
                System.out.println(Thread.currentThread().getName()
                        +":notify()方法執行結束...");
                // 已經通知過了,為避免通知先執行,等待線程等不到通知,是以修改條件
                condition = false;
            }
        }
    };

    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        Thread a = new Thread(myService.runnableA);
        a.start();
        Thread.sleep(1000);
        Thread b = new Thread(myService.runnableB);
        b.start();
    }
}
           

執行結果

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

從上述代碼的邏輯可以看出,等待線程等待是有條件的,通知線程執行完通知都會去修改等待的條件,是以,不管是哪個線程先執行,都不會出現類似死鎖的情況。

二、join()方法——線程排隊

2.1join()方法的執行效果

join()方法具有使線程排隊的作用,有些類似于同步的運作效果。

join()方法和synchronized關鍵字的差別:

join在内部使用wait()方法進行等待,synchronized關鍵字使用的是“對象螢幕”原理做同步。

join()方法的源碼如下:看到wait()了吧?

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

那麼執行join()方法的效果到底是什麼樣子的?我們來一段代碼測試以下

測試join()方法的效果:

public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()
                    + ":方法執行開始...");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName()
                    + ":方法執行結束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

在不加join()方法前的執行效果:

public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(500);
        System.out.println(Thread.currentThread().getName() + ":執行結束...");
    }
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

接下來,修改下執行邏輯,加上join()的方法:

public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        // 加上了join的方法
        myThread.join();
        System.out.println(Thread.currentThread().getName() + ":執行結束...");
    }
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

從上述測試可以看出,添加join()方法後,主線程一直等到線程0結束以後,才開始運作。

2.2join(long)和sleep(long)的差別

join(long)設定線程等待時間,一旦逾時,便不再等待。

差別:join(long)方法釋放鎖,sleep(long)方法不釋放鎖。

這是因為join(long)方法内部是使用wait(long)方法來實作的,是以該方法具有釋放鎖的特點。

同樣的原因,當join(long)在執行過程中遇到interrupt()方法時,也會抛出中斷異常。

2.3join()方法和中斷異常

在執行join()方法的過程當中,如果目前線程代用interrupt()方法,目前線程會抛出異常,但是,并不會影響目前線程建立的子線程的運作。

測試代碼:

建立線程A

public class ThreadA extends Thread {

    public void run() {
        try {
            while (true) {
                System.out.println(Thread.currentThread().getName() + ":執行方法...");
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

建立線程B

public class ThreadB extends Thread {
    
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + ":方法執行開始...");
            ThreadA threadA = new ThreadA();
            threadA.setName("B線程中的A線程");
            threadA.start();
            threadA.join();
            System.out.println(Thread.currentThread().getName() + ":方法執行結束...");
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"出現中斷異常了...");
            e.printStackTrace();
        }
    }
}
           

建立線程C

public class ThreadC extends Thread {

    private ThreadB threadB;

    public ThreadC(ThreadB threadB) {
        this.threadB = threadB;
    }

    public void run() {
        threadB.interrupt();
    }
}
           

建立執行類

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        ThreadB threadB = new ThreadB();
        threadB.setName("線程B");
        threadB.start();
        Thread.sleep(1000);
        ThreadC threadC = new ThreadC(threadB);
        threadC.setName("線程C");
        threadC.start();
    }
}
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

代碼邏輯:

1、建立線程A、線程B、線程C

2、線程A執行死循環(模仿一直執行任務),B線程的任務主要是開啟A線程,并代用join()方法,等待A線程運作結束,C線程主要用來中斷B線程。

3、當B線程在執行join()的過程當中,C線程開啟,B線程中斷,抛出異常。但這個時候并沒有使程式運作結束,因為B線程建立的子線程A還在繼續運作。

三、ThreadLocal類和InheritableThreadLocal類

1、ThreadLocal類的使用

ThreadLocal類可以用來存放線程的資料,每個線程都可以通過ThreadLocal來綁定自己的值,ThreadLocal使變量線上程之間具有隔離性,也就是說ThreadLocal存的變量值是私有的。

驗證隔離性:

建立工具類Tools

public class Tools {
    public static final ThreadLocal t1 = new ThreadLocal();
}
           

建立線程A

public class ThreadA extends Thread {

    public void run() {
        try {
            for (int i = 0; i < 2; i++) {
                Tools.t1.set(Thread.currentThread().getName() + "存入-" + i);
                System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t1.get());
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

建立線程B

public class ThreadB extends Thread {

    public void run() {
        try {
            for (int i = 0; i < 3; i++) {
                Tools.t1.set(Thread.currentThread().getName() + "存入-" + i);
                System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t1.get());
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

建立執行類MyService

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();
        threadA.start();
        threadB.start();
        for (int i = 0; i < 3; i++) {
            Tools.t1.set(Thread.currentThread().getName() + "存入-" + i);
            System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t1.get());
            Thread.sleep(200);
        }
    }
}
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

從上邊的結果看到,雖然三個線程同時向t1(t1是唯一的)存儲值,但是最後取出來的都是各自線程存放的值,其他線程并不會取到别的線程存放的資料,驗證了ThreadLocal類的隔離性。

1.1ThreadLocal的預設值和唯一鍵值對

ThreadLocal類可以通過重寫initialValue()方法來設定預設值,實作初始值不為null的效果。

源碼方法的預設值為null,如下:

protected T initialValue() {
        return null;
    }
           

重寫initialValue()方法測試代碼:

重新建立一個類繼承ThreadLocal類

public class ThreadLocalExt extends ThreadLocal {
    @Override
    protected Object initialValue() {
        return Thread.currentThread().getName()+"-設定的預設值";
    }
}
           

重新編寫工具類

public class Tools {
    public static final ThreadLocalExt t = new ThreadLocalExt();
}
           

建立線程A,用來展示隔離效果

public class ThreadA extends Thread {

    public void run() {
        try {
            if(Tools.t.get()==null){
                for (int i = 0; i < 2; i++) {
                    Tools.t.set(Thread.currentThread().getName() + "存入-" + i);
                    System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
                    Thread.sleep(200);
                }
            }
            System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

建立執行類

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();
        Thread.sleep(200);
        System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
        Tools.t.set(Thread.currentThread().getName()+"-設定的非預設值1");
        System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
    }
}
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

從程式的輸出結果,我們可以看到,ThreadLocal類在設定預設值時,預設值會生效,此時預設值仍然具有隔離性。同時我們也看到,當主線程往ThreadLocal類裡邊設定新值時,預設值會被替換掉。這說明,ThreadLocal隻會存儲唯一的值,存多個值時,前邊的值都會被替換掉。

被替換掉的原因,是ThreadLocal的存儲通過Map集合來實作的,而且隻存儲了唯一的鍵值對。通過源碼,可以看到這個map存值的鍵是唯一的,這也是為什麼不同線程具有不同值的原因,因為不同線程this對象的值是不一樣的,而相同線程this值都是相同的。

ThreadLocal存儲實作的源碼:

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

2、InheritableThreadLocal類

使用ThreadLocal線程之間不能實作父子線程資料的傳遞(局限),InheritableThreadLocal類可以使子線程能夠擷取到父線程設定的值。

如果父線程修改了值,那麼子線程将擷取到父線程修改後的值。

測試代碼:

建立InheritableThreadLocalExt對象

public class InheritableThreadLocalExt extends InheritableThreadLocal {
    @Override
    protected Object initialValue() {
        return Thread.currentThread().getName()+"-InheritableThreadLocal設定的預設值";
    }
}
           

建構工具類

public class Tools {
    public static InheritableThreadLocalExt t = new InheritableThreadLocalExt();
}
           

建立A線程

public class ThreadA extends Thread {

    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + ":A線程取出:" + Tools.t.get());
            Thread.sleep(200);
            Tools.t.set(Thread.currentThread().getName()+":A線程修改了值");
            System.out.println(Thread.currentThread().getName() + ":把值修改為——" + Tools.t.get());
            Thread thread = new Thread(new Runnable() {
                // 建立一個線程(子子線程)。看會不會傳遞值
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":A子線程取出的值:" + Tools.t.get());
                }
            });
            thread.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

建立執行程式

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":主線程取出的值:" + Tools.t.get());
        // 主線程修改了值
        Tools.t.set(Thread.currentThread().getName()+"-設定的非預設值1");
        System.out.println(Thread.currentThread().getName() + ":把值修改為——" + Tools.t.get());
        Thread.sleep(200);
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}
           

執行結果

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

代碼邏輯:

1、建立InheritableThreadLocalExt對象,繼承了InheritableThread類,設定預設值,并構造工具。

2、主線程先執行,這個時候預設值打上了主線程的标記(main),然後主線程修改了預設值。

3、線程A在主線程之後執行,這個時候拿到了主線程修改後的值(父子線程資料傳遞實作了)。

4、線程A中又建立了A子線程,在建立之前,線程A又修改了資料,我們看到A子線程拿到的是線程A修改後的資料(又是一次父子線程的資料傳遞)

其實,這裡想說的是,父子線程指的是臨近線程,在父線程修改了資料的情況下,A子線程不會去越界取到main線程的值。

另外,隻要資料不修改,所有子線程将取到一樣的值,這個值在子線程中具有無限傳遞性,下面是以三個線程執行結果為示例(主線程、線程A都不修改值的情況),但我們知道,就算有更多的線程,其結論也是一樣的。

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

寫在最後:如果子線程在取值的同時,父線程修改了InheritableThreadLocal裡的值,子線程可能取到修改之前的舊值。

四、yied()的使用

yield()執行線程讓步,讓步的是CPU的執行權,而不是釋放鎖。

yield()使線程從運作狀态轉換到可運作狀态,以允許具有相同優先級的其他線程獲得運作機會。是以,使用yield()的目的是讓相同優先級的線程之間能适當的輪換轉型。但是,實際中無法保證yield()達到讓步目的,因為讓步線程可能被線程排程程式再次選中。

注:yield()從未導緻線程轉換到等待/睡眠/阻塞狀态

public class MyThread extends Thread {

    public MyThread(String s){
        super(s);
    }

    @Override
    public void run() {
        for(int i =0;i<5;i++){
            System.out.println(getName()+":"+i);
            if("t1".equals(getName())){
                if(i==0){
                    yield();
                }
            }
        }
    }

    public static void main(String[] args) {
        MyThread myThreadA = new MyThread("t1");
        MyThread myThreadB = new MyThread("t2");
        myThreadA.start();
        myThreadB.start();
    }
}
           

執行結果:

多線程程式設計(七)——線程間通信基礎詳解一、等待通知機制(wait/notify)二、join()方法——線程排隊三、ThreadLocal類和InheritableThreadLocal類四、yied()的使用

繼續閱讀