Java並發編程(八)線程間協作(上)
多線程並發執行時,不同的線程執行的內容之間可能存在一些依賴關系,比如線程一執行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並發編程(八)線程間協作(上)