永遠不要在循環之外調用wait方法

1. 前言

隨着摩爾定律的失效,Amdahl定律成爲了多核計算機性能發展的指導。對於現在的java程序員們來說,併發編程越來越重要和習以爲常。很慚愧和恐慌的是我對java的併發編程一直是隻知道概念,入門都不算。最近工作需要,開始認真學習java併發編程。先找了一本簡單的電子書《Java7併發編程實戰手冊》開始看。剛剛看到簡單的生產者消費者問題,在書中給出的代碼中,有一點不理解:爲什麼wait()語句要放在while循環之內?經過網上搜索以及翻看《effective java》第二版。終於明白了一些。特此記錄下來。

2. 生產者消費者代碼

生產者消費者代碼如下:
數據存儲類:EventStorage:(get和set標記爲同步方法,並使用了wait和notify機制)


import java.util.Date;
import java.util.LinkedList;
import java.util.List;

/**
 * This class implements an Event storage. Producers will storage
 * events in it and Consumers will process them. An event will
 * be a java.util.Date object
 *
 */
public class EventStorage {

    /**
     * Maximum size of the storage
     */
    private int maxSize;
    /**
     * Storage of events
     */
    private List<Date> storage;

    /**
     * Constructor of the class. Initializes the attributes.
     */
    public EventStorage(){
        maxSize=10;
        storage=new LinkedList<>();
    }

