Java 多線程詳解(三)

一、線程的同步與死鎖

1、線程同步問題的引出

所謂的同步問題指的是多個線程操作同一資源時所帶來的安全性問題。例如,下面模擬一個簡單的賣票程序,要求有5個線程,賣6張票。

package com.wz.threaddemo;

class MyThread implements Runnable {
    private int ticket = 6;

    @Override
    public void run() {
        for (int x = 0; x < 10; x++) {
            if (this.ticket > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "賣票,ticket = " + this.ticket--);
            }
        }
    }
}

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        new Thread(mt, "賣票線程A").start();
        new Thread(mt, "賣票線程B").start();
        new Thread(mt, "賣票線程C").start();
        new Thread(mt, "賣票線程D").start();
        new Thread(mt, "賣票線程E").start();

    }
}

運行結果:

賣票線程B賣票,ticket = 6
賣票線程E賣票,ticket = 3
賣票線程A賣票,ticket = 6
賣票線程D賣票,ticket = 4
賣票線程C賣票,ticket = 5
賣票線程B賣票,ticket = 2
賣票線程E賣票,ticket = 1
賣票線程D賣票,ticket = 0
賣票線程A賣票,ticket = 1
賣票線程C賣票,ticket = -1
賣票線程B賣票,ticket = -2

這時我們發現,操作的結果出現了負數,這個就可以理解爲不同步問題。那麼,到底是如何造成這種不同步的呢?

整個賣票的過程分爲兩個步驟:
第一步,判斷是否還有剩餘的票數;
第二步,票數減一。
但是在上面的操作代碼中,兩個步驟之間加了延遲操作,那麼一個線程可能在還沒有票數減一之前,其他線程就已經把票數減一了,於是,這就產生了負數。

2、線程同步問題的解決

如果想要解決這樣的問題。就必須使用同步。所謂同步,就是指多個操作在同一個時間段內只能有一個線程進行,其他線程要等這個線程執行完成纔可以繼續執行。

想解決資源共享的同步操作問題,可以使用同步代碼塊和同步方法兩種方式完成。

方式一:同步代碼塊,使用synchronized關鍵字定義的代碼塊就稱爲同步代碼塊。同步代碼塊格式:

synchronized(同步對象){
     需要同步的代碼
}

在進行同步的操作時必須設置一個要同步的對象,而這個對象應該理解爲當前對象:this。

package com.wz.threaddemo;

class MyThread implements Runnable {
    private int ticket = 6;

    @Override
    public void run() { 
        for (int x = 0; x < 10; x++) {
            synchronized (this) {//同步代碼塊,當前操作每次只允許一個對象進入
                if (this.ticket > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "賣票,ticket = " + this.ticket--);
                }
            }

        }
    }
}

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        new Thread(mt, "賣票線程A").start();
        new Thread(mt, "賣票線程B").start();
        new Thread(mt, "賣票線程C").start();
        new Thread(mt, "賣票線程D").start();
        new Thread(mt, "賣票線程E").start();

    }
}

運行結果:

賣票線程A賣票,ticket = 6
賣票線程C賣票,ticket = 5
賣票線程E賣票,ticket = 4
賣票線程D賣票,ticket = 3
賣票線程B賣票,ticket = 2
賣票線程D賣票,ticket = 1

方式二:同步方法。除了可以將需要的代碼設置成同步代碼塊之外,也可以使用synchronized關鍵字將一個方法聲明成同步方法。定義格式:

synchronized 方法返回值 方法名稱(參數列表){   }
package com.wz.threaddemo;

class MyThread implements Runnable { 
    private int ticket = 6;

    @Override
    public void run() { 
        for (int x = 0; x < 10; x++) {
            this.sale();
        }
    }

    public synchronized void sale() {//同步方法
        if (this.ticket > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "賣票,ticket = " + this.ticket--);
        }
    }
}

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        new Thread(mt, "賣票線程A").start();
        new Thread(mt, "賣票線程B").start();
        new Thread(mt, "賣票線程C").start();
        new Thread(mt, "賣票線程D").start();
        new Thread(mt, "賣票線程E").start();

    }
}

