多線程作為java中很重要的一個知識點,在此還是有必要總結一下的。
一.線程的生命周期及五種基本狀態
關于Java中線程的生命周期,首先看一下下面這張較為經典的圖:
上圖中基本上囊括了Java中多線程各重要知識點。掌握了上圖中的各知識點,Java中的多線程也就基本上掌握了。主要包括:
Java線程具有五中基本狀態
新建狀態(New):當線程對象對創建后,即進入了新建狀態,如:Thread t = new MyThread();
就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處于就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,并不是說執行了t.start()此線程立即就會執行;
運行狀態(Running):當CPU開始調度處于就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就 緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處于就緒狀態中;
阻塞狀態(Blocked):處于運行狀態中的線程由于某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:
1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;
2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態;
3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
二. Java多線程的創建及啟動
Java中線程的創建常見有如三種基本形式
1.繼承Thread類,重寫該類的run()方法。
1 class MyThread extends Thread { 2 3 PRivate int i = 0; 4 5 @Override 6 public void run() { 7 for (i = 0; i < 100; i++) { 8 System.out.println(Thread.currentThread().getName() + " " + i); 9 }10 }11 }1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 for (int i = 0; i < 100; i++) { 5 System.out.println(Thread.currentThread().getName() + " " + i); 6 if (i == 30) { 7 Thread myThread1 = new MyThread(); // 創建一個新的線程 myThread1 此線程進入新建狀態 8 Thread myThread2 = new MyThread(); // 創建一個新的線程 myThread2 此線程進入新建狀態 9 myThread1.start(); // 調用start()方法使得線程進入就緒狀態10 myThread2.start(); // 調用start()方法使得線程進入就緒狀態11 }12 }13 }14 }如上所示,繼承Thread類,通過重寫run()方法定義了一個新的線程類MyThread,其中run()方法的方法體代表了線程需要完成的任務,稱之為線程執行體。當創建此線程類對象時一個新的線程得以創建,并進入到線程新建狀態。通過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程并不一定會馬上得以執行,這取決于CPU調度時機。
2.實現Runnable接口,并重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的實例,并以此實例作為Thread類的target來創建Thread對象,該Thread對象才是真正的線程對象。
1 class MyRunnable implements Runnable { 2 private int i = 0; 3 4 @Override 5 public void run() { 6 for (i = 0; i < 100; i++) { 7 System.out.println(Thread.currentThread().getName() + " " + i); 8 } 9 }10 }1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 for (int i = 0; i < 100; i++) { 5 System.out.println(Thread.currentThread().getName() + " " + i); 6 if (i == 30) { 7 Runnable myRunnable = new MyRunnable(); // 創建一個Runnable實現類的對象 8 Thread thread1 = new Thread(myRunnable); // 將myRunnable作為Thread target創建新的線程 9 Thread thread2 = new Thread(myRunnable);10 thread1.start(); // 調用start()方法使得線程進入就緒狀態11 thread2.start();12 }13 }14 }15 }相信以上兩種創建新線程的方式大家都很熟悉了,那么Thread和Runnable之間到底是什么關系呢?我們首先來看一下下面這個例子。
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 for (int i = 0; i < 100; i++) { 5 System.out.println(Thread.currentThread().getName() + " " + i); 6 if (i == 30) { 7 Runnable myRunnable = new MyRunnable(); 8 Thread thread = new MyThread(myRunnable); 9 thread.start();10 }11 }12 }13 }14 15 class MyRunnable implements Runnable {16 private int i = 0;17 18 @Override19 public void run() {20 System.out.println("in MyRunnable run");21 for (i = 0; i < 100; i++) {22 System.out.println(Thread.currentThread().getName() + " " + i);23 }24 }25 }26 27 class MyThread extends Thread {28 29 private int i = 0;30 31 public MyThread(Runnable runnable){32 super(runnable);33 }34 35 @Override36 public void run() {37 System.out.println("in MyThread run");38 for (i = 0; i < 100; i++) {39 System.out.println(Thread.currentThread().getName() + " " + i);40 }41 }42 }同樣的,與實現Runnable接口創建線程方式相似,不同的地方在于
1 Thread thread = new MyThread(myRunnable);那么這種方式可以順利創建出一個新的線程么?答案是肯定的。至于此時的線程執行體到底是MyRunnable接口中的run()方法還是MyThread類中的run()方法呢?通過輸出我們知道線程執行體是MyThread類中的run()方法。其實原因很簡單,因為Thread類本身也是實現了Runnable接口,而run()方法最先是在Runnable接口中定義的方法。
1 public interface Runnable {2 3 public abstract void run();4 5 }我們看一下Thread類中對Runnable接口中run()方法的實現:
@Override public void run() { if (target != null) { target.run(); } }也就是說,當執行到Thread類中的run()方法時,會首先判斷target是否存在,存在則執行target中的run()方法,也就是實現了Runnable接口并重寫了run()方法的類中的run()方法。但是上述給到的列子中,由于多態的存在,根本就沒有執行到Thread類中的run()方法,而是直接先執行了運行時類型即MyThread類中的run()方法。
3.使用Callable和Future接口創建線程。具體是創建Callable接口的實現類,并實現clall()方法。并使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作為Thread對象的target來創建線程。
看著好像有點復雜,直接來看一個例子就清晰了。
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 5 Callable<Integer> myCallable = new MyCallable(); // 創建MyCallable對象 6 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象 7 8 for (int i = 0; i < 100; i++) { 9 System.out.println(Thread.currentThread().getName() + " " + i);10 if (i == 30) {11 Thread thread = new Thread(ft); //FutureTask對象作為Thread對象的target創建新的線程12 thread.start(); //線程進入到就緒狀態13 }14 }15 16 System.out.println("主線程for循環執行完畢..");17 18 try {19 int sum = ft.get(); //取得新創建的新線程中的call()方法返回的結果20 System.out.println("sum = " + sum);21 } catch (InterruptedException e) {22 e.printStackTrace();23 } catch (ExecutionException e) {24 e.printStackTrace();25 }26 27 }28 }29 30 31 class MyCallable implements Callable<Integer> {32 private int i = 0;33 34 // 與run()方法不同的是,call()方法具有返回值35 @Override36 public Integer call() {37 int sum = 0;38 for (; i < 100; i++) {39 System.out.println(Thread.currentThread().getName() + " " + i);40 sum += i;41 }42 return sum;43 }44 45 }首先,我們發現,在實現Callable接口中,此時不再是run()方法了,而是call()方法,此call()方法作為線程執行體,同時還具有返回值!在創建新的線程時,是通過FutureTask來包裝MyCallable對象,同時作為了Thread對象的target。那么看下FutureTask類的定義:
1 public class FutureTask<V> implements RunnableFuture<V> {2 3 //....4 5 }1 public interface RunnableFuture<V> extends Runnable, Future<V> {2 3 void run();4 5 }于是,我們發現FutureTask類實際上是同時實現了Runnable和Future接口,由此才使得其具有Future和Runnable雙重特性。通過Runnable特性,可以作為Thread對象的target,而Future特性,使得其可以取得新創建線程中的call()方法的返回值。
執行下此程序,我們發現sum = 4950永遠都是最后輸出的。而“主線程for循環執行完畢..”則很可能是在子線程循環中間輸出。由CPU的線程調度機制,我們知道,“主線程for循環執行完畢..”的輸出時機是沒有任何問題的,那么為什么sum =4950會永遠最后輸出呢?
原因在于通過ft.get()方法獲取子線程call()方法的返回值時,當子線程此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。
上述主要講解了三種常見的線程創建方式,對于線程的啟動而言,都是調用線程對象的start()方法,需要特別注意的是:不能對同一線程對象兩次調用start()方法。
三. Java多線程的就緒、運行和死亡狀態
就緒狀態轉換為運行狀態:當此線程得到處理器資源;
運行狀態轉換為就緒狀態:當此線程主動調用yield()方法或在運行過程中失去處理器資源。
運行狀態轉換為死亡狀態:當此線程線程執行體執行完畢或發生了異常。
此處需要特別注意的是:當調用線程的yield()方法時,線程從運行狀態轉換為就緒狀態,但接下來CPU調度就緒狀態中的哪個線程具有一定的隨機性,因此,可能會出現A線程調用了yield()方法后,接下來CPU仍然調度了A線程的情況。
由于實際的業務需要,常常會遇到需要在特定時機終止某一線程的運行,使其進入到死亡狀態。目前最通用的做法是設置一boolean型的變量,當條件滿足時,使線程執行體快速執行完畢。如:
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 5 MyRunnable myRunnable = new MyRunnable(); 6 Thread thread = new Thread(myRunnable); 7 8 for (int i = 0; i < 100; i++) { 9 System.out.println(Thread.currentThread().getName() + " " + i);10 if (i == 30) {11 thread.start();12 }13 if(i == 40){14 myRunnable.stopThread();15 }16 }17 }18 }19 20 class MyRunnable implements Runnable {21 22 private boolean stop;23 24 @Override25 public void run() {26 for (int i = 0; i < 100 && !stop; i++) {27 System.out.println(Thread.currentThread().getName() + " " + i);28 }29 }30 31 public void stopThread() {32 this.stop = true;33 }34 35 }
四.Java多線程的阻塞狀態與線程控制
上文已經提到Java阻塞的幾種具體類型。下面分別看下引起Java線程阻塞的主要方法。
1.join()
join —— 讓一個線程等待另一個線程完成才繼續執行。如A線程線程執行體中調用B線程的join()方法,則A線程被阻塞,知道B線程執行完為止,A才能得以繼續執行。
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 5 MyRunnable myRunnable = new MyRunnable(); 6 Thread thread = new Thread(myRunnable); 7 8 for (int i = 0; i < 100; i++) { 9 System.out.println(Thread.currentThread().getName() + " " + i);10 if (i == 30) {11 thread.start();12 try {13 thread.join(); // main線程需要等待thread線程執行完后才能繼續執行14 } catch (InterruptedException e) {15 e.printStackTrace();16 }17 }18 }19 }20 }21 22 class MyRunnable implements Runnable {23 24 @Override25 public void run() {26 for (int i = 0; i < 100; i++) {27 System.out.println(Thread.currentThread().getName() + " " + i);28 }29 }30 }
2.sleep()
sleep —— 讓當前的正在執行的線程暫停指定的時間,并進入阻塞狀態。在其睡眠的時間段內,該線程由于不是處于就緒狀態,因此不會得到執行的機會。即使此時系統中沒有任何其他可執行的線程,出于sleep()中的線程也不會執行。因此sleep()方法常用來暫停線程執行。
前面有講到,當調用了新建的線程的start()方法后,線程進入到就緒狀態,可能會在接下來的某個時間獲取CPU時間片得以執行,如果希望這個新線程必然性的立即執行,直接調用原來線程的sleep(1)即可。
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 5 MyRunnable myRunnable = new MyRunnable(); 6 Thread thread = new Thread(myRunnable); 7 8 for (int i = 0; i < 100; i++) { 9 System.out.println(Thread.currentThread().getName() + " " + i);10 if (i == 30) {11 thread.start();12 try {13 Thread.sleep(1); // 使得thread必然能夠馬上得以執行14 } catch (InterruptedException e) {15 e.printStackTrace();16 }17 }18 }19 }20 }21 22 class MyRunnable implements Runnable {23 24 @Override25 public void run() {26 for (int i = 0; i < 100; i++) {27 System.out.println(Thread.currentThread().getName() + " " + i);28 }29 }30 }注:睡一個毫秒級夠了,因為CPU不會空閑,會切換到新建的線程。
3.后臺線程(Daemon Thread)
概念/目的:后臺線程主要是為其他線程(相對可以稱之為前臺線程)提供服務,或“守護線程”。如JVM中的垃圾回收線程。
生命周期:后臺線程的生命周期與前臺線程生命周期有一定關聯。主要體現在:當所有的前臺線程都進入死亡狀態時,后臺線程會自動死亡(其實這個也很好理解,因為后臺線程存在的目的在于為前臺線程服務的,既然所有的前臺線程都死亡了,那它自己還留著有什么用...偉大啊 ! !)。
設置后臺線程:調用Thread對象的setDaemon(true)方法可以將指定的線程設置為后臺線程。
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 Thread myThread = new MyThread(); 5 for (int i = 0; i < 100; i++) { 6 System.out.println("main thread i = " + i); 7 if (i == 20) { 8 myThread.setDaemon(true); 9 myThread.start();10 }11 }12 }13 14 }15 16 class MyThread extends Thread {17 18 public void run() {19 for (int i = 0; i < 100; i++) {20 System.out.println("i = " + i);21 try {22 Thread.sleep(1);23 } catch (InterruptedException e) {24 // TODO Auto-generated catch block25 e.printStackTrace();26 }27 }28 }29 }判斷線程是否是后臺線程:調用thread對象的isDeamon()方法。
注:main線程默認是前臺線程,前臺線程創建中創建的子線程默認是前臺線程,后臺線程中創建的線程默認是后臺線程。調用setDeamon(true)方法將前臺線程設置為后臺線程時,需要在start()方法調用之前。前天線程都死亡后,JVM通知后臺線程死亡,但從接收指令到作出響應,需要一定的時間。
4.改變線程的優先級/setPriority():
每個線程在執行時都具有一定的優先級,優先級高的線程具有較多的執行機會。每個線程默認的優先級都與創建它的線程的優先級相同。main線程默認具有普通優先級。
設置線程優先級:setPriority(int priorityLevel)。參數priorityLevel范圍在1-10之間,常用的有如下三個靜態常量值:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
獲取線程優先級:getPriority()。
注:具有較高線程優先級的線程對象僅表示此線程具有較多的執行機會,而非優先執行。
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 Thread myThread = new MyThread(); 5 for (int i = 0; i < 100; i++) { 6 System.out.println("main thread i = " + i); 7 if (i == 20) { 8 myThread.setPriority(Thread.MAX_PRIORITY); 9 myThread.start();10 }11 }12 }13 14 }15 16 class MyThread extends Thread {17 18 public void run() {19 for (int i = 0; i < 100; i++) {20 System.out.println("i = " + i);21 }22 }23 }
5.線程讓步:yield()
上一篇博文中已經講到了yield()的基本作用,同時,yield()方法還與線程優先級有關,當某個線程調用yiled()方法從運行狀態轉換到就緒狀態后,CPU從就緒狀態線程隊列中只會選擇與該線程優先級相同或優先級更高的線程去執行。
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 Thread myThread1 = new MyThread1(); 5 Thread myThread2 = new MyThread2(); 6 myThread1.setPriority(Thread.MAX_PRIORITY); 7 myThread2.setPriority(Thread.MIN_PRIORITY); 8 for (int i = 0; i < 100; i++) { 9 System.out.println("main thread i = " + i);10 if (i == 20) {11 myThread1.start();12 myThread2.start();13 Thread.yield();14 }15 }16 }17 18 }19 20 class MyThread1 extends Thread {21 22 public void run() {23 for (int i = 0; i < 100; i++) {24 System.out.println("myThread 1 -- i = " + i);25 }26 }27 }28 29 class MyThread2 extends Thread {30 31 public void run() {32 for (int i = 0; i < 100; i++) {33 System.out.println("myThread 2 -- i = " + i);34 }35 }36 }
本文主要接著前面多線程的兩篇文章總結Java多線程中的線程安全問題。
一.一個典型的Java線程安全例子
1 public class ThreadTest { 2 3 public static void main(String[] args) { 4 Account account = new Account("123456", 1000); 5 DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); 6 Thread myThread1 = new Thread(drawMoneyRunnable); 7 Thread myThread2 = new Thread(drawMoneyRunnable); 8 myThread1.start(); 9 myThread2.start();10 }11 12 }13 14 class DrawMoneyRunnable implements Runnable {15 16 private Account account;17 private double drawAmount;18 19 public DrawMoneyRunnable(Account account, double drawAmount) {20 super();21 this.account = account;22 this.drawAmount = drawAmount;23 }24 25 public void run() {26 if (account.getBalance() >= drawAmount) { //127 System.out.println("取錢成功, 取出錢數為:" + drawAmount);28 double balance = account.getBalance() - drawAmount;29 account.setBalance(balance);30 System.out.println("余額為:" + balance);31 }32 }33 }34 35 class Account {36 37 private String accountNo;38 private double balance;39 40 public Account() {41 42 }43 44 public Account(String accountNo, double balance) {45 this.accountNo = accountNo;46 this.balance = balance;47 }48 49 public String getAccountNo() {50 return accountNo;51 }52 53 public void setAccountNo(String accountNo) {54 this.accountNo = accountNo;55 }56 57 public double getBalance() {58 return balance;59 }60 61 public void setBalance(double balance) {62 this.balance = balance;63 }64 65 }上面例子很容易理解,有一張銀行卡,里面有1000的余額,程序模擬你和你老婆同時在取款機進行取錢操作的場景。多次運行此程序,可能具有多個不同組合的輸出結果。其中一種可能的輸出為:
1 取錢成功, 取出錢數為:700.02 余額為:300.03 取錢成功, 取出錢數為:700.04 余額為:-400.0也就是說,對于一張只有1000余額的銀行卡,你們一共可以取出1400,這顯然是有問題的。
經過分析,問題在于Java多線程環境下的執行的不確定性。CPU可能隨機的在多個處于就緒狀態中的線程中進行切換,因此,很有可能出現如下情況:當thread1執行到//1處代碼時,判斷條件為true,此時CPU切換到thread2,執行//1處代碼,發現依然為真,然后執行完thread2,接著切換到thread1,接著執行完畢。此時,就會出現上述結果。
因此,講到線程安全問題,其實是指多線程環境下對共享資源的訪問可能會引起此共享資源的不一致性。因此,為避免線程安全問題,應該避免多線程環境下對此共享資源的并發訪問。
二.同步方法
對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱為同步方法??梢院唵卫斫獬蓪Υ朔椒ㄟM行了加鎖,其鎖對象為當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要獲得此同步鎖(且同時最多只有一個線程能夠獲得),只有當線程執行完此同步方法后,才會釋放鎖對象,其他的線程才有可能獲取此同步鎖,以此類推...
在上例中,共享資源為account對象,當使用同步方法時,可以解決線程安全問題。只需在run()方法前加上synshronized關鍵字即可。
1 public synchronized void run() {2 3 // ....4 5 }
三.同步代碼塊
正如上面所分析的那樣,解決線程安全問題其實只需限制對共享資源訪問的不確定性即可。使用同步方法時,使得整個方法體都成為了同步執行狀態,會使得可能出現同步范圍過大的情況,于是,針對需要同步的代碼可以直接另一種同步方式——同步代碼塊來解決。
同步代碼塊的格式為:
1 synchronized (obj) {2 3 //...4 5 }其中,obj為鎖對象,因此,選擇哪一個對象作為鎖是至關重要的。一般情況下,都是選擇此共享資源對象作為鎖對象。
如上例中,最好選用account對象作為鎖對象。(當然,選用this也是可以的,那是因為創建線程使用了runnable方式,如果是直接繼承Thread方式創建的線程,使用this對象作為同步鎖會其實沒有起到任何作用,因為是不同的對象了。因此,選擇同步鎖時需要格外小心...)
四.Lock對象同步鎖
上面我們可以看出,正因為對同步鎖對象的選擇需要如此小心,有沒有什么簡單點的解決方案呢?以方便同步鎖對象與共享資源解耦,同時又能很好的解決線程安全問題。
使用Lock對象同步鎖可以方便的解決此問題,唯一需要注意的一點是Lock對象需要與資源對象同樣具有一對一的關系。Lock對象同步鎖一般格式為:
1 class X { 2 3 // 顯示定義Lock同步鎖對象,此對象與共享資源具有一對一關系 4 private final Lock lock = new ReentrantLock(); 5 6 public void m(){ 7 // 加鎖 8 lock.lock(); 9 10 //... 需要進行線程安全同步的代碼11 12 // 釋放Lock鎖13 lock.unlock();14 }15 16 }
五.wait()/notify()/notifyAll()線程通信
在博文《Java總結篇系列:java.lang.Object》中有提及到這三個方法,雖然這三個方法主要都是用于多線程中,但實際上都是Object類中的本地方法。因此,理論上,任何Object對象都可以作為這三個方法的主調,在實際的多線程編程中,只有同步鎖對象調這三個方法,才能完成對多線程間的線程通信。
wait():導致當前線程等待并使其進入到等待阻塞狀態。直到其他線程調用該同步鎖對象的notify()或notifyAll()方法來喚醒此線程。
notify():喚醒在此同步鎖對象上等待的單個線程,如果有多個線程都在此同步鎖對象上等待,則會任意選擇其中某個線程進行喚醒操作,只有當前線程放棄對同步鎖對象的鎖定,才可能執行被喚醒的線程。
notifyAll():喚醒在此同步鎖對象上等待的所有線程,只有當前線程放棄對同步鎖對象的鎖定,才可能執行被喚醒的線程。
1 package com.QQyumidi; 2 3 public class ThreadTest { 4 5 public static void main(String[] args) { 6 Account account = new Account("123456", 0); 7 8 Thread drawMoneyThread = new DrawMoneyThread("取錢線程", account, 700); 9 Thread depositeMoneyThread = new DepositeMoneyThread("存錢線程", account, 700); 10 11 drawMoneyThread.start(); 12 depositeMoneyThread.start(); 13 } 14 15 } 16 17 class DrawMoneyThread extends Thread { 18 19 private Account account; 20 private double amount; 21 22 public DrawMoneyThread(String threadName, Account account, double amount) { 23 super(threadName); 24 this.account = account; 25 this.amount = amount; 26 } 27 28 public void run() { 29 for (int i = 0; i < 100; i++) { 30 account.draw(amount, i); 31 } 32 } 33 } 34 35 class DepositeMoneyThread extends Thread { 36 37 private Account account; 38 private double amount; 39 40 public DepositeMoneyThread(String threadName, Account account, double amount) { 41 super(threadName); 42 this.account = account; 43 this.amount = amount; 44 } 45 46 public void run() { 47 for (int i = 0; i < 100; i++) { 48 account.deposite(amount, i); 49 } 50 } 51 } 52 53 class Account { 54 55 private String accountNo; 56 private double balance; 57 // 標識賬戶中是否已有存款 58 private boolean flag = false; 59 60 public Account() { 61 62 } 63 64 public Account(String accountNo, double balance) { 65 this.accountNo = accountNo; 66 this.balance = balance; 67 } 68 69 public String getAccountNo() { 70 return accountNo; 71 } 72 73 public void setAccountNo(String accountNo) { 74 this.accountNo = accountNo; 75 } 76 77 public double getBalance() { 78 return balance; 79 } 80 81 public void setBalance(double balance) { 82 this.balance = balance; 83 } 84 85 /** 86 * 存錢 87 * 88 * @param depositeAmount 89 */ 90 public synchronized void deposite(double depositeAmount, int i) { 91 92 if (flag) { 93 // 賬戶中已有人存錢進去,此時當前線程需要等待阻塞 94 try { 95 System.out.println(Thread.currentThread().getName() + " 開始要執行wait操作" + " -- i=" + i); 96 wait(); 97 // 1 98 System.out.println(Thread.currentThread().getName() + " 執行了wait操作" + " -- i=" + i); 99 } catch (InterruptedException e) {100 e.printStackTrace();101 }102 } else {103 // 開始存錢104 System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i);105 setBalance(balance + depositeAmount);106 flag = true;107 108 // 喚醒其他線程109 notifyAll();110 111 // 2112 try {113 Thread.sleep(3000);114 } catch (InterruptedException e) {115 e.printStackTrace();116 }117 System.out.println(Thread.currentThread().getName() + "-- 存錢 -- 執行完畢" + " -- i=" + i);118 }119 }120 121 /**122 * 取錢123 * 124 * @param drawAmount125 */126 public synchronized void draw(double drawAmount, int i) {127 if (!flag) {128 // 賬戶中還沒人存錢進去,此時當前線程需要等待阻塞129 try {130 System.out.println(Thread.currentThread().getName() + " 開始要執行wait操作" + " 執行了wait操作" + " -- i=" + i);131 wait();132 System.out.println(Thread.currentThread().getName() + " 執行了wait操作" + " 執行了wait操作" + " -- i=" + i);133 } catch (InterruptedException e) {134 e.printStackTrace();135 }136 } else {137 // 開始取錢138 System.out.println(Thread.currentThread().getName() + " 取錢:" + drawAmount + " -- i=" + i);139 setBalance(getBalance() - drawAmount);140 141 flag = false;142 143 // 喚醒其他線程144 notifyAll();145 146 System.out.println(Thread.currentThread().getName() + "-- 取錢 -- 執行完畢" + " -- i=" + i); // 3147 }148 }149 150 }上面的例子演示了wait()/notify()/notifyAll()的用法。部分輸出結果為:
1 取錢線程 開始要執行wait操作 執行了wait操作 -- i=0 2 存錢線程 存款:700.0 -- i=0 3 存錢線程-- 存錢 -- 執行完畢 -- i=0 4 存錢線程 開始要執行wait操作 -- i=1 5 取錢線程 執行了wait操作 執行了wait操作 -- i=0 6 取錢線程 取錢:700.0 -- i=1 7 取錢線程-- 取錢 -- 執行完畢 -- i=1 8 取錢線程 開始要執行wait操作 執行了wait操作 -- i=2 9 存錢線程 執行了wait操作 -- i=110 存錢線程 存款:700.0 -- i=211 存錢線程-- 存錢 -- 執行完畢 -- i=212 取錢線程 執行了wait操作 執行了wait操作 -- i=213 取錢線程 取錢:700.0 -- i=314 取錢線程-- 取錢 -- 執行完畢 -- i=315 取錢線程 開始要執行wait操作 執行了wait操作 -- i=416 存錢線程 存款:700.0 -- i=317 存錢線程-- 存錢 -- 執行完畢 -- i=318 存錢線程 開始要執行wait操作 -- i=419 取錢線程 執行了wait操作 執行了wait操作 -- i=420 取錢線程 取錢:700.0 -- i=521 取錢線程-- 取錢 -- 執行完畢 -- i=522 取錢線程 開始要執行wait操作 執行了wait操作 -- i=623 存錢線程 執行了wait操作 -- i=424 存錢線程 存款:700.0 -- i=525 存錢線程-- 存錢 -- 執行完畢 -- i=526 存錢線程 開始要執行wait操作 -- i=627 取錢線程 執行了wait操作 執行了wait操作 -- i=628 取錢線程 取錢:700.0 -- i=729 取錢線程-- 取錢 -- 執行完畢 -- i=730 取錢線程 開始要執行wait操作 執行了wait操作 -- i=831 存錢線程 執行了wait操作 -- i=632 存錢線程 存款:700.0 -- i=7由此,我們需要注意如下幾點:
1.wait()方法執行后,當前線程立即進入到等待阻塞狀態,其后面的代碼不會執行;
2.notify()/notifyAll()方法執行后,將喚醒此同步鎖對象上的(任意一個-notify()/所有-notifyAll())線程對象,但是,此時還并沒有釋放同步鎖對象,也就是說,如果notify()/notifyAll()后面還有代碼,還會繼續進行,知道當前線程執行完畢才會釋放同步鎖對象;
3.notify()/notifyAll()執行后,如果右面有sleep()方法,則會使當前線程進入到阻塞狀態,但是同步對象鎖沒有釋放,依然自己保留,那么一定時候后還是會繼續執行此線程,接下來同2;
4.wait()/notify()/nitifyAll()完成線程間的通信或協作都是基于不同對象鎖的,因此,如果是不同的同步對象鎖將失去意義,同時,同步對象鎖最好是與共享資源對象保持一一對應關系;
5.當wait線程喚醒后并執行時,是接著上次執行到的wait()方法代碼后面繼續往下執行的。
當然,上面的例子相對來說比較簡單,只是為了簡單示例wait()/notify()/noitifyAll()方法的用法,但其本質上說,已經是一個簡單的生產者-消費者模式了
新聞熱點
疑難解答