    /**
     * This method creates and storage an event.
     */
    public synchronized void set(){
            while (storage.size()>=maxSize){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            storage.add(new Date());
            System.out.printf("Set: %d\n",storage.size());
            notify();
    }

    /**
     * This method delete the first event of the storage.
     */
    public synchronized void get(){
            while (storage.size()==0){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.printf("Get: %d: %s\n",storage.size(),((LinkedList<?>)storage).poll());
            notify();
    }

}

生產者類:Producer:(調用EventStorage類中的set方法存入數據)

/**
 * This class implements a producer of events.
 *
 */
public class Producer implements Runnable {

    /**
     * Store to work with
     */
    private EventStorage storage;

    /**
     * Constructor of the class. Initialize the storage.
     * @param storage The store to work with
     */
    public Producer(EventStorage storage){
        this.storage=storage;
    }

    /**
     * Core method of the producer. Generates 100 events.
     */
    @Override
    public void run() {
        for (int i=0; i<100; i++){
            storage.set();
        }
    }
}

消費者類:Consumer:(調用EventStorage類中的get方法取出數據)

/**
 * This class implements a consumer of events.
 *
 */
public class Consumer implements Runnable {

    /**
     * Store to work with
     */
    private EventStorage storage;

    /**
     * Constructor of the class. Initialize the storage
     * @param storage The store to work with
     */
    public Consumer(EventStorage storage){
        this.storage=storage;
    }

    /**
     * Core method for the consumer. Consume 100 events
     */
    @Override
    public void run() {
        for (int i=0; i<100; i++){
            storage.get();
        }
    }

}

主類:Main:(分別啓動一個生產者和一個消費者線程)

/**
 * Main class of the example
 */
public class Main {

    /**
     * Main method of the example
     */
    public static void main(String[] args) {

        // Creates an event storage
        EventStorage storage=new EventStorage();

        // Creates a Producer and a Thread to run it
        Producer producer=new Producer(storage);
        Thread thread1=new Thread(producer);

        // Creates a Consumer and a Thread to run it
        Consumer consumer=new Consumer(storage);
        Thread thread2=new Thread(consumer);

        // Starts the thread
        thread2.start();
        thread1.start();
    }

}

運行截圖如下所示:

這裏寫圖片描述

3. 永遠不要在循環之外調用wait方法

《Effective Java》第二版中文版第69條244頁位置對這一點說了一頁,我看着一知半解。我能理解的一點是:對於從wait中被notify的進程來說,它在被notify之後還需要重新檢查是否符合執行條件,如果不符合,就必須再次被wait,如果符合才能往下執行。所以:wait方法應該使用循環模式來調用。按照上面的生產者和消費者問題來說:錯誤情況一:如果有兩個生產者A和B,一個消費者C。當存儲空間滿了之後,生產者A和B都被wait,進入等待喚醒隊列。當消費者C取走了一個數據後,如果調用了notifyAll(),注意,此處是調用notifyAll(),則生產者線程A和B都將被喚醒,如果此時A和B中的wait不在while循環中而是在if中,則A和B就不會再次判斷是否符合執行條件,都將直接執行wait()之後的程序,那麼如果A放入了一個數據至存儲空間,則此時存儲空間已經滿了;但是B還是會繼續往存儲空間裏放數據,錯誤便產生了。錯誤情況二:如果有兩個生產者A和B,一個消費者C。當存儲空間滿了之後,生產者A和B都被wait,進入等待喚醒隊列。當消費者C取走了一個數據後,如果調用了notify(),則A和B中的一個將被喚醒,假設A被喚醒,則A向存儲空間放入了一個數據,至此空間就滿了。A執行了notify()之後,如果喚醒了B,那麼B不會再次判斷是否符合執行條件,將直接執行wait()之後的程序,這樣就導致向已經滿了數據存儲區中再次放入數據。錯誤產生。
下面是錯誤情況二的演示代碼。根據第二節的代碼修改而來:
數據存儲類:EventStorage:(set中使用if代替while判斷執行條件)

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

/**
 * This class implements an Event storage. Producers will storage
 * events in it and Consumers will process them. An event will
 * be a java.util.Date object
 *
 */
public class EventStorage {

    /**
     * Maximum size of the storage
     */
    private int maxSize;
    /**
     * Storage of events
     */
    private List<Date> storage;

    /**
     * Constructor of the class. Initializes the attributes.
     */
    public EventStorage(){
        maxSize=10;
        storage=new LinkedList<>();
    }

    /**
     * This method creates and storage an event.
     */
    public synchronized void set(){
            if (storage.size()>=maxSize){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            storage.add(new Date());
            System.out.printf("Set: %d\n",storage.size());
            notify();
    }

    /**
     * This method delete the first event of the storage.
     */
    public synchronized void get(){
            while (storage.size()==0){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.printf("Get: %d: %s\n",storage.size(),((LinkedList<?>)storage).poll());
            notify();
    }

}

生產者類:Producer:(調用EventStorage類中的set方法存入數據,沒有修改)
消費者類:Consumer:(調用EventStorage類中的get方法取出數據,在run方法中加入了一個1ms的休眠)

import java.util.concurrent.TimeUnit;

/**
 * This class implements a consumer of events.
 *
 */
public class Consumer implements Runnable {

    /**
     * Store to work with
     */
    private EventStorage storage;

    /**
     * Constructor of the class. Initialize the storage
     * @param storage The store to work with
     */
    public Consumer(EventStorage storage){
        this.storage=storage;
    }

    /**
     * Core method for the consumer. Consume 100 events
     */
    @Override
    public void run() {
        for (int i=0; i<100; i++){
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            storage.get();
        }
    }

}

主類:Main:(啓動了兩個生產者線程和一個消費者線程)

/**
 * Main class of the example
 */
public class Main {

    /**
     * Main method of the example
     */
    public static void main(String[] args) {

        // Creates an event storage
        EventStorage storage=new EventStorage();

        // Creates a Producer and a Thread to run it
        Producer producer=new Producer(storage);
        Thread thread1=new Thread(producer);
        Thread thread3=new Thread(producer);

        // Creates a Consumer and a Thread to run it
        Consumer consumer=new Consumer(storage);
        Thread thread2=new Thread(consumer);

        // Starts the thread
        thread2.start();
        thread1.start();
        thread3.start();
    }

}

程序運行截圖如下:說明存儲數據區已經錯誤的存儲了超過規定的最大存儲量的數據。併發錯誤。

這裏寫圖片描述

寫在最後

像我一樣的老程序員們,醒醒吧,學習學習java.util.concurrent包吧;學習學習java7和java8的新特性吧。再不學習,我們就要被淘汰了!

參考資料:
《Java7併發編程實戰手冊》
《Effective Java》第二版中文版

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