1. 程式人生 > 實用技巧 >關於多執行緒併發環境下的資料安全的問題

關於多執行緒併發環境下的資料安全的問題

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 }

  這樣之前所說的概率事件就成了一個肯定發生的事件,即每次執行都會出現問題。