天天看點

Thread和Object中的重要方法詳解

推薦:​​Java并發程式設計彙總​​

Thread和Object中的重要方法詳解

原文位址

​​多線程學習(二)——Thread和Object類中的重要方法詳解​​

方法概覽

方法名 簡介
Thread sleep 線程休眠一段時間
join 等待其他線程執行完畢
yield 放棄已獲得的CPU資源
currentThread 擷取目前執行線程的引用
start 啟動線程
interrupt 中斷線程
stop, suspend, resume 已廢棄
Object wait / notify / notifyAll 讓線程暫時休息/喚醒

wait / notify / notifyAll 方法

作用

​wait​

​​讓線程休息,進入阻塞階段,執行完​

​wait​

​​方法會釋放​

​monitor​

​鎖。

四種被喚醒的情況:

  1. 另一個線程調用這個對象的​

    ​notify()​

    ​方法,且剛好被喚醒的是本線程。
  2. 另一個線程調用這個對象的​

    ​notifyAll()​

    ​方法。
  3. 過了​

    ​wait(long timeout)​

    ​​規定的逾時時間,如果傳入的是​

    ​0​

    ​ ,則表示永久等待。
  4. 線程自身調用了​

    ​interrupt()​

    ​​(遇到中斷時,會抛出​

    ​InterruptedException​

    ​​并釋放​

    ​monitor​

    ​鎖)。

特點

  • 需要在​

    ​synchronized​

    ​關鍵字修飾的代碼塊或方法中執行。
  • 執行​

    ​wait​

    ​​、​

    ​notify​

    ​​、​

    ​notifyAll​

    ​​都必須先擁有該​

    ​monitor​

    ​。
  • ​notify​

    ​隻能喚醒其中一個線程。
  • 多個鎖的情況下,隻會釋放目前的鎖。
  • 都屬于​

    ​Object​

    ​類

代碼示範

  • ​場景​

    ​​:線程​

    ​1​

    ​​睡了,線程​

    ​2​

    ​去喚醒。
  • ​執行順序​

    ​​:​

    ​Thread1​

    ​​進入同步代碼塊,執行了​

    ​wait()​

    ​​方法後,線程釋放了鎖; 此時​

    ​Thread2​

    ​​ 獲得鎖,執行了​

    ​notify()​

    ​​方法喚醒了​

    ​Thread1​

    ​​,執行完 ​

    ​Thread2​

    ​​同步代碼塊中的所有語句後,​

    ​Thread1​

    ​​繼續執行 ​

    ​wait()​

    ​ 後的代碼。
public class WaitNotify {
    public static Object object = new Object();

    static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + " 開始執行");
                try {
                    // 在同步代碼塊中執行了 wait() 方法後,釋放了鎖
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 擷取到了monitor鎖");
            }
        }
    }

    // 喚醒 Thread1
    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                object.notify();
                System.out.println(Thread.currentThread().getName() + " 調用了notify");
            }
        }
    }

    // 讓Thread1先進入wait狀态,再被Thread2喚醒
    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
        thread1.start();
        Thread.sleep(200);
        thread2.start();
    }
}      
Thread和Object中的重要方法詳解

notify與notifyAll的差別

  • ​場景​

    ​​:線程​

    ​1​

    ​​和線程​

    ​2​

    ​​被阻塞,線程​

    ​3​

    ​喚醒它們。
  • ​notifyAll​

    ​​ :調用​

    ​notifyAll​

    ​​喚醒全部線程(​

    ​start​

    ​​的調用順序不一定能保證線程的執行順序,是以線程​

    ​3​

    ​​需要等待一會兒再​

    ​start​

    ​)。
