一、執行緒併發同步概念
執行緒同步其核心就在於一個“同”。所謂“同”就是協同、協助、配合,“同步”就是協同步調昨,也就是按照預定的先後順序進行執行,即“你先,我等, 你做完,我再做”。
執行緒同步,就是當執行緒發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不會返回,其他執行緒也不能呼叫該方法。
就一般而言,我們在說同步、非同步的時候,特指那些需要其他元件來配合或者需要一定時間來完成的任務。在多執行緒程式設計裡面,一些較為敏感的資料時不允許被多個執行緒同時訪問的,使用執行緒同步技術,確保資料在任何時刻最多隻有一個執行緒訪問,保證資料的完整性。
二、執行緒同步中可能存在安全隱患
用生活中的場景來舉例:小生去銀行開個銀行賬戶,銀行給 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); } }