java多線程同步(wait、notify)生產者消費者簡單示例

一、爲何寫

最爲一個Android開發者,如果做得不夠深入可能爲不會去處理多線程同步的問題,稍微簡單點可能使用一個線程池就可以搞定了,有關線程池的介紹可以參考我的另一篇文章:ExecutorService+LruCache+DiskLruCache用一個類打造簡單的圖片加載庫
只是前段時間研究Android音視頻硬解碼,看到開源項目中用到了線程同步,就是在視頻的YUV數據的暫存,和解碼爲視頻並展示,用到了兩個線程去做,一個線程收集視頻源數據,一個線程負責解碼並播放視頻,一個視頻數據池是兩個線程共享的,數據池滿了或者空了的時候兩個線程是要做出相應處理的,這就涉及到線程同步了。
這裏寫圖片描述
學習、工作和生活的心態就要像向日葵,就算是太陽不在也要迎着月亮!

二、名字講解

什麼是線程同步?

當使用多個線程來訪問同一個數據時,非常容易出現線程安全問題(比如多個線程都在操作同一數據導致數據不一致),所以我們用同步機制來解決這些問題。

實現同步機制有兩個方法:

1、同步代碼塊:

synchronized(同一個數據){} 同一個數據:就是N條線程同時訪問一個數據。

2、同步方法:

public synchronized 數據返回類型方法名(){}

通過使用同步方法,可非常方便的將某類變成線程安全的類,具有如下特徵:
1,該類的對象可以被多個線程安全的訪問。
2,每個線程調用該對象的任意方法之後,都將得到正確的結果。
3,每個線程調用該對象的任意方法之後,該對象狀態依然保持合理狀態。
注:synchronized關鍵字可以修飾方法,也可以修飾代碼塊,但不能修飾構造器,屬性等

※不要對線程安全類的所有方法都進行同步,只對那些會改變共享資源方法的進行同步。
線程通訊:
當使用synchronized 來修飾某個共享資源時(分同步代碼塊和同步方法兩種情況),當某個線程獲得共享資源的鎖後就可以執行相應的代碼段,直到該線程運行完該代碼段後才釋放對該共享資源的鎖,讓其他線程有機會執行對該共享資源的修改。當某個線程佔有某個共享資源的鎖時,如果另外一個線程也想獲得這把鎖運行就需要使用wait() 和notify()/notifyAll()方法來進行線程通訊了。
Java.lang.object 裏的三個方法wait() notify() notifyAll()

wait()
導致當前線程等待,直到其他線程調用同步監視器的notify方法或notifyAll方法來喚醒該線程。

wait(mills)
都是等待指定時間後自動甦醒,調用wait方法的當前線程會釋放該同步監視器的鎖定,可以不用notify或notifyAll方法把它喚醒。

notify()
喚醒在同步監視器上等待的單個線程,如果所有線程都在同步監視器上等待,則會選擇喚醒其中一個線程,選擇是任意性的,只有當前線程放棄對該同步監視器的鎖定後,也就是使用wait方法後,纔可以執行被喚醒的線程。

notifyAll()
喚醒在同步監視器上等待的所有的線程。只用當前線程放棄對該同步監視器的鎖定後,也就是使用wait方法後,纔可以執行被喚醒的線程。

注意,notify方法一定要在synchronized同步裏面調用,還有做異常捕捉。


原子操作:根據Java規範,對於基本類型的賦值或者返回值操作,是原子操作。但這裏的基本數據類型不包括long和double, 因爲JVM看到的基本存儲單位是32位,而long 和double都要用64位來表示。所以無法在一個時鐘週期內完成。

自增操作(++)不是原子操作,因爲它涉及到一次讀和一次寫。

原子操作:由一組相關的操作完成,這些操作可能會操縱與其它的線程共享的資源,爲了保證得到正確的運算結果,一個線程在執行原子操作其間,應該採取其他的措施使得其他的線程不能操縱共享資源。

同步代碼塊:爲了保證每個線程能夠正常執行原子操作,Java引入了同步機制,具體的做法是在代表原子操作的程序代碼前加上synchronized標記,這樣的代碼被稱爲同步代碼塊。

同步鎖:每個JAVA對象都有且只有一個同步鎖,在任何時刻,最多隻允許一個線程擁有這把鎖。