運行結果:

賣票線程A賣票,ticket = 6
賣票線程A賣票,ticket = 5
賣票線程E賣票,ticket = 4
賣票線程D賣票,ticket = 3
賣票線程C賣票,ticket = 2
賣票線程B賣票,ticket = 1

同步操作與異步操作相比,異步操作的速度要明顯高於同步操作,但同步操作是數據的安全性較高,屬於安全的線程操作。

3、死鎖

通過分析可以發現,所謂的同步就是指一個線程對象等待另外一個線程對象執行完成後再執行。但是過多的同步會導致線程死鎖

所謂死鎖, 是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。

那麼爲什麼會產生死鎖呢?
(1)因爲系統資源不足。
(2)進程運行推進的順序不合適。
(3)資源分配不當。

學過操作系統的都知道,產生死鎖的條件有四個:
(1)互斥條件:所謂互斥就是進程在某一時間內獨佔資源。
(2)請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
(3)不剝奪條件:進程已獲得資源,在末使用完之前,不能強行剝奪。
(4)循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

如何解決死鎖?
可以從死鎖的四個條件出發去解決,只要破壞一個必要條件,那麼我們的死鎖就解決了。
在Java中使用多線程的時候一定要考慮是否有死鎖的問題。

二、線程間通信—生產者/消費者模型

對於多線程程序來說,不管任何編程語言,生產者和消費者模型都是最經典的。就像學習每一門編程語言一樣,Hello World!都是最經典的例子。

1、生產者/消費者問題的引出

生產者-消費者(producer-consumer)問題,也稱作有界緩衝區(bounded-buffer)問題,兩個進程共享一個公共的固定大小的緩衝區。其中一個是生產者,用於將消息放入緩衝區;另外一個是消費者,用於從緩衝區中取出消息。問題出現在當緩衝區已經滿了,而此時生產者還想向其中放入一個新的數據項的情形,其解決方法是讓生產者此時進行休眠,等待消費者從緩衝區中取走了一個或者多個數據後再去喚醒它。同樣地,當緩衝區已經空了,而消費者還想去取消息,此時也可以讓消費者進行休眠,等待生產者放入一個或者多個數據時再喚醒它。

2、生產者/消費者模型的實現

(1)首先定義公共資源類,其中的變量number是保存的公共數據。並且定義兩個方法,增加number的值和減少number的值(由於多線程的原因,必須加上synchronized關鍵字,注意while判斷的條件)。

package com.wz.threaddemo;
/**
 * 公共資源類
 */
public class PublicResource {
    private int number = 0;
    private int maxSize = 10;

