Java 基礎(十四)執行緒——下

diamond_lin發表於2019-03-04

上週因為一些事情回了一趟長沙,所以更新晚了幾天。Sorry~

Java 執行緒:執行緒的互動

執行緒互動的基礎知識

首先我們從 Object 類中的三個方法來學習。

方法名 作用
void notify() 喚醒在此物件監視器上等待的單個執行緒
void notifyAll() 喚醒在此物件監視器上等待的所有執行緒
void wait() 使當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法

關於 等待/通知,要記住的關鍵點是:

  • 必須從同步環境內呼叫 wait()、notify()、notifyAll()方法。執行緒不能呼叫物件上的等待或通知方法,除非它擁有那個物件的鎖。
  • wait()、notify()、notifyAll()都是 Object 的例項方法。與每個物件具有鎖一樣,每個物件可以有一個執行緒列表,他們等待來自該訊號。執行緒通過執行物件上的 wait 方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到呼叫物件的 notify 方法為止。如果多個執行緒在同一個物件上等待,則將只選擇一個執行緒(不保證順序)繼續執行。如果沒有執行緒等待,則不採取任何特殊操作。

敲黑板!!!??上面這段話是重點。會用 wait、notify 方法的童鞋先理解這段話,不會用 wait、notify 方法的童鞋請看懂下面的例子再結合例子理解。

public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        thread1.start();
        synchronized (thread1.obj) {
            try {
                System.out.println("等待 thread1 完成計算。。。");
                //執行緒等待
                thread1.obj.wait();
//                thread1.sleep(1000);//思考一下,如果把上面這行程式碼注掉,執行這行程式碼
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("thread1 物件計算的總和是:" + thread1.total);
        }


    }


    public static class Thread1 extends Thread {
        int total;
        public final Object obj = new Object();

        @Override
        public void run() {
            synchronized (obj) {
                for (int i = 0; i < 101; i++) {
                    total += i;
                }
                //(完成計算了)喚醒在此物件監視器上等待的單個執行緒,在本例中執行緒 thread1 被喚醒
                obj.notify();
                System.out.println("計算結束:" + total);
            }

        }
    }
}複製程式碼

以上程式碼的兩個 synchronize 程式碼塊的鎖都用 Thread1 的例項物件也是可以的,這裡為了方便大家理解必須要用同一個鎖,才 new 了一個 Obj 物件。

注意:當在物件上呼叫 wait 方法時,執行該程式碼的執行緒立即放棄它在物件上的鎖。然而呼叫 notify 時,並不意味著這時執行緒會放棄其鎖。如果執行緒仍然在完成同步程式碼,則執行緒在同步程式碼結束之前不會放棄鎖。因此,呼叫了 notify 並不意味著這時該鎖變得可用

上面的執行結果忘記貼上出來了,童鞋們自行測試吧~

多個執行緒在等待一個物件鎖時使用 notifyAll()

在多數情況下,最好通知等待某個物件的所有執行緒。如果這也做,可以在物件使用 notifyAll()讓所有在此物件上等待的執行緒重新活躍。

public class ThreadMutual extends Thread{
    int total;

    public static void main(String[] args) {
        ThreadMutual t = new ThreadMutual();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        t.start();

    }

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 11; i++) {
                total += i;
            }
            //(完成計算了)喚醒在此物件監視器上等待的單個執行緒,在本例中執行緒A被喚醒
            System.out.println("計算結束:" + total);
            notifyAll();

        }

    }


    public static class Thread1 extends Thread {

        private final ThreadMutual lock;

        public Thread1(ThreadMutual lock){
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "得到結果:"+lock.total);
            }

        }
    }
}

計算結束:55
Thread-5得到結果:55
Thread-6得到結果:55
Thread-4得到結果:55
Thread-3得到結果:55
Thread-2得到結果:55
Thread-1得到結果:55複製程式碼

注意:上面的程式碼如果執行緒 t 如果第一個 start,則會發生很多意料之外的情況,比如說notifyAll 已經執行了,wait 的程式碼還沒執行。然後, 就造成了某個執行緒一直處於等待狀態。
通常,解決上面問題的最佳方式是利用某種迴圈,該迴圈檢查某個條件表示式,只有當正在等待的事情還沒有發生的情況下,它才繼續等待。

