併發編程線程5大狀態切換時機分析及sleep,join,wait,notify,notifyAll,yield剖析

線程5大狀態分析


上圖是線程從創建到消亡的一個切換過程。下面我們簡單類分析每一個狀態。

  1. 新建狀態:新建狀態具體是指調用new Thread()創建出線程對象,但是還沒有調用start方法的這段時間。前面的一篇文章《Java虛擬機剖析之內存區域,內存的溢出,泄漏》一文中有說到,每一個線程都會有自己的私有內存區域。處於新建狀態下的線程,此時還未分配系統資源,也即是還沒有分配到私有內存。
  2. 就緒狀態:start方法剛被調用的一段時機。處於當前狀態的線程已經分配到所需資源,但是還沒有獲得CPU使用權,在此狀態的線程會相互競爭CPU使用權。
  3. 運行狀態:被os選中,獲得CPU使用權,開始執行任務,也即是開始運行run/main方法。
  4. 阻塞狀態:在執行任務的過程中由於一些原因導致線程阻塞(後面會重點講阻塞狀態,這也是本文重點)。
  5. 終止狀態:任務執行完畢(run/main方法執行完畢)或者線程異常終止。

阻塞狀態,阻塞原因

阻塞狀態對我們開發人員來說是最關鍵的一個狀態,因爲我們能通過各種造成阻塞的手段合理的調度指定的線程執行特定的任務,能準確的控制每一個任務執行的時機。造成阻塞的原因大致爲下面4種:

  1. 調用sleep/join方法
  2. 調用wait方法
  3. 訪問臨界資源時(如synchronized字段修飾的方法或者代碼塊),等待競爭鎖對象所有權
  4. I/O導致阻塞(比如:等待用戶輸入)
其中這四種方式中1,2兩種阻塞可以中斷,3,4兩種不會對喚醒線程的操作有反應。

關鍵方法分析sleep,join,wait,notify,notifyAll,yield

  1. sleep方法是Thread的靜態方法,當該方法調用時,會讓調用的線程進入阻塞狀態,直到歷時sleep的參數時間後喚醒該線程。當線程在持有臨界資源對象鎖持有權時調用sleep方法,線程進入阻塞,不會釋放所持有的對象鎖持有權,容易造成死鎖。
  2. join方法是Thread的公有方法,歸對象所有。在A線程中調用B線程的join方,A線程進入阻塞狀態,知道B線程的任務執行完畢纔會喚醒A線程繼續執行未完成的任務。
  3. wait方法是超類Object的方法,該方跟notify/notifyAll配套使用,這一對方法用於線程間通訊控制併發。這一對方法需要線程在持有臨界資源對象鎖所有權的情況下才能調用,否則拋出IllegalMonitorStateException。調用wait方法的線程交出CPU使用權進入阻塞狀態,需等到競爭同一鎖對相的線程調用notify/notifyAll方法纔會被喚醒(notify是在所有相關的處於等待狀態的線程中隨機選擇一個線程喚醒,notifyAll是將所有相關的處於等待狀態的線程全部喚醒),然後進入鎖池,重新競爭對象鎖的持有權。wait方法調用的線程會釋放臨界資源對象鎖的持有權。
  4. yield方法是Thread的靜態方法,調用該方法的線程相當於線程的時間片用完,回到就緒狀態,重新競爭跟同等優先級線程CPU使用權(直觀的說就是A,B線程爲同等優先級線程,同時開始競爭CPU使用權。A線程獲取到CPU使用權進入運行狀態,正在執行任務,run方法運行到一半的時候調用了yield方法,此時A線程將會跟B線程再一次平等繼續競爭CPU使用權,如果A得到使用權,會繼續剛纔完成到一半的任務繼續未完成的任務執行完)
  5. 另外suspend和resume方法配對使用,跟wait和notify/notifyAll這一對差不多,調用suspend的線程進入阻塞,需要等到對應的resume方法被調用纔會喚醒。但是有一個區別是suspend方法調用的線程會釋放對象鎖的持有權。這對方法不經常用,所以略過。。。

sleep和wait方法的區別及特性驗證

共同點:

  1. 都能是線程進入阻塞狀態
  2. 都可以設置阻塞時間
不同點:

  1. 喚醒方式不同,sleep方法是在指定的時間之後自動喚醒。wait必須等到別的相關線程調用notify/notifyAll纔會喚醒(當然了,wait(long time)方法也能指定等待時間,等待時間到了之後還未調用notify/notifyAll將會自動喚醒,特殊情況)
  2. sleep調用時不一定需要線程持有臨界資源的對象鎖,但是wait方法的調用線程必須持有臨界資源的對象鎖,否則會拋出異常。
  3. 調用sleep方法進入阻塞狀態後不會釋放持有的對象鎖,但是wait方法會釋放所持有的對象鎖(主要區別)
