關於多執行緒併發環境下的資料安全的問題
1.為什麼是重點?
以後在開發中,專案都是執行在伺服器當中,而伺服器已經將執行緒的定義、執行緒物件的建立、執行緒的啟動等,都已經實現完了。這些程式碼都不需要編寫,最重要的是要知道:編寫的程式需要放到一個多執行緒的環境下執行,更需要關注這些資料在多執行緒併發的環境下是否是安全的。
2.什麼時候資料在多執行緒併發的環境下會存在安全問題?
三個條件:
(1)條件1:多執行緒併發
(2)條件2:多執行緒有共享的資料
(3)條件3:共享的資料有修改的行為
滿足以上3個條件之後,就會存線上程安全問題。
3.如何解決執行緒安全問題?
當多執行緒併發的環境下,有共享資料,並且這個資料還會被修改,此時就存線上程安全問題。
此時應當使執行緒排隊執行(不能併發)。用排隊執行解決執行緒安全問題,這種機制稱為:執行緒同步機制。(這是專業術語的叫法,實際上就是執行緒不能併發了,必須排隊執行)。執行緒同步(也就是執行緒排隊)會犧牲一部分效率,但是資料安全是第一位的。
4.執行緒同步涉及到的兩個專業術語
4.1 非同步程式設計模型
執行緒t1和t2各自執行各自的,相互不管,誰也不需要等誰。(其實就是多執行緒併發,效率較高)
4.2 同步程式設計模型
執行緒t1和t2,在某一執行緒執行的時候,另一個執行緒必須等待正在執行的執行緒,一直到其執行完畢為止。兩個執行緒之間發生了等待關係,執行緒排隊執行,效率較低。
5.例子分析
1 package thread_safe; 2 3 public class Account { 4 5 //賬戶 6 private String sctno; 7 8 //餘額 9 private double balance; 10 11 public Account() { 12 13 } 14 15 public Account(String sctno, double balance) { 16 this.sctno = sctno; 17 this.balance = balance; 18 } 19 20 public String getSctno() { 21 return sctno; 22 } 23 24 public void setSctno(String sctno) { 25 this.sctno = sctno; 26 } 27 28 public double getBalance() { 29 return balance; 30 } 31 32 public void setBalance(double balance) { 33 this.balance = balance; 34 } 35 36 //取款方法 37 public void withdraw(double money){ 38 //t1和t2併發這個方法;t1、t2是兩個棧。兩個棧操作堆中同一個物件 39 //取款前的餘額 40 double before=this.getBalance(); 41 double after=before-money;//取款後的餘額 42 //更新餘額 43 //若t1執行到這裡,但還沒來得及執行第44行程式碼,t2執行緒進來withdraw()方法了,此時一定出現問題。 44 this.setBalance(after); 45 46 } 47 48 }
1 package thread_safe; 2 3 public class AccountThread extends Thread { 4 5 //兩個執行緒必須共享同一個賬戶物件 6 private Account act; 7 8 //通過構造方法傳遞過來賬戶物件 9 public AccountThread(Account act){ 10 this.act=act; 11 } 12 public void run(){ 13 //run()方法執行取款操作 14 //假設取款5000 15 double money=5000; 16 //取款 17 act.withdraw(money); 18 System.out.println(Thread.currentThread().getName()+"對賬戶"+act.getSctno()+"取款成功,餘額:"+act.getBalance()); 19 } 20 }
1 package thread_safe; 2 3 public class Test { 4 public static void main(String[] args){ 5 6 //建立一個賬戶物件 7 Account act=new Account("act-001",10000); 8 9 //建立兩個執行緒 10 Thread t1=new AccountThread(act); 11 Thread t2=new AccountThread(act); 12 13 //設定名字 14 t1.setName("t1"); 15 t2.setName("t2"); 16 17 //啟動執行緒取款 18 t1.start(); 19 t2.start(); 20 21 } 22 23 }
執行結果:
可以看到,兩個餘額都是5000,這就說明出現了問題。但是要注意但是,出現問題是個概率事件,即這種情況可能發生,也可能不發生,關鍵在於當某個執行緒即將執行this.setBalance(after);這行程式碼的時候,另一個執行緒是否已經執行withdraw()方法了,倘若另一個執行緒已經執行了withdraw()方法,那麼即將執行this.setBalance(after)這行程式碼的執行緒執行完這行程式碼後,肯定會出現問題。
為了放大這個問題,可以在this.setBalance(after)之前設定一個延時,這樣一定出錯,問題顯示的也更加明顯:
1 package thread_safe; 2 3 public class Account { 4 5 //賬戶 6 private String sctno; 7 8 //餘額 9 private double balance; 10 11 public Account() { 12 13 } 14 15 public Account(String sctno, double balance) { 16 this.sctno = sctno; 17 this.balance = balance; 18 } 19 20 public String getSctno() { 21 return sctno; 22 } 23 24 public void setSctno(String sctno) { 25 this.sctno = sctno; 26 } 27 28 public double getBalance() { 29 return balance; 30 } 31 32 public void setBalance(double balance) { 33 this.balance = balance; 34 } 35 36 //取款方法 37 public void withdraw(double money){ 38 //t1和t2併發這個方法;t1、t2是兩個棧。兩個棧操作堆中同一個物件 39 //取款前的餘額 40 double before=this.getBalance(); 41 double after=before-money;//取款後的餘額 42 43 //進行1秒的睡眠,模擬網路延時 44 try { 45 Thread.sleep(1000); 46 } catch (InterruptedException e) { 47 // TODO Auto-generated catch block 48 e.printStackTrace(); 49 } 50 //更新餘額 51 //若t1執行到這裡,但還沒來得及執行第44行程式碼,t2執行緒進來withdraw()方法了,此時一定出現問題。 52 this.setBalance(after); 53 54 } 55 56 }
這樣之前所說的概率事件就成了一個肯定發生的事件,即每次執行都會出現問題。