Java 執行緒:執行緒的排程與休眠

Java 執行緒的排程是 Java 多執行緒的核心,只有良好的排程,才能充分發揮系統的效能,提高程式的執行效率。

這裡要明確一點,不管程式設計師怎麼編寫排程,只能最大限度的影響執行緒執行的次序,而不能做到精準控制。

執行緒休眠的目的是時執行緒讓出 CPU 的最簡單的做法之一,執行緒休眠時,會將 CPU資源交給其他執行緒,以便能輪換執行,當休眠一定時間後,執行緒會甦醒,進入準備狀態等待執行。

執行緒休眠的方法是 Thread.sleep(),是個靜態方法,那個執行緒呼叫了這個方法,就睡眠這個執行緒。

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        Thread t2 = new Thread(new MyRunnable());
        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("執行緒1第" + i + "次執行!");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("執行緒2第" + i + "次執行!");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}複製程式碼

執行結果:

執行緒2第0次執行!
執行緒1第0次執行!
執行緒1第1次執行!
執行緒2第1次執行!
執行緒1第2次執行!
執行緒2第2次執行!複製程式碼

Java 執行緒:執行緒的排程-優先順序

與執行緒休眠類似,執行緒的優先順序仍然無法保證執行緒的執行次序。只不過,優先順序高的執行緒獲取 CPU 資源的概率較大,低優先順序的並非沒有機會執行。

執行緒的優先順序用1-10之間的整數表示,數值越大優先順序越高,預設為5.

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        Thread t2 = new Thread(new MyRunnable());
        t1.setPriority(10);
        t2.setPriority(1);

        t2.start();
        t1.start();
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("執行緒1第" + i + "次執行!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("執行緒2第" + i + "次執行!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}複製程式碼

執行結果:

執行緒2第0次執行!
執行緒1第0次執行!
執行緒1第1次執行!
執行緒2第1次執行!
執行緒1第2次執行!
執行緒2第2次執行!
執行緒1第3次執行!
執行緒2第3次執行!
執行緒1第4次執行!
執行緒2第4次執行!
執行緒1第5次執行!
執行緒2第5次執行!
執行緒1第6次執行!
執行緒2第6次執行!
執行緒1第7次執行!
執行緒2第7次執行!
執行緒1第8次執行!
執行緒2第8次執行!
執行緒1第9次執行!
執行緒2第9次執行!複製程式碼

我們可以看到,每隔50ms 列印一次,優先順序高的執行緒1大概率先執行。

Java 執行緒:執行緒的排程-讓步

執行緒的讓步含義就是使當前執行著的執行緒讓出 CPU 資源,但是給誰不知道,只是讓出,執行緒回到可執行狀態。

執行緒讓步使用的是靜態方法 Thread.yield(),用法和 sleep 一樣,作用的是當前執行執行緒。

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        Thread t2 = new Thread(new MyRunnable());

        t2.start();
        t1.start();
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("執行緒1第" + i + "次執行!");
        }
    }
}

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("執行緒2第" + i + "次執行!");
            Thread.yield();
        }
    }
}複製程式碼

執行結果:

執行緒1第0次執行!
執行緒2第0次執行!
執行緒1第1次執行!
執行緒2第1次執行!
執行緒1第2次執行!
執行緒1第3次執行!
執行緒1第4次執行!
執行緒1第5次執行!
執行緒1第6次執行!
執行緒1第7次執行!
執行緒1第8次執行!
執行緒1第9次執行!
執行緒2第2次執行!
執行緒2第3次執行!
執行緒2第4次執行!
執行緒2第5次執行!
執行緒2第6次執行!
執行緒2第7次執行!
執行緒2第8次執行!
執行緒2第9次執行!複製程式碼

Java 執行緒:執行緒的排程-合併

執行緒的合併的含義就是將幾個並行執行緒的執行緒合併為一個單執行緒,應用場景是當一個執行緒必須等待另一個執行緒執行完畢才能執行,使用 join 方法。

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        t1.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主執行緒第" + i + "次執行!");
            if (i > 2) try {
                //t1執行緒合併到主執行緒中,主執行緒停止執行過程,轉而執行t1執行緒,直到t1執行完畢後繼續。
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("執行緒1第" + i + "次執行!");
        }
    }
}複製程式碼