這些區別導致了控制線程的方式完全不同,使用的場景也不相同,我們用實例能直觀的驗證sleep和wait的特性以及區別。下面用生產者/消費者模式進行說明驗證,其實真正的生產者/消費者模式需要用的是wait方法,用sleep方法有可能造成死鎖,這兒只是爲了證明兩種方法的區別!

工廠:

package com.example;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class FactoryClass {
    //用於存放產品的容器
    public List<String> products = new ArrayList<>();

    /**
     * 生產量是否已經達到飽滿
     *
     * @return true 表示已經飽滿
     */
    private boolean isFull() {
        return products.size() >= 40 ? true : false;
    }

    /**
     * 庫存是否已經爲0
     *
     * @return true 表示已售完
     */
    private boolean isEmpty() {
        return products.size() <= 0 ? true : false;
    }

    /**
     * 商店賣東西,調用wait進入阻塞
     *
     * @param consumer 消費者的名字
     */
    public void sell1(String consumer) {
        synchronized (products) {
            while (isEmpty()) {
                System.out.print("The goods is sold out ," + consumer + " need to wait a while !\n");
                try {
                    products.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(consumer + " has been wait for 2 seconds,try to get goods !\n");
            }
            System.out.print(consumer + " bought the goods !\n");
            products.remove(products.size() - 1);
            products.notifyAll();
        }
    }

    /**
     * 商店賣東西,調用sleep進入阻塞
     *
     * @param consumer 消費者的名字
     */
    public void sell2(String consumer) {
        synchronized (products) {
            while (isEmpty()) {
                System.out.print("The goods is sold out ," + consumer + " need to wait for a while !\n");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(consumer + " has been waiting for 2 seconds , try to get goods ! \n");
            }
            products.remove(products.size() - 1);
            System.out.print(consumer + " bought the goods \n");
        }
    }

    /**
     * 工人生產,調用wait進入阻塞
     *
     * @param employee 工人的名字
     */
    public void produce1(String employee) {
        synchronized (products) {
            while (isFull()) {
                System.out.print("the warehouse is full ," + employee + " can have a rest for 2 seconds !\n");
                try {
                    products.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(employee + " has been had a rest for 2 seconds, request to start working !\n");
            }

            products.add("product");
            System.out.print(employee + " has been produced a goods !\n");
            products.notifyAll();
        }
    }

    /**
     * 工人生產,調用sleep 進入阻塞
     *
     * @param employee 工人的名字
     */
    public void produce2(String employee) {
        synchronized (products) {
            while (isFull()) {
                System.out.print("the warehouse is full ," + employee + " can have a rest for 2 seconds !\n");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(employee + " has been had a rest for 2 seconds, request to start working !\n");
            }
            products.add("product");
            System.out.print(employee + " has been produced a goods !\n");
        }
    }
}
員工任務類(線程):

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class ProduceRun implements Runnable {
    private FactoryClass factory;

    public ProduceRun(FactoryClass factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        Employee employee = null;
        for (int i = 0; i < 60; i++) {
            employee = new Employee(factory);
            employee.setName("employee-" + i);
            employee.work();
        }
    }
}
消費者任務類(線程):

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class ConsumeRun implements Runnable {

    private FactoryClass factory;

    public ConsumeRun(FactoryClass factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        Consumer consumer = null;
        for (int i = 0; i < 60; i++) {
            consumer = new Consumer(factory);
            consumer.setName("consumer->" + i);
            consumer.shopping();
        }
    }
}

主程序入口:

package com.example;

public class MyClass {

    public static void main(String[] args0) throws InterruptedException {

        FactoryClass factory = new FactoryClass();
        ProduceRun produceRun = new ProduceRun(factory);
        ConsumeRun consumeRun = new ConsumeRun(factory);
        new Thread(consumeRun).start();
        new Thread(produceRun).start();
    }
}
員工:

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class Employee {

    //工人工作的工廠
    private FactoryClass factory;
    //工人姓名
    private String name;

    public Employee(FactoryClass factory) {
        this.factory = factory;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 幹活,調試wait結果調用produce1方法,調試sleep,調用produce2
     */
    public void work() {
        factory.produce1(name);
        //  factory.produce2(name);
    }
}

消費者:

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class Consumer {
    //消費的商店
    FactoryClass factory;
    //消費者的姓名
    String name;

    public Consumer(FactoryClass factory) {
        this.factory = factory;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 購物,運行wait結果調用shell1方法,sleep調用sell2方法。
     */
    public void shopping() {
        factory.sell1(name);
        // factory.sell2(name);
    }
}

wait方法運行結果:


sleep方法運行結果:


對於第一,第二不同點,就不具體驗證了,有興趣可以自己寫兩個小Demo便可以驗證。這兒具體驗證第三點。分析上面的兩個結果,很清楚的看出一個不同之處,對於wait方法的運行結果來說,當商品賣完的時候消費者進入等待狀態,員工能及時生產出商品給消費者,然而對於sleep方法的運行結果來說,當商品賣完的時候,消費者一直在處於等待->嘗試得到商品->等待->嘗試得到商品.....這樣的一個死循環,並且員工並不能去生產新的商品給消費者。

爲什麼會出現這種情況呢?其實很簡單,首先wait方法導致消費者線程進入阻塞狀態的時候,釋放了CPU以及對象鎖,此時兩個線程共同競爭CPU使用權和對象鎖的持有權,當員工線程得到CPU的使用權之後執行任務,再得到對象鎖之後,開始生產商品,商品完成之後,喚醒所有阻塞的線程。然後再一次釋放CPU和對象鎖。然後他們兩個再一次平等競爭CPU和對象鎖,一直這樣循環直到兩個線程任務都完成。其次sleep方法導致消費者線程進入阻塞的時候,它只是釋放了CPU,並沒有釋放對象鎖,此時兩個線程只能平等競爭CPU,而員工線程是不能得到對象鎖的。員工線程得到CPU使用權之後開始運行任務,但是因爲對象鎖被消費者線程佔有,並沒有釋放,員工線程得不到對象鎖,所以無法訪問臨界資源(synchronized代碼塊),所以會進入上面提到的第三種阻塞(也即是競爭對象鎖造成阻塞)。這兩個線程一直循環着這個過程,導致員工一直無法幹活,消費者一直在處於等待->嘗試得到商品->等待->嘗試得到商品.....這樣的一個死循環。

重點:對上面的區別驗證,其實只需要弄清楚下面這兩點就不難理解!

  1. 線程競爭到CPU使用僅僅只能開始執行任務(執行run方法),並不一定能完成任務。爲什麼這麼說呢?因爲上面對阻塞狀態,阻塞原因小節中提到阻塞原因的第3條是競爭臨界資源對象鎖造成阻塞,所以有可能線程開始執行任務了,但是在等待對象鎖導致阻塞,從而無法完成任務。上面的sleep方法死鎖了就是這個原因。
  2. 線程競爭到CPU的使用權之後需要得到臨界資源的訪問權,也即是拿到對象鎖的使用權,才能訪問臨界資源,才能完成任務(把run方法走完)。

join方法特性驗證

上面已經說過了join方法的特性:該API能讓線程進入阻塞,並且會等到另一個線程執行完之後纔會喚醒該線程繼續未完成的任務。下面看代碼和運行結果驗證。

程序入口:

package com.example;

public class MyClass {

    public static void main(String[] args0) throws InterruptedException {

        JoinTh2 joinTh2 = new JoinTh2();
        joinTh2.setName("Thread_A");
        JoinTh1 joinTh1 = new JoinTh1(joinTh2);
        joinTh1.setName("Thread_B");
        joinTh1.start();
    }
}
線程A:

package com.example;

/**
 * Created by PICO-USER on 2017/11/8.
 */

public class JoinTh1 extends Thread {
    Thread thread;

    public JoinTh1(Thread thread) {
        this.thread = thread;
    }

    @Override
    public void run() {
        super.run();
        String name = getName();
        for (int i = 0; i < 5; i++) {
            System.out.print(name + " is performing tasks ! i = " + i + "\n");
            if (i == 2 && thread != null) {
                try {
                    System.out.print(name + " will start another thread " + thread.getName()
                            + ", the time is :" + MyUtils.getCurrentTime() + " !\n");
                    thread.start();
                    thread.join();
                    System.out.print(name + " continue to perform outstanding tasks , the time is :" + MyUtils.getCurrentTime() + "!\n");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

線程B:

package com.example;

/**
 * Created by PICO-USER on 2017/11/8.
 */

public class JoinTh2 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.print(getName() + " start to perform the tasks !\n");
        try {
            //休眠3秒,模擬一個耗時操作,以便看出來joinTh1會不會等到本線程完成任務再繼續執行未完成任務!
            sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.print(getName() + " to complete all tasks, the time is :" + MyUtils.getCurrentTime() + "!\n");
    }
}

調用join方法時的結果:


不調用join方法時結果:


結果對比驗證分析:第一次調用在A中調用了B的join方法,第二次將線程A中“thread.join();”這行代碼註釋掉,也即是說不調用join方法。此案例我故意在B中做了一個休眠3秒的操作,目的就是達到一個耗時的操作讓“A線程等待B線程執行完成之後再開始恢復任務執行”的效果更明顯,更突出。從時間上看,調用join方法後,A線程並沒有第一時間開始執行i=3的任務,而是停了3秒鐘纔開始執行。但是不調用的時候,幾乎是同一時間執行了i=2和i=3的操作。驗證成功!




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章