    /**
     * 增加公共資源
     */
    public synchronized void increace() {
        while (number == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number++;
        System.out.println("增加一個資源,現在資源數爲:"+number);
        notify();
    }

    /**
     * 減少公共資源
     */
    public synchronized void decreace() {
        while (number == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println("減少一個資源,現在資源數爲:"+number);
        notify();
    }
}

(2)分別定義生產者線程和消費者線程,並模擬多次生產和消費,即增加和減少公共資源的number值。

package com.wz.threaddemo;

/**
 * 生產者線程,負責生產公共資源
 */
public class ProducerThread implements Runnable {
    private PublicResource resource;

    public ProducerThread(PublicResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep((long) (Math.random() * 100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.increace();
        }
    }
}
package com.wz.threaddemo;

/** 
 * 消費者線程,負責消費公共資源 
 */  
public class ConsumerThread implements Runnable {  
    private PublicResource resource;  

    public ConsumerThread(PublicResource resource) {  
        this.resource = resource;  
    }  

    @Override  
    public void run() {  
        for (int i = 0; i < 10; i++) {  
            try {  
                Thread.sleep((long) (Math.random() * 100));  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            resource.decreace();  
        }  
    }  
}  

(3)模擬多個生產者和消費者操作公共資源的情形,結果須保證是在允許的範圍內。

package com.wz.threaddemo;

public class TestDemo {
    public static void main(String[] args) throws Exception {
        PublicResource resource = new PublicResource();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
    }

}

運行結果:

增加一個資源,現在資源數爲:1
減少一個資源,現在資源數爲:0
增加一個資源,現在資源數爲:1
減少一個資源,現在資源數爲:0
增加一個資源,現在資源數爲:1
增加一個資源,現在資源數爲:2
增加一個資源,現在資源數爲:3
減少一個資源,現在資源數爲:2
減少一個資源,現在資源數爲:1
增加一個資源,現在資源數爲:2
減少一個資源,現在資源數爲:1
增加一個資源,現在資源數爲:2
... ...//後面還有

下面是生產者/消費者模型的一些優點:
(1)它簡化了開發,你可以獨立地或併發的編寫消費者和生產者,它僅僅只需知道共享對象是誰;
(2)生產者不需要知道誰是消費者或者有多少消費者,對消費者來說也是一樣;
(3)生產者和消費者可以以不同的速度執行;
(4)分離的消費者和生產者在功能上能寫出更簡潔、可讀、易維護的代碼。

一個小問題:解釋sleep()和wait()的區別?
(1)sleep()是Thread類定義的方法,而wait()方法時Object定義的方法;
(2)sleep()可以設置休眠時間,時間一到自動喚醒,而wait()方法需要使用notify()方法進行喚醒。

3、使用阻塞隊列實現生產者/消費者模式

在上面的實現中,使用變量number來保存公共數據,並且使用wait()和notify()方法在生產者和消費者線程中調配資源,這樣,是不是略顯複雜了呢?有沒有更簡單的方式來實現呢?

有,使用阻塞隊列實現生產者/消費者模式,它提供開箱即用支持阻塞的方法put()和take(),開發者不需要寫困惑的wait-nofity代碼去實現通信了。實現代碼如下:

生產者線程:

package com.wz.threaddemo;

import java.util.concurrent.BlockingQueue;

/**
 * 生產者線程,負責生產資源
 */
public class ProducerThread implements Runnable {
    private BlockingQueue<Integer> sharedQueue;

    public ProducerThread(BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        for(int i=0; i<10; i++){
            try {

                System.out.println(Thread.currentThread().getName()+" Produced:" + i);
                sharedQueue.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

消費者線程:

package com.wz.threaddemo;

import java.util.concurrent.BlockingQueue;

/**
 * 消費者線程,負責消費公共資源
 */
public class ConsumerThread implements Runnable {
    private BlockingQueue sharedQueue;

    public ConsumerThread(BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println(Thread.currentThread().getName()+" Consumed:" + sharedQueue.take());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

模擬生產者和消費者操作公共資源:

package com.wz.threaddemo;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class TestDemo {
    public static void main(String[] args) throws Exception {

        BlockingQueue sharedQueue = new LinkedBlockingQueue();

        Thread prodThread = new Thread(new ProducerThread(sharedQueue), "生產線程");
        Thread consThread = new Thread(new ConsumerThread(sharedQueue), "消費線程");

        prodThread.start();
        consThread.start();

    }

}

運行結果:

生產線程 Produced:0
生產線程 Produced:1
生產線程 Produced:2
消費線程 Consumed:0
生產線程 Produced:3
消費線程 Consumed:1
生產線程 Produced:4
消費線程 Consumed:2
生產線程 Produced:5
消費線程 Consumed:3
生產線程 Produced:6
消費線程 Consumed:4
生產線程 Produced:7
消費線程 Consumed:5
生產線程 Produced:8
消費線程 Consumed:6
生產線程 Produced:9
消費線程 Consumed:7
消費線程 Consumed:8
消費線程 Consumed:9

可見,阻塞隊列實現生產者消費者模式,對比傳統的wait、nofity代碼,它更易於理解。

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