當一個線程試圖訪問帶有synchronized(this)標記的代碼塊時,必須獲得 this關鍵字引用的對象的鎖,在以下的兩種情況下,本線程有着不同的命運。
1、 假如這個鎖已經被其它的線程佔用,JVM就會把這個線程放到本對象的鎖池中。本線程進入阻塞狀態。鎖池中可能有很多的線程,等到其他的線程釋放了鎖,JVM就會從鎖池中隨機取出一個線程,使這個線程擁有鎖,並且轉到就緒狀態。
2、 假如這個鎖沒有被其他線程佔用,本線程會獲得這把鎖,開始執行同步代碼塊。 (一般情況下在執行同步代碼塊時不會釋放同步鎖,但也有特殊情況會釋放對象鎖 如在執行同步代碼塊時,遇到異常而導致線程終止,鎖會被釋放;在執行代碼塊時,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池中)

線程同步的特徵:
1、 如果一個同步代碼塊和非同步代碼塊同時操作共享資源,仍然會造成對共享資源的競爭。因爲當一個線程執行一個對象的同步代碼塊時,其他的線程仍然可以執行對象的非同步代碼塊。(所謂的線程之間保持同步,是指不同的線程在執行同一個對象的同步代碼塊時,因爲要獲得對象的同步鎖而互相牽制)
2、 每個對象都有唯一的同步鎖
3、 在靜態方法前面可以使用synchronized修飾符。
4、 當一個線程開始執行同步代碼塊時,並不意味着必須以不間斷的方式運行,進入同步代碼塊的線程可以執行Thread.sleep()或執行Thread.yield()方法,此時它並不釋放對象鎖,只是把運行的機會讓給其他的線程。
5、 Synchronized聲明不會被繼承,如果一個用synchronized修飾的方法被子類覆蓋,那麼子類中這個方法不在保持同步,除非用synchronized修飾。

釋放對象的鎖:
1、 執行完同步代碼塊就會釋放對象的鎖
2、 在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖也會被釋放
3、 在執行同步代碼塊的過程中,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池。

死鎖:
線程1獨佔(鎖定)資源A,等待獲得資源B後,才能繼續執行
同時
線程2獨佔(鎖定)資源B,等待獲得資源A後,才能繼續執行
這樣就會發生死鎖,程序無法正常執行

如何避免死鎖
一個通用的經驗法則是:當幾個線程都要訪問共享資源A、B、C 時,保證每個線程都按照同樣的順序去訪問他們。


注意:
1、線程同步就是線程排隊。同步就是排隊。線程同步的目的就是避免線程“同步”執行。
2、只有共享資源的讀寫訪問才需要同步。如果不是共享資源,那麼就根本沒有同步的必要。
3、只有“變量”才需要同步訪問。如果共享的資源是固定不變的,那麼就相當於“常量”,線程同時讀取常量也不需要同步。至少一個線程修改共享資源,這樣的情況下,線程之間就需要同步。
4、多個線程訪問共享資源的代碼有可能是同一份代碼,也有可能是不同的代碼;無論是否執行同一份代碼,只要這些線程的代碼訪問同一份可變的共享資源,這些線程之間就需要同步。

5、我們要儘量避免這種直接把synchronized加在函數定義上的偷懶做法。因爲我們要控制同步粒度。同步的代碼段越小越好。synchronized控制的範圍越小越好。

同步鎖:
我們可以給共享資源加一把鎖,這把鎖只有一把鑰匙。哪個線程獲取了這把鑰匙,纔有權利訪問該共享資源。
同步鎖不是加在共享資源上,而是加在訪問共享資源的代碼段上。
訪問同一份共享資源的不同代碼段,應該加上同一個同步鎖;如果加的是不同的同步鎖,那麼根本就起不到同步的作用,沒有任何意義。
這就是說,同步鎖本身也一定是多個線程之間的共享對象。

三、生產者消費者代碼示例

產品倉庫

package com.danxx.javalib2;

import java.util.LinkedList;
import java.util.Queue;

/**
 * 數據存儲倉庫和操作
 * 一個緩衝區,緩衝區有最大限制,當緩衝區滿
 * 的時候,生產者是不能將產品放入到緩衝區裏面的,
 * 當然,當緩衝區是空的時候,消費者也不能從中拿出來產品,
 * 這就涉及到了在多線程中的條件判斷
 * Created by dawish on 2017/7/13.
 */
public class Storage {

    private static volatile int goodNumber = 1;