public class NotifyAndNotifyAll implements Runnable{
    private static final Object resource = new Object();

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new NotifyAndNotifyAll();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(() -> {
            synchronized (resource) {
                resource.notifyAll();
                System.out.println(Thread.currentThread().getName() + " 看我叫醒這兩個豬");
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(200);
        thread3.start();
    }

    @Override
    public void run() {
        synchronized (resource) {
            System.out.println(Thread.currentThread().getName() + " 得到了鎖");
            try {
                System.out.println(Thread.currentThread().getName() + " 困了,休息會兒");
                resource.wait();
                System.out.println(Thread.currentThread().getName() + " 醒啦,一起來happy");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}      
Thread和Object中的重要方法詳解
  • ​notify​

    ​​:​

    ​notify​

    ​隻能喚醒其中一個線程。

證明wait隻會釋放目前的鎖

  • ​場景​

    ​​:線程​

    ​1​

    ​​持有兩把鎖并釋放其中一把,線程​

    ​2​

    ​能拿到幾把鎖。
public class ReleaseOwnMonitor {
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resourceA) {
                System.out.println(Thread.currentThread().getName() + " 得到了resourceA鎖");
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + " 得到了resourceB鎖");
                    try {
                        System.out.println(Thread.currentThread().getName() + " 釋放了resourceA鎖");
                        resourceA.wait();
                        System.out.println(Thread.currentThread().getName() + " 後續happy");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (resourceA) {
                System.out.println(Thread.currentThread().getName() + " 表示收到了老鐵釋放的resourceA了");
                System.out.println(Thread.currentThread().getName() + " 心想:那麼resourceB還在它手上嗎,拿到了我再吱聲");
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + " 我拿到resourceB啦");
                }
            }
        }).start();
    }
}      

根據控制台輸出,我們可以看到線程​

​2​

​​隻拿到了線程​

​1​

​​釋放的目前的鎖​

​A​

​​,鎖​

​B​

​​沒有被釋放導緻線程​

​2​

​一直在等待鎖的過程中。

Thread和Object中的重要方法詳解

sleep 方法

作用

隻想讓線程在預期的時間執行,其他時間不占用CPU資源。

特點

與​

​wait​

​​不同,執行​

​wait​

​​會釋放鎖,而執行​

​sleep​

​​的時候不釋放鎖(​

​synchronized​

​​和​

​lock​

​​),等​

​sleep​

​設定的時間到了以後(正常結束)才會釋放鎖。

代碼示範

  • ​sleep​

    ​方法不釋放鎖。
public class SleepDontReleaseMonitor implements Runnable{
    public static void main(String[] args) {
        SleepDontReleaseMonitor target = new SleepDontReleaseMonitor();
        new Thread(target).start();
        new Thread(target).start();

    }
    
    @Override
    public void run() {
        sync();
    }
    private synchronized void sync() {
        System.out.println(Thread.currentThread().getName() + " 擷取到了monitor");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 退出了同步代碼塊");
    }
}      
Thread-0 擷取到了monitor
Thread-0 退出了同步代碼塊
Thread-1 擷取到了monitor
Thread-1 退出了同步代碼塊      
public class SleepDontReleaseLock implements Runnable{
    private static final Lock LOCK =  new ReentrantLock();

    public static void main(String[] args) {
        SleepDontReleaseLock target = new SleepDontReleaseLock();
        new Thread(target).start();
        new Thread(target).start();
    }
    @Override
    public void run() {
        LOCK.lock();
        System.out.println(Thread.currentThread().getName() + " 擷取到了鎖");
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + " 醒了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            LOCK.unlock();
        }
    }
}      
Thread-1 擷取到了鎖
Thread-1 醒了
Thread-0 擷取到了鎖
Thread-0 醒了      
  • ​sleep​

    ​​方法響應中斷,線程中斷會抛出​

    ​InterruptedException​

    ​,随即會清除中斷狀态。
public class SleepInterrupted implements Runnable{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SleepInterrupted());
        thread.start();
        Thread.sleep(8000);
        thread.interrupt();
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(new Date());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                System.out.println("遇到了中斷");
                e.printStackTrace();
            }
        }
    }
}      
Thread和Object中的重要方法詳解

TimeUnit上面的代碼中,我們使用了 ​

​TimeUnit.SECONDS.sleep(long)​

​ 進行休眠,它幫我們進行了時間的機關換算。

Thread和Object中的重要方法詳解

而它的​

​sleep​

​​方法本質也是​

​Thread.sleep​

​​,隻不過當它的逾時時間 ​

​< 0​

​​時,不作操作,而 ​

​Thread.sleep​

​​ 的設定的時間如果 ​

​< 0​

​​,則會 ​

​throw new IllegalArgumentException("timeout value is negative")​

​。

