1. 程式人生 > >Java並發編程(八)線程間協作(上)

Java並發編程(八)線程間協作(上)

lse 依賴關系 文章 sign finally style new 關系 service

多線程並發執行時,不同的線程執行的內容之間可能存在一些依賴關系,比如線程一執行a()方法和c()方法,線程二執行b()方法,方法a()必須在方法b()之前執行,而方法c()必須在方法b()之後執行。這時兩個線程之間就需要協作才能完成這個任務,使兩個線程協作有一個簡單粗暴的方法,即監控布爾變量,代碼如下:

boolean finishA = false;
boolean finishB = false;

線程一執行下面的代碼:

a();
finishA = true;
while(!finishB){}
c();

線程二執行下面的代碼:

while(!finishA){}
b();
finishB 
= true;

在執行b()方法和c()方法時都會檢查依賴的方法是否執行結束,只有依賴的方法執行結束才跳出循環。這種方法的優點是簡單粗暴,缺點是在等待依賴的方法時線程處於忙等待的狀態,即線程處於運行狀態(占用CPU時間)但是沒有做任何有實際意義的東西,更好的辦法是在線程等待時將其阻塞,阻塞狀態時不占用CPU時間,從而提高CPU的利用率。

使用內置鎖協作

Java提供了線程間合作的機制,即Object.wait()方法、Object.notify()和Object.notifyAll()方法。

wait()方法:使當前線程阻塞,等待其它線程調用notify()方法,釋放當前獲取的鎖。

notify()方法:喚醒一個等待著的線程,這個線程喚醒之後嘗試獲取鎖,其它線程繼續等待。

notifyAll()方法:喚醒所有等待著的線程嘗試獲取鎖,這些線程排隊等待鎖。

使用這些方法舉個小例子,學生去食堂吃飯的時候先取一碗,然後把碗交給盛飯阿姨,阿姨盛完飯把碗還給同學,這時候同學就可以吃飯了,用代碼模擬這個例子如下:

class Student implements Runnable {
    public void run() {
        synchronized(CafeteriaTest.wan) {
            System.out.println("學生:取到了一個碗");
            System.out.println("學生:阿姨幫忙盛飯");
            CafeteriaTest.wan.notify();
            
try { CafeteriaTest.wan.wait(); } catch (InterruptedException e) { } System.out.println("學生:吃飯"); } } } class CafeteriaWorker implements Runnable { public void run() { synchronized(CafeteriaTest.wan) { try { CafeteriaTest.wan.wait(); } catch (InterruptedException e) { } System.out.println("阿姨:給學生盛飯"); CafeteriaTest.wan.notify(); } } } public class CafeteriaTest { public static Object wan = new Object(); public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new CafeteriaWorker()); Thread.sleep(100);//等阿姨準備好 exec.execute(new Student()); exec.shutdown(); } }

輸出結果如下:

學生:取到了一個碗

學生:阿姨幫忙盛飯

阿姨:給學生盛飯

學生:吃飯

例子中先創建了一個“阿姨線程”,這個線程先獲取“碗”的鎖,然後調用了wait()方法進入阻塞狀態並釋放了鎖。接著我們創建了“學生線程”,學生先打印取到了碗,然後調用notify()方法通知“阿姨線程”盛飯,並且調用wait()方法使當前線程釋放鎖並阻塞;隨後“阿姨線程”從阻塞狀態恢復為學生打飯,然後“阿姨線程”調用notify()方法通知學生打完飯了,“阿姨線程”運行結束並釋放了鎖,“學生線程”拿到了“碗的鎖”開始吃飯。

在這個過程中有三點需要註意:

1.在調用wait()和notify()方法之前必須使用synchronized關鍵字獲取這個對象的鎖,否則系統會拋異常。因此不能在使用顯示鎖的臨界區內調用這些方法。

2.調用wait()方法之後有兩個因素阻止線程執行:1.線程由於等待notify()方法而處於阻塞狀態。2.獲得notify()方法的通知後,嘗試獲取鎖,此時鎖有可能是不可用的,因此會等待其它線程釋放鎖,而使線程阻塞。

3.一定要讓“阿姨線程”先拿到鎖,如果“學生線程”先拿到鎖,“阿姨線程”會由於拿不到鎖而被阻塞,直到“學生線程”執行到wait()方法;但在這之前已經調用了notify()方法了,而“阿姨線程”沒有執行到wait()方法,錯過了“學生線程”發來的信號。

使用顯示鎖協作

調用一個對象的wait()、notify()方法之前必須獲得這個對象的鎖,但是使用顯示的鎖時不能獲取某個特定對象的鎖,因此也就不能在顯示鎖的臨界區內使用這些方法。顯示鎖為我們提供了另一種類似wait()和notify()方法的線程協作機制,使用起來與wait()和notify()方法完全相同,我們用這種方式來改寫學生打飯的例子:

class Student implements Runnable {
    public void run() {
        CafeteriaLockTest.lock.lock();
        try{
            System.out.println("學生:取到了一個碗");
            System.out.println("學生:阿姨幫忙盛飯");
            CafeteriaLockTest.wan.signal();
            CafeteriaLockTest.wan.await();
            System.out.println("學生:吃飯");
        } catch (InterruptedException e) { }
        finally {
            CafeteriaLockTest.lock.unlock();
        }
    }
}
class CafeteriaWorker implements Runnable {
    public void run() {
        CafeteriaLockTest.lock.lock();
        try {
            CafeteriaLockTest.wan.await();
            System.out.println("阿姨:給學生盛飯");
            CafeteriaLockTest.wan.signal();
        }
        catch (InterruptedException e) { }
        finally {
            CafeteriaLockTest.lock.unlock();
        }
    }
}
public class CafeteriaLockTest {
    public static Lock lock = new ReentrantLock();
    public static Condition wan;
    public static void main(String[] args) throws InterruptedException {
        wan = lock.newCondition();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new CafeteriaWorker());
        Thread.sleep(100);//等阿姨準備好
        exec.execute(new Student());
        exec.shutdown();
    }
}

輸出結果如下:

學生:取到了一個碗

學生:阿姨幫忙盛飯

阿姨:給學生盛飯

學生:吃飯

在代碼中我們定義了一個重入鎖的對象作為兩個線程共用的鎖,又調用lock.newCondition()方法獲取一個Condition對象用來實現多線程協作,Condition的await()方法相當於Object的wait()方法,signal()方法相當於Object的notify()方法,Condition還有一個signalAll()方法相當於Object的notifyAll()方法。有個需要註意的地方就是,在調用Condition的await()方法時不要誤用wait()方法。

總結

本節講了如何讓多個線程協作完成某項任務,其中wait()方法和之前講過的Thread.sleep()方法類似,兩者都使線程處於阻塞狀態,但wait()方法要求調用之前必須獲取對象的內置鎖,sleep()方法調用時沒有前置條件;另一個區別是wait()方法調用後會釋放對象的鎖,而sleep()方法不釋放鎖。線程間的協作未完待續。

公眾號:今日說碼。關註我的公眾號,可查看連載文章。遇到不理解的問題,直接在公眾號留言即可。

Java並發編程(八)線程間協作(上)