執行結果:

主執行緒第0次執行!
主執行緒第1次執行!
主執行緒第2次執行!
主執行緒第3次執行!
執行緒1第0次執行!
執行緒1第1次執行!
執行緒1第2次執行!
主執行緒第4次執行!
主執行緒第5次執行!
主執行緒第6次執行!
主執行緒第7次執行!
主執行緒第8次執行!
主執行緒第9次執行!複製程式碼

不逼逼了,執行緒 join 只有第一次有效。這裡我也很懵逼,我以為執行緒1第***這句話的列印次數應該是(10-3)*3 次的。
這裡我們來回顧一下上篇文章說的執行緒的基本知識,執行緒是死亡之後就不能重新啟動了對吧。我們再來理解一下 join 的概念當一個執行緒必須等待另一個執行緒執行完畢才能執行,我們在主執行緒中join 執行緒 t1,所以直到 t1執行完畢,才能再次執行主執行緒。當 i=4 的時候再次執行 t1.join()時,t1 執行緒已經是處於死亡狀態,所以不會再次執行 run 方法。因此 t1執行緒裡面 run 方法的列印語句只執行了三次。為了驗證我們的猜想,我建議去閱讀以下原始碼。

以下是 Java8 Thread#join() 方法的原始碼。

public final void join() throws InterruptedException {
    this.join(0L);
}

public final synchronized void join(long var1) throws InterruptedException {
    long var3 = System.currentTimeMillis();
    long var5 = 0L;
    if(var1 < 0L) {
        throw new IllegalArgumentException("timeout value is negative");
    } else {
        if(var1 == 0L) {
            while(this.isAlive()) {
                this.wait(0L);
            }
        } else {
            while(this.isAlive()) {
                long var7 = var1 - var5;
                if(var7 <= 0L) {
                    break;
                }

                this.wait(var7);
                var5 = System.currentTimeMillis() - var3;
            }
        }

    }
}

public final native boolean isAlive();複製程式碼

我們可以看到 t1呼叫 join 方法的時候呼叫了過載的方法,並且傳了引數0,然後關鍵來了while(this.isAlive())條件一直滿足的情況下,呼叫了 this.wait(0),這裡的 this 相當於物件 t1。

我們來思考一下,t1.wait()到底是哪個執行緒需要 wait?給你們三秒鐘時間。

3…
2…
1…

好了,我直接說了,大家記住,t1只是個物件,這裡不能當成是 t1執行緒 wait,主執行緒裡面通過物件 t1作為鎖,並呼叫了 wait 方法,其實是主執行緒 wait 了。while 的判斷條件是執行緒 t1.isAlive(),注意,這裡是判斷執行緒 t1是否存活,如果存活,則主執行緒一直 wait(0),直到 t1 執行緒執行結束死亡。這樣可以瞭解了吧,再來思考一下如果在 Android 主執行緒裡面呼叫 join 方法可能會造成什麼問題?

這個問題很簡單,我就不說答案了。

Java 執行緒:執行緒的排程-守護執行緒

守護執行緒與普通執行緒寫法上基本沒啥區別,呼叫執行緒物件的方法 setDaemon(true),則可以將其設定為守護執行緒。

守護執行緒的使用情況較少,但並非無用,舉例來說,JVM 的垃圾回收、記憶體管理等執行緒都是守護執行緒。還有就是在做資料庫應用的時候,使用資料庫連線池,連線池本身也包含著很多後臺現場,監控連線個數、超時時間、狀態等等。

  • setDaemon(boolean on)

將該執行緒標記為守護執行緒或使用者執行緒。當正在執行的執行緒都是守護執行緒時,Java虛擬機器退出。該方法必須在啟動執行緒前呼叫。

public class ThreadDaemon {

    public static void main(String[] args) {
        Thread t1 = new MyCommon();
        Thread t2 = new Thread(new MyDaemon());
        t2.setDaemon(true);        //設定為守護執行緒
        t2.start();
        t1.start();
    }
}