Thread和Object中的重要方法詳解

join 方法

作用

新的線程加入,需要等待它執行完畢再出發,例如​

​main​

​​線程等待子線程​

​thread1​

​執行完畢。

代碼示範

  • ​join​

    ​​ 方法的用法,如果沒有調用 ​

    ​join​

    ​​ 方法,兩條輸出語句​

    ​如廁​

    ​​和​

    ​上完出來​

    ​​是緊接着輸出的,而調用了 ​

    ​join​

    ​​ 方法,​

    ​main​

    ​線程就會等待它們執行完畢再執行。
public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 我花了 2 秒上完了廁所");
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 我花了 3 秒上完了廁所");
        });
        thread1.start();
        thread2.start();
        System.out.println(Thread.currentThread().getName() + " 小夥伴要如廁,我在門口開始等候它們");
        thread1.join();
        thread2.join();
        System.out.println(Thread.currentThread().getName() + " 它們終于上完出來啦");
    }
}      
main 小夥伴要如廁,我在門口開始等候它們
Thread-0 我花了 2 秒上完了廁所
Thread-1 我花了 3 秒上完了廁所
main 它們終于上完出來啦      
  • 當主線程中斷時,子線程也需要中斷。
public class JoinInterrupted {
    public static void main(String[] args) {
        Thread mianThread = Thread.currentThread();
        Thread thread1 = new Thread(() -> {
            try {
                mianThread.interrupt();
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + " 我執行完畢了");
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 我也中斷了");

            }
        });
        thread1.start();
        System.out.println(Thread.currentThread().getName() + " 等待子線程搞完");
        try {
            // 主線程加入 Thread1
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 被中斷了");
            // 将主線程的中斷傳遞給子線程,不然會不一緻
            thread1.interrupt();
        }
        System.out.println(Thread.currentThread().getName() + " 子線程執行完畢了");
    }
}      
main 等待子線程搞完
main 被中斷了
main 子線程執行完畢了
Thread-0 我也中斷了      
  • ​join​

    ​​期間,線程是 ​

    ​Waiting​

    ​狀态。
public class JoinState {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + " 我想知道此時主線程的狀态 : " + mainThread.getState());
                System.out.println(Thread.currentThread().getName() + " 運作結束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        System.out.println(Thread.currentThread().getName() + " 等待子線程運作完畢");
        thread.join();
        System.out.println(Thread.currentThread().getName() + " 子線程運作完畢");
    }
}      
main 等待子線程運作完畢
Thread-0 我想知道此時主線程的狀态 : WAITING
Thread-0 運作結束
main 子線程運作完畢      

源碼

​join​

​​内部調用了 ​

​wait()​

​​ 方法,​

​wait(0)​

​​ 表示一直處于休眠直至被喚醒。然而我們并沒有看到喚醒的方法,這是因為​

​Thread​

​​類​

​run​

​​方法執行完畢後,會進行自動喚醒。底層​

​c/c++​

​​代碼中線上程退出後,會執行 ​

​lock.notify_all(thread)​

​喚醒。

void JavaThread::exit(booldestory_vm, ExitTypeexit_type);
static void ensure_join(JavaThread*thread) {
  Handle threadObj(thread, thread -> threadObj());
  ObjectLocker lock(threadObj, thread);
  thread -> clear_pending_exception();
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  java_lang_Thread::set_thread(threadObj(), NULL);
  lock.notify_all(thread);
  thread -> clear_pending_exception();
}      

​join​

​ 源碼:

public final void join() throws InterruptedException {
        join(0);
    }
    
    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;
            }
        }
    }      

​CountDownLatch​

​​ / ​

​CyclicBarrier​

​​ 是 ​

​join​

​ 的等價類 (感興趣的小夥伴可以參考另外資料了解)。

yeild 方法

作用

釋放自己的CPU時間片,但不會釋放鎖,也不會陷入阻塞,線程狀态依然是​

​Runnable​

​狀态。

與​

​sleep​

​​的差別,​

​yield​

​​隻是暫時把CPU排程權讓給别的線程,但該線程還是能立刻處于競争狀态被再次排程,​

​sleep​

​的話,會被認為已經阻塞了。