    private final static int MAX_SIZE = 20;
    /**
     *  Queue操作解析:
     *  add       增加一個元索                 如果隊列已滿, 則拋出一個IIIegaISlabEepeplian異常
     *  remove    移除並返回隊列頭部的元素     如果隊列爲空, 則拋出一個NoSuchElementException異常
     *  element   返回隊列頭部的元素           如果隊列爲空, 則拋出一個NoSuchElementException異常
     *  offer     添加一個元素並返回true       如果隊列已滿, 則返回false
     *  poll      移除並返問隊列頭部的元素     如果隊列爲空, 則返回null
     *  peek      返回隊列頭部的元素           如果隊列爲空, 則返回null
     *  put       添加一個元素                 如果隊列滿,   則阻塞
     *  take      移除並返回隊列頭部的元素     如果隊列爲空, 則阻塞
     *
     */
    Queue<String> storage;
    public Storage() {
        storage = new LinkedList<String>();
    }

    /**
     *
     * @param dataValue
     */
    public synchronized void put(String dataValue, String threadName){
        if(storage.size() >= MAX_SIZE){
            try {
                goodNumber = 1;
                super.wait();  //當生產滿了後讓生產線程等待
                return;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(dataValue + goodNumber++);
        System.out.println(threadName + dataValue + goodNumber);
        super.notify();  //每次添加一個數據就喚醒一個消費等待的線程來消費
    }

    /**
     *
     * @return
     * @throws InterruptedException
     */
    public synchronized String get(String threadName) {
        if(storage.size() == 0){
            try {
                super.wait();  //當產品倉庫爲空的時候讓消費線程等待
                System.out.println(threadName + "wait");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }
        super.notify();  //當數據不爲空的時候就喚醒一個生產線程來生產
        String value = storage.remove();
        return value;
    }

}

生產者

package com.danxx.javalib2;

import java.util.UUID;

/**
 * 生產者
 * Created by dawish on 2017/7/13.
 */
public class Producer extends Thread{

    private Storage storage;//生產者倉庫
    private String name="";

    public Producer(Storage storage, String name) {
        this.storage = storage;
        this.name = name;
    }
    public void run(){
        //生產者每隔1s生產1~100消息
        long oldTime = System.currentTimeMillis();
        while(true){
            synchronized(storage){
                if (System.currentTimeMillis() - oldTime >= 1000) {
                    oldTime = System.currentTimeMillis();
                    String msg = UUID.randomUUID().toString();
                    storage.put("-ID:" ,name);
                }
            }
        }
    }
}

消費者

package com.danxx.javalib2;

/**
 * 消費者
 * Created by dawish on 2017/7/13.
 */

public class Consumer extends Thread{

    private Storage storage;//倉庫

    private String name="";

    public Consumer(Storage storage, String name) {
        this.storage = storage;
        this.name = name;
    }
    public void run(){
        while(true){
            synchronized(storage){
                //消費者去倉庫拿消息的時候,如果發現倉庫數據爲空,則等待
                String data = storage.get(name);
                if(data != null){

                    System.out.println(name +"-------------"+ data);

                }
            }
        }
    }
}

main方法

package com.danxx.javalib2;

/**
 *  Java中的多線程會涉及到線程間通信,常見的線程通信方式,例如共享變量、管道流等,
 *  這裏我們要實現生產者消費者模式,也需要涉及到線程通信,不過這裏我們用到了java中的
 *  wait()、notify()方法:
 *  wait():進入臨界區的線程在運行到一部分後,發現進行後面的任務所需的資源還沒有準備充分,
 *  所以調用wait()方法,讓線程阻塞,等待資源,同時釋放臨界區的鎖,此時線程的狀態也從RUNNABLE狀態變爲WAITING狀態;
 *  notify():準備資源的線程在準備好資源後,調用notify()方法通知需要使用資源的線程,
 *  同時釋放臨界區的鎖,將臨界區的鎖交給使用資源的線程。
 *  wait()、notify()這兩個方法,都必須要在臨界區中調用,即是在synchronized同步塊中調用,
 *  不然會拋出IllegalMonitorStateException的異常。
 *  Created by dawish on 2017/7/14.
 */

public class MainApp {
    public static void main(String[] args) {
        Storage storage = new Storage();

        Producer producer1 = new Producer(storage, "Producer-1");
        Producer producer2 = new Producer(storage, "Producer-2");
        Producer producer3 = new Producer(storage, "Producer-3");
        Producer producer4 = new Producer(storage, "Producer-4");

        Consumer consumer1 = new Consumer(storage, "Consumer-1");
        Consumer consumer2 = new Consumer(storage, "Consumer-2");

        producer1.start();
        producer2.start();
        producer3.start();
        producer4.start();

        consumer1.start();
        consumer2.start();
    }
}

運行結果(4個生產者2個消費者)

這裏寫圖片描述

四、Github地址

https://github.com/Dawish/CustomViews/tree/master/JavaLib

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