class MyCommon extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("執行緒1第" + i + "次執行!"+"——————活著執行緒數量:"+Thread.currentThread().getThreadGroup().activeCount());
            try {
                Thread.sleep(7);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyDaemon implements Runnable {
    public void run() {
        for (long i = 0; i < 9999999L; i++) {
            System.out.println("後臺執行緒第" + i + "次執行!");
            try {
                Thread.sleep(7);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}複製程式碼

啥也別說了,來看結果吧:

後臺執行緒第0次執行!
執行緒1第0次執行!——————活著執行緒數量:4
後臺執行緒第1次執行!
執行緒1第1次執行!——————活著執行緒數量:4
後臺執行緒第2次執行!
執行緒1第2次執行!——————活著執行緒數量:4
後臺執行緒第3次執行!
執行緒1第3次執行!——————活著執行緒數量:4
後臺執行緒第4次執行!
執行緒1第4次執行!——————活著執行緒數量:4
後臺執行緒第5次執行!複製程式碼

從上面的結果我們可以看出,前臺執行緒是包裝執行完畢的,後臺執行緒還沒有執行完畢就退出了。也就是說除了守護執行緒以為的其他執行緒執行完之後,守護執行緒也就結束了。

然後,我們來看看,為什麼活著的執行緒數量會是4,明明只開了兩個子執行緒呀,加上 main 執行緒也才三個,那再加一個垃圾回收執行緒吧哈哈哈哈。

這個問題也是我在學習過程中困擾了很久的問題。之前糾結的是,main 執行緒執行完了,如果還有子執行緒在執行。那麼 main 執行緒到底是先結束還是等待子執行緒執行結束之後再結束?main 執行緒結束是不是代表程式退出?

然後我就 Debug 執行緒池裡面所有的執行緒,發現裡面有一個叫 DestoryJavaVM 的執行緒,然後我也不知道這是個什麼東西,遂問了一下度娘,度娘告訴我~

DestroyJavaVM:main執行完後呼叫JNI中的jni_DestroyJavaVM()方法喚起DestroyJavaVM執行緒。 JVM在Jboss伺服器啟動之後,就會喚起DestroyJavaVM執行緒,處於等待狀態,等待其它執行緒(java執行緒和native執行緒)退出時通知它解除安裝JVM。執行緒退出時,都會判斷自己當前是否是整個JVM中最後一個非deamon執行緒,如果是,則通知DestroyJavaVM執行緒解除安裝JVM

大概就是醬紫吧,4個執行緒分別是兩個我手動開的子執行緒,一個DestroyJavaVM ,還有一個大概是垃圾回收執行緒吧,哈哈哈哈,如果不對,請務必拍磚~

Java 執行緒:執行緒的同步-同步方法同步塊

上一篇已經就同步問題做了詳細的講解。

對於多執行緒來說,不管任何程式語言,生產者消費者模型都是最經典的。這裡我們拿一個生產者消費者模型來深入學習吧~

實際上,應該是“生產者-消費者-倉儲”模型,離開了倉儲,生產者消費者模型就顯得沒有說服力。

對於此模型,應該明確以下幾點:

  • 生產者僅僅在倉儲未滿時候生產,倉滿則停止生產
  • 消費者僅僅在倉儲有產品時候才能消費,倉空則等待
  • 當消費者發現倉儲沒產品可消費時候會通知生產者生產
  • 生產者在生產出可消費產品時候,應該通知等待的消費者去消費

此模型將要的知識點,我們上面都學過了,直接擼程式碼吧~

public class Model {
    public static void main(String[] args) {
        Godown godown = new Godown(30);
        Consumer c1 = new Consumer(50, godown);
        Consumer c2 = new Consumer(20, godown);
        Consumer c3 = new Consumer(30, godown);
        Producer p1 = new Producer(10, godown);
        Producer p2 = new Producer(10, godown);
        Producer p3 = new Producer(10, godown);
        Producer p4 = new Producer(10, godown);
        Producer p5 = new Producer(10, godown);
        Producer p6 = new Producer(10, godown);
        Producer p7 = new Producer(40, godown);

        c1.start();
        c2.start();
        c3.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();
        p6.start();
        p7.start();
    }
}

/**
 * 倉庫
 */
class Godown {
    public static final int max_size = 100;//最大庫存量
    public int curnum;    //當前庫存量

    Godown() {
    }

    Godown(int curnum) {
        this.curnum = curnum;
    }

    /**
     * 生產指定數量的產品
     *
     * @param neednum
     */
    public synchronized void produce(int neednum) {
        //測試是否需要生產
        while (neednum + curnum > max_size) {
            System.out.println("要生產的產品數量" + neednum + "超過剩餘庫存量" + (max_size - curnum) + ",暫時不能執行生產任務!");
            try {
                //當前的生產執行緒等待
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //滿足生產條件,則進行生產,這裡簡單的更改當前庫存量
        curnum += neednum;
        System.out.println("已經生產了" + neednum + "個產品,現倉儲量為" + curnum);
        //喚醒在此物件監視器上等待的所有執行緒
        notifyAll();
    }

    /**
     * 消費指定數量的產品
     *
     * @param neednum
     */
    public synchronized void consume(int neednum) {
        //測試是否可消費
        while (curnum < neednum) {
            try {
                //當前的生產執行緒等待
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //滿足消費條件,則進行消費,這裡簡單的更改當前庫存量
        curnum -= neednum;
        System.out.println("已經消費了" + neednum + "個產品,現倉儲量為" + curnum);
        //喚醒在此物件監視器上等待的所有執行緒
        notifyAll();
    }
}

/**
 * 生產者
 */
class Producer extends Thread {
    private int neednum;                //生產產品的數量
    private Godown godown;            //倉庫

    Producer(int neednum, Godown godown) {
        this.neednum = neednum;
        this.godown = godown;
    }

    public void run() {
        //生產指定數量的產品
        godown.produce(neednum);
    }
}

/**
 * 消費者
 */
class Consumer extends Thread {
    private int neednum;                //生產產品的數量
    private Godown godown;            //倉庫

    Consumer(int neednum, Godown godown) {
        this.neednum = neednum;
        this.godown = godown;
    }

    public void run() {
        //消費指定數量的產品
        godown.consume(neednum);
    }
}

已經消費了20個產品,現倉儲量為10
已經生產了10個產品,現倉儲量為20
已經生產了10個產品,現倉儲量為30
已經生產了10個產品,現倉儲量為40
已經生產了10個產品,現倉儲量為50
已經消費了30個產品,現倉儲量為20
已經生產了40個產品,現倉儲量為60
已經生產了10個產品,現倉儲量為70
已經消費了50個產品,現倉儲量為20
已經生產了10個產品,現倉儲量為30複製程式碼

在本例中,要說明的是當發現不能滿足生產者或消費條件的時候,呼叫物件的 wait 方法,wait 方法的作用是釋放當前執行緒的所獲得的鎖,並呼叫物件的 notifyAll()方法,通知(喚醒)該物件上其他等待的執行緒,使其繼續執行。這樣,整個生產者、消費者執行緒得以正確的協作執行。

Java 執行緒:volatile 關鍵字

Java 語言包含兩種內在同步機制:同步塊(方法)和 volatile 變數。這兩種機制的提出都是為了實現程式碼執行緒的安全性。其中 volatile 變數的同步性較差(但有時它更簡單並且開銷更地),並且其使用也容易出錯。

首先考慮一個問題,為什麼變數需要volatile來修飾呢?
要搞清楚這個問題,首先應該明白計算機內部都做什麼了。比如做了一個i++操作,計算機內部做了三次處理:讀取-修改-寫入。
同樣,對於一個long型資料,做了個賦值操作,在32系統下需要經過兩步才能完成,先修改低32位,然後修改高32位。

假想一下,當將以上的操作放到一個多執行緒環境下操作時候,有可能出現的問題,是這些步驟執行了一部分,而另外一個執行緒就已經引用了變數值,這樣就導致了讀取髒資料的問題。

通過這個設想,就不難理解volatile關鍵字了。

更多的內容,請參考《Java理論與實踐:正確使用 Volatile 變數》一文,寫得很好。

參考資料

Java執行緒詳解
JDK 中文文件

推薦

這兩天在逛 github 的時候無意發現了這個專案LeetCode 演算法與 java 解決方案,每天上班之前刷一個演算法題,真的巨爽,強烈推薦想打好基礎去大廠的小夥伴們一起刷題。

相關文章