問題

  • 為什麼線程通信的方法 ​

    ​wait()​

    ​, ​

    ​notify()​

    ​ 和 ​

    ​notifyAll()​

    ​被定義在​

    ​Object​

    ​類中?而​

    ​sleep()​

    ​定義在​

    ​Thread​

    ​類?

    因為 ​

    ​wait()​

    ​, ​

    ​notify()​

    ​ 和 ​

    ​notifyAll()​

    ​是鎖級别操作,而鎖是屬于對象的,每一個對象的對象頭中都包含幾位用于儲存目前鎖的狀态的預留,是以鎖是綁定于某一個對象,而不是線程。
  • ​wait​

    ​​/​

    ​notify​

    ​與​

    ​sleep​

    ​的異同點?

    相同:都會使線程進入阻塞狀态,可以響應中斷。

    不同:​

    ​wait​

    ​/​

    ​notify​

    ​必須在同步方法中執行,防止死鎖和永久等待,而​

    ​sleep​

    ​不需要;​

    ​wait​

    ​釋放鎖,​

    ​sleep​

    ​不釋放鎖;​

    ​sleep​

    ​必須指定參數,​

    ​wait​

    ​可不傳參;所屬的類不同,前者屬于​

    ​Object​

    ​類,後者屬于​

    ​Thread​

    ​類。

使用 ​

​wait​

​​/​

​notify​

​ 實作生産者消費者設計模式,生産者往隊列中存放資料,如果隊列滿了會阻塞;消費者從隊列擷取資料,如果隊列空了也會阻塞。

如果隊列中有了資料,生産者會通知消費者去擷取資料;同樣如果隊列中資料不滿,消費者會通知生産者生産資料。

Thread和Object中的重要方法詳解
public class ConsumerProducer {
    public static void main(String[] args) {
        DataStorage storage = new DataStorage();
        Producer producer = new Producer(storage);
        Consumer consumer = new Consumer(storage);
        new Thread(producer).start();
        new Thread(consumer).start();
    }

}
class DataStorage {
    private int maxSize;
    private LinkedList<Date> storage;

    // 初始化存儲隊列
    public DataStorage() {
        this.maxSize = 10;
        this.storage = new LinkedList<>();
    }

    // 生産資料
    public synchronized void put() {
        // 如果隊列中已滿,則進入等待狀态
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("生産者生産資料了,倉庫中有 " + storage.size() + " 條資料");
        // 通知消費者
        notify();
    }

    // 消費資料
    public synchronized void get() {
        // 同理,如果隊列中沒有資料,則進入等待資料的狀态
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 擷取并删除隊列中資料
        System.out.println("消費者取到 " + storage.poll() + " ; 倉庫還剩 " + storage.size() + " 條資料");
        // 消費後必然會有空閑容量,通知生産者
        notify();
    }
}      
Thread和Object中的重要方法詳解

兩個線程交替列印 0~100 的奇偶數

僅通過加鎖​

​synchronized​

​​,分别判斷奇、偶性進行輸出。

該方法雖然能得到正确的輸出,但是效率很低。兩個線程同時在競争鎖,如果同一個線程一直搶到鎖,另一個線程就會一直等待無法接着輸出,這會經曆多餘的 ​​

​while​

​ 循環(隻不過不符合輸出要求不會列印罷了)。

public class PrintOddEvenAlternately {
    private static Object lock = new Object();
    private static int num;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new AlternateRunner(), "OddThread").start();
        Thread.sleep(100);
        new Thread(new AlternateRunner(), "EvenThread").start();
    }

    private static class AlternateRunner implements Runnable {
        @Override
        public void run() {
            while (num <= 100) {
                synchronized (lock) {
                    // int類型變量初始化值為 0,第一次會是偶數線程列印 0
                    System.out.println(Thread.currentThread().getName() + " : " + num++);
                    // 偶(奇)線程在列印一次後,喚醒對方執行列印
                    lock.notify();
                    // 這裡必須再進行一次判斷再進入wait
                    // 否則如果直接進入休眠,而正好100輸出後通過num++變成了101,另外個線程就無法進入while循環進行喚醒
                    // 那麼線程将一直處于等待狀态,程式無法停止運作
                    if (num <= 100) {
                        try {
                            // 進入等待狀态,釋放鎖給對方
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}