Java總結篇系列:Java多線程(三)

本文主要接着前面多線程的兩篇文章總結Java多線程中的線程安全問題。html

一.一個典型的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) {  //1
27             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.0
2 餘額爲:300.0
3 取錢成功, 取出錢數爲:700.0
4 餘額爲:-400.0

也就是說,對於一張只有1000餘額的銀行卡,大家一共能夠取出1400,這顯然是有問題的。安全

通過分析,問題在於Java多線程環境下的執行的不肯定性。CPU可能隨機的在多個處於就緒狀態中的線程中進行切換,所以,頗有可能出現以下狀況:當thread1執行到//1處代碼時,判斷條件爲true,此時CPU切換到thread2,執行//1處代碼,發現依然爲真,而後執行完thread2,接着切換到thread1,接着執行完畢。此時,就會出現上述結果。多線程

所以,講到線程安全問題,實際上是指多線程環境下對共享資源的訪問可能會引發此共享資源的不一致性。所以,爲避免線程安全問題,應該避免多線程環境下對此共享資源的併發訪問。併發

 

二.同步方法this

對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱爲同步方法。能夠簡單理解成對此方法進行了加鎖,其鎖對象爲當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要得到此同步鎖(且同時最多隻有一個線程可以得到),只有當線程執行完此同步方法後,纔會釋放鎖對象,其餘的線程纔有可能獲取此同步鎖,以此類推...spa

在上例中,共享資源爲account對象,當使用同步方法時,能夠解決線程安全問題。只需在run()方法前加上synshronized關鍵字便可。線程

1 public synchronized void run() { 2        
3     // ....
4  
5 }

 

三.同步代碼塊code

正如上面所分析的那樣,解決線程安全問題其實只需限制對共享資源訪問的不肯定性便可。使用同步方法時,使得整個方法體都成爲了同步執行狀態,會使得可能出現同步範圍過大的狀況,因而,針對須要同步的代碼能夠直接另外一種同步方式——同步代碼塊來解決。

同步代碼塊的格式爲:

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             // 2
112             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 drawAmount 125      */
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); // 3
147  } 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=1
10 存錢線程 存款:700.0 -- i=2
11 存錢線程-- 存錢 -- 執行完畢 -- i=2
12 取錢線程 執行了wait操做 執行了wait操做 -- i=2
13 取錢線程 取錢:700.0 -- i=3
14 取錢線程-- 取錢 -- 執行完畢 -- i=3
15 取錢線程 開始要執行wait操做 執行了wait操做 -- i=4
16 存錢線程 存款:700.0 -- i=3
17 存錢線程-- 存錢 -- 執行完畢 -- i=3
18 存錢線程 開始要執行wait操做 -- i=4
19 取錢線程 執行了wait操做 執行了wait操做 -- i=4
20 取錢線程 取錢:700.0 -- i=5
21 取錢線程-- 取錢 -- 執行完畢 -- i=5
22 取錢線程 開始要執行wait操做 執行了wait操做 -- i=6
23 存錢線程 執行了wait操做 -- i=4
24 存錢線程 存款:700.0 -- i=5
25 存錢線程-- 存錢 -- 執行完畢 -- i=5
26 存錢線程 開始要執行wait操做 -- i=6
27 取錢線程 執行了wait操做 執行了wait操做 -- i=6
28 取錢線程 取錢:700.0 -- i=7
29 取錢線程-- 取錢 -- 執行完畢 -- i=7
30 取錢線程 開始要執行wait操做 執行了wait操做 -- i=8
31 存錢線程 執行了wait操做 -- i=6
32 存錢線程 存款: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()方法的用法,但其本質上說,已是一個簡單的生產者-消費者模式了。

相關文章
相關標籤/搜索