【java 多執行緒】多執行緒併發同步問題及解決方法

weixin_30699831發表於2017-11-18

一、執行緒併發同步概念

執行緒同步其核心就在於一個“同”。所謂“同”就是協同、協助、配合,“同步”就是協同步調昨,也就是按照預定的先後順序進行執行,即“你先,我等, 你做完,我再做”。

執行緒同步,就是當執行緒發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不會返回,其他執行緒也不能呼叫該方法。

就一般而言,我們在說同步、非同步的時候,特指那些需要其他元件來配合或者需要一定時間來完成的任務。在多執行緒程式設計裡面,一些較為敏感的資料時不允許被多個執行緒同時訪問的,使用執行緒同步技術,確保資料在任何時刻最多隻有一個執行緒訪問,保證資料的完整性。

二、執行緒同步中可能存在安全隱患

用生活中的場景來舉例:小生去銀行開個銀行賬戶,銀行給 me 一張銀行卡和一張存摺,小生用銀行卡和存摺來搞事情:

銀行卡瘋狂存錢,存完一次就看一下餘額;同時用存摺子不停地取錢,取一次錢就看一下餘額;

具體程式碼實現如下:

先弄一個銀行賬戶物件,封裝了存取插錢的方法:

 1 package com.test.threadDemo2;
 2 
 3 /**
 4  * 銀行賬戶
 5  * @author Administrator
 6  *
 7  */
 8 public class Acount {
 9     private int count=0;
10     
11     /**
12      * 存錢
13      * @param money
14      */
15     public void addAcount(String name,int money) {
16         17             // 存錢
18             count += money;
19             System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
20             SelectAcount(name);
21 22     }
23     
24     /**
25      * 取錢
26      * @param money
27      */
28     public void subAcount(String name,int money) {
29         30             // 先判斷賬戶現在的餘額是否夠取錢金額
31             if(count-money < 0){  
32                 System.out.println("賬戶餘額不足!"); 
33                 return;  
34             } 
35             // 取錢
36             count -= money;
37             System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
38             SelectAcount(name);
39 40     }
41     
42     /**
43      * 查詢餘額
44      */
45     public void SelectAcount(String name) {
46         System.out.println(name+"...餘額:"+count);
47     }
48 }

編寫銀行卡物件:

 1 package com.test.threadDemo2;
 2 /**
 3  * 銀行卡負責存錢
 4  * @author Administrator
 5  *
 6  */
 7 public class Card implements Runnable{
 8     private String name;
 9     private Account account = new Account();
10     
11     public Card(String name,Account account) {
12         this.account = account;
13         this.name = name;
14     }
15     
16     @Override
17     public void run() {
18         
19         while(true) {
20             try {
21                 Thread.sleep(1000);
22             } catch (InterruptedException e) {
23                 e.printStackTrace();
24             }
25             account.addAccount(name,100);26         }
27     }
28     
29 }

編寫存摺物件(和銀行卡方法幾乎一模一樣,就是名字不同而已):

package com.test.threadDemo2;
/**
 * 存摺負責取錢
 * @author Administrator
 *
 */
public class Paper implements Runnable{
    private String name;
    private Account account = new Account();
    
    public Paper(String name,Account account) {
        this.account = account;
        this.name = name;
    }
    
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.subAccount(name,50);
        }
        
    }

}

主方法測試,演示銀行卡瘋狂存錢,存摺瘋狂取錢:

 1 package com.test.threadDemo2;
 2 
 3 public class ThreadDemo2 {
 4     public static void main(String[] args) {
 5         
 6         // 開個銀行帳號
 7         Account account = new Account();
 8         // 開銀行帳號之後銀行給張銀行卡
 9         Card card = new Card("card",account);
10         // 開銀行帳號之後銀行給張存摺
11         Paper paper = new Paper("存摺",account);
12         
13         Thread thread1 = new Thread(card);
14         Thread thread2 = new Thread(paper);
15         
16         thread1.start();
17         thread2.start();            
18     }
19 }

結果顯示:從中可以看出 bug

 

從上面的例子裡就可以看出,銀行卡存錢和存摺取錢的過程中使用了 sleep() 方法,這只不過是小生模擬“系統卡頓”現象:銀行卡存錢之後,還沒來得及查餘額,存摺就在取錢,剛取完錢,銀行卡這邊“卡頓”又好了,查詢一下餘額,發現錢存的數量不對!當然還有“卡頓”時間比較長,存摺在卡頓的過程中,把錢全取了,等銀行卡這邊“卡頓”好了,一查發現錢全沒了的情況可能。

因此多個執行緒一起訪問共享的資料的時候,就會可能出現資料不同步的問題,本來一個存錢的時候不允許別人打斷我(當然實際中可以存在剛存就被取了,有交易記錄在,無論怎麼動這個帳號,都是自己的銀行卡和存摺在動錢。小生這個例子裡,要求的是存錢和查錢是一個完整過程,不可以拆分開),但從結果來看,並沒有實現小生想要出現的效果,這破壞了執行緒“原子性”。

三、執行緒同步中可能存在安全隱患的解決方法

  從上面的例子中可以看出執行緒同步中存在安全隱患,我們必須不能忽略,所以要引入“鎖”(術語叫監聽器)的概念:

3.1 同步程式碼塊:

  使用 synchronized() 對需要完整執行的語句進行“包裹”,synchronized(Obj obj) 構造方法裡是可以傳入任何類的物件,

  但是既然是監聽器就傳一個唯一的物件來保證“鎖”的唯一性,因此一般使用共享資源的物件來作為 obj 傳入 synchronized(Obj obj) 裡:

  只需要鎖 Account 類中的存錢取錢方法就行了:

package com.test.threadDemo2;

/**
 * 銀行賬戶
 * @author Administrator
 *
 */
public class Acount {
    private int count=0;
    
    /**
     * 存錢
     * @param money
     */
    public void addAcount(String name,int money) {
        synchronized(this) {
            // 存錢
            count += money;
            System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
            SelectAcount(name);
        }
    }
    
    /**
     * 取錢
     * @param money
     */
    public void subAcount(String name,int money) {
        synchronized(this) {
            // 先判斷賬戶現在的餘額是否夠取錢金額
            if(count-money < 0){  
                System.out.println("賬戶餘額不足!"); 
                return;  
            } 
            // 取錢
            count -= money;
            System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
            SelectAcount(name);
        }
    }
    
    /**
     * 查詢餘額
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...餘額:"+count);
    }
}

3.2 同步方法

者在方法的申明裡申明 synchronized 即可:

 1 package com.test.threadDemo2;
 2 /**
 3  * 銀行賬戶
 4  * @author Administrator
 5  *
 6  */
 7 public class Acount {
 8     private int count;
 9     
10     /**
11      * 存錢
12      * @param money
13      */
14     public synchronized void addAcount(String name,int money) {
15             // 存錢
16             count += money;
17             System.out.println(name+"...存入:"+money);
18     }
19     
20     /**
21      * 取錢
22      * @param money
23      */
24     public synchronized void subAcount(String name,int money) {
25             // 先判斷賬戶現在的餘額是否夠取錢金額
26             if(count-money < 0){  
27                 System.out.println("賬戶餘額不足!");  
28                 return;  
29             } 
30             // 取錢
31             count -= money;
32             System.out.println(name+"...取出:"+money);
33     }
34     
35     /**
36      * 查詢餘額
37      */
38     public void SelectAcount(String name) {
39         System.out.println(name+"...餘額:"+count);
40     }
41 }

執行效果:

3.3 使用同步鎖:

account 類建立私有的 ReetrantLock 物件,呼叫 lock() 方法,同步執行體執行完畢之後,需要用 unlock() 釋放鎖。

package com.test.threadDemo2;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 銀行賬戶
 * @author Administrator
 *
 */
public class Acount {
    private int count;
    private ReentrantLock lock = new ReentrantLock();
    
    /**
     * 存錢
     * @param money
     */
    public void addAcount(String name,int money) {
        lock.lock();
        try{
            // 存錢
            count += money;
            System.out.println(name+"...存入:"+money);
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 取錢
     * @param money
     */
    public void subAcount(String name,int money) {
        lock.lock();
        try{
            // 先判斷賬戶現在的餘額是否夠取錢金額
            if(count-money < 0){  
                System.out.println("賬戶餘額不足!");  
                return;  
            } 
            // 取錢
            count -= money;
            System.out.println(name+"...取出:"+money);
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 查詢餘額
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...餘額:"+count);
    }
}

執行效果: 

 

 四、死鎖

當執行緒需要同時持有多個鎖時,有可能產生死鎖。考慮如下情形:

執行緒 A 當前持有互斥所鎖 lock1,執行緒 B 當前持有互斥鎖 lock2。

接下來,當執行緒 A 仍然持有 lock1 時,它試圖獲取 lock2,因為執行緒 B 正持有 lock2,因此執行緒 A 會阻塞等待執行緒 B 對 lock2 的釋放。

如果此時執行緒 B 在持有 lock2 的時候,也在試圖獲取 lock1,因為執行緒 A 正持有 lock1,因此執行緒 B 會阻塞等待 A 對 lock1 的釋放。

二者都在等待對方所持有鎖的釋放,而二者卻又都沒釋放自己所持有的鎖,這時二者便會一直阻塞下去。這種情形稱為死鎖。

1 package com.testDeadLockDemo;
2 
3 public class LockA {
4     
5     private LockA(){}
6     
7     public static final LockA lockA = new LockA();
8 }
1 package com.testDeadLockDemo;
2 
3 public class LockB {
4     private LockB(){}
5     
6     public static final LockB lockB = new LockB();
7 }
package com.testDeadLockDemo;

public class DeadLock implements Runnable{
    private int i=0;
    
    @Override
    public void run() {
        while(true) {
            if(i%2==0){
                synchronized(LockA.lockA) {
                    System.out.println("if...lockA");
                    synchronized(LockB.lockB) {
                        System.out.println("if...lockB");
                    }
                }
            }else {
                synchronized(LockB.lockB) {
                    System.out.println("else...lockB");
                    synchronized(LockA.lockA) {
                        System.out.println("else...lockA");
                    }
                }
            }
            i++;
        }
        
    }
}

測試:

 1 package com.testDeadLockDemo;
 2 
 3 public class Test {
 4     public static void main(String[] args) {
 5         DeadLock deadLock = new DeadLock();
 6         
 7         Thread t1 = new Thread(deadLock);
 8         Thread t2 = new Thread(deadLock);
 9         t1.start();
10         t2.start();
11         
12     }
13 }

執行結果:

 五、執行緒通訊

在共享資源中增加鏢旗,當鏢旗為真的時候才可以存錢,存完了就把鏢旗設定成假,當取款的時候發現鏢旗為假的時候,可以取款,取完款就把鏢旗設定為真。

只需修改 Account 類 和 測試類 即可

 1 package com.test.threadDemo2;
 2 
 3 /**
 4  * 銀行賬戶
 5  * @author Administrator
 6  *
 7  */
 8 public class Acount {
 9     private boolean flag=false;    // 預設flag 為false,要求必須先存款再取款
10     private int count=0;
11     
12     /**
13      * 存錢
14      * @param money
15      */
16     public void addAcount(String name,int money) {
17         synchronized(this) {
18             // flag 為true 表示可以存款,否則不可以存款
19             if(flag) {
20                 try {
21                     this.wait();
22                 } catch (InterruptedException e) {
23                     // TODO Auto-generated catch block
24                     e.printStackTrace();
25                 }
26             }else {
27                 // 存錢
28                 count += money;
29                 System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
30                 SelectAcount(name);
31                 flag = true;
32                 this.notifyAll();
33             }
34         }
35     }
36     
37     /**
38      * 取錢
39      * @param money
40      */
41     public void subAcount(String name,int money) {
42         synchronized(this) {
43             if(!flag) {
44                 try {
45                     this.wait();
46                 } catch (InterruptedException e) {
47                     e.printStackTrace();
48                 }
49             }else {
50             // 先判斷賬戶現在的餘額是否夠取錢金額
51             if(count-money < 0){  
52                 System.out.println("賬戶餘額不足!"); 
53                 return;  
54             } 
55                 // 取錢
56                 count -= money;
57                 System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
58                 SelectAcount(name);
59                 flag = false;
60                 this.notifyAll();
61             }
62         }
63     }
64     
65     /**
66      * 查詢餘額
67      */
68     public void SelectAcount(String name) {
69         System.out.println(name+"...餘額:"+count);
70     }
71 }
 1 package com.test.threadDemo2;
 2 
 3 public class ThreadDemo2 {
 4     public static void main(String[] args) {
 5         
 6         // 開個銀行帳號
 7         Acount acount = new Acount();
 8         
 9         // 開銀行帳號之後銀行給張銀行卡
10         Card card1 = new Card("card1",acount);
11         Card card2 = new Card("card2",acount);
12         Card card3 = new Card("card3",acount);
13         
14         // 開銀行帳號之後銀行給張存摺
15         Paper paper1 = new Paper("paper1",acount);
16         Paper paper2 = new Paper("paper2",acount);
17         
18         // 建立三個銀行卡
19         Thread thread1 = new Thread(card1,"card1");
20         Thread thread2 = new Thread(card2,"card2");
21         Thread thread3 = new Thread(card3,"card3");
22         // 建立兩個存摺
23         Thread thread4 = new Thread(paper1,"paper1");
24         Thread thread5 = new Thread(paper2,"paper2");
25         
26         thread1.start();
27         thread2.start();
28         thread3.start();
29         
30         thread4.start();
31         thread5.start();
32     }
33 }

執行結果:

 使用同步鎖也可以達到相同的目的:

package com.test.threadDemo2;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 銀行賬戶
 * @author Administrator
 *
 */
public class Acount2 {
    private boolean flag=false;    // 預設flag 為false,要求必須先存款再取款
    private int count=0;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    
    /**
     * 存錢
     * @param money
     */
    public void addAcount(String name,int money) {
        lock.lock();
        try {
            // flag 為true 表示可以存款,否則不可以存款
            if(flag) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }else {
                // 存錢
                count += money;
                System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
                flag = true;
                condition.signalAll();
            }
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 取錢
     * @param money
     */
    public void subAcount(String name,int money) {
        lock.lock();
        try {
            if(!flag) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
            // 先判斷賬戶現在的餘額是否夠取錢金額
            if(count-money < 0){  
                System.out.println("賬戶餘額不足!"); 
                return;  
            } 
                // 取錢
                count -= money;
                System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
                flag = false;
                condition.signalAll();
            }
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 查詢餘額
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...餘額:"+count);
    }
}

 

轉載於:https://www.cnblogs.com/mujingyu/p/7856388.html

相關文章