java執行緒安全問題以及同步的幾種方式
一、執行緒併發同步概念
執行緒同步其核心就在於一個“同”。所謂“同”就是協同、協助、配合,“同步”就是協同步調昨,也就是按照預定的先後順序進行執行,即“你先,我等, 你做完,我再做”。
執行緒同步,就是當執行緒發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不會返回,其他執行緒也不能呼叫該方法。
就一般而言,我們在說同步、非同步的時候,特指那些需要其他元件來配合或者需要一定時間來完成的任務。在多執行緒程式設計裡面,一些較為敏感的資料時不允許被多個執行緒同時訪問的,使用執行緒同步技術,確保資料在任何時刻最多隻有一個執行緒訪問,保證資料的完整性。
二、多執行緒中可能存在安全隱患
用生活中的場景來舉例:小明去銀行開個銀行賬戶,銀行給他 一張銀行卡和一張存摺,明用銀行卡和存摺來搞事情:
銀行卡瘋狂存錢,存完一次就看一下餘額;同時用存摺子不停地取錢,取一次錢就看一下餘額;
具體程式碼實現如下:
先弄一個銀行賬戶物件,封裝了存取插錢的方法:
package com.demo.thread.synchronize.no; /** * 銀行賬戶 提供存錢和取錢的方法 * 執行緒不安全 * @author 進擊的菜鳥 * @date: 2018年7月12日 上午10:23:00 */ public class Account { private int count = 0; /** * 存錢 * * @param money */ public void addAccount(String name, int money) { // 存錢 count += money; System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName()); SelectAccount(name); } /** * 取錢 * * @param name * @param money */ public void subAccount(String name, int money) { // 先判斷賬戶現在的餘額是否夠取錢金額 if (count - money < 0) { System.out.println("賬戶餘額不足!"); return; } // 取錢 count -= money; System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName()); SelectAccount(name); } /** * 查詢餘額 */ public void SelectAccount(String name) { System.out.println(name + "...賬戶餘額:" + count); } }
編寫銀行卡物件:
package com.demo.thread.synchronize.no; /** * 銀行卡賬戶 負責存錢 執行緒 * * @author 進擊的菜鳥 * @date: 2018年7月12日 上午10:27:36 */ public class Card implements Runnable { private String name; private Account account = new Account(); public Card(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.addAccount(name, 100); } } }
編寫存摺物件(和銀行卡方法幾乎一模一樣,就是名字不同而已):
package com.demo.thread.synchronize.no;
/**
* 存摺賬號 負責取錢 執行緒
* @author 進擊的菜鳥
* @date: 2018年7月12日 上午10:42:07
*/
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);
}
}
}
主方法測試,演示銀行卡瘋狂存錢,存摺瘋狂取錢:
package com.demo.thread.synchronize.no;
/**
* 這樣模擬 存錢取錢的執行緒安全問題 這樣檢視到資料明顯是有問題的
* @author 進擊的菜鳥
* @date: 2018年7月12日 上午10:49:34
*/
public class ACPTest {
public static void main(String[] args) {
// 開個銀行帳號
Account account = new Account();
// 開銀行帳號之後銀行給張銀行卡
Card card = new Card("card",account);
// 開銀行帳號之後銀行給張存摺
Paper paper = new Paper("存摺",account);
Thread thread1 = new Thread(card);
Thread thread2 = new Thread(paper);
thread1.start();
thread2.start();
}
}
結果顯示:從中可以看出 bug
從上面的例子裡就可以看出,銀行卡存錢和存摺取錢的過程中使用了 sleep() 方法,這只不過是模擬“系統卡頓”現象:銀行卡存錢之後,還沒來得及查餘額,存摺就在取錢,剛取完錢,銀行卡這邊“卡頓”又好了,查詢一下餘額,發現錢存的數量不對!當然還有“卡頓”時間比較長,存摺在卡頓的過程中,把錢全取了,等銀行卡這邊“卡頓”好了,一查發現錢全沒了的情況可能。
因此多個執行緒一起訪問共享的資料的時候,就會可能出現數據不同步的問題,本來一個存錢的時候不允許別人打斷我(當然實際中可以存在剛存就被取了,有交易記錄在,無論怎麼動這個帳號,都是自己的銀行卡和存摺在動錢。小明這個例子裡,要求的是存錢和查錢是一個完整過程,不可以拆分開),但從結果來看,並沒有實現小生想要出現的效果,這破壞了執行緒“原子性”。
三、執行緒同步中可能存在安全隱患的解決方法
3.1 同步程式碼塊:
使用 synchronized() 對需要完整執行的語句進行“包裹”,synchronized(Obj obj) 構造方法裡是可以傳入任何類的物件,
但是既然是監聽器就傳一個唯一的物件來保證“鎖”的唯一性,因此一般使用共享資源的物件來作為 obj 傳入 synchronized(Obj obj) 裡:
只需要鎖 Account 類中的存錢取錢方法就行了:
package com.demo.thread.synchronize.yes;
/**
* 銀行賬戶 提供存錢和取錢的方法 執行緒安全
*
* @author 進擊的菜鳥
* @date: 2018年7月12日 上午10:23:00
*/
public class Account {
private int count = 0;
/**
* 存錢
* synchronized 同步程式碼塊
* @param money
*/
public void addAccount(String name, int money) {
synchronized (this) {
// 存錢
count += money;
System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName());
SelectAccount(name);
}
}
/**
* 取錢
* synchronized 同步程式碼塊
* @param name
* @param money
*/
public void subAccount(String name, int money) {
synchronized (this) {
// 先判斷賬戶現在的餘額是否夠取錢金額
if (count - money < 0) {
System.out.println("賬戶餘額不足!");
return;
}
// 取錢
count -= money;
System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName());
SelectAccount(name);
}
}
/**
* 查詢餘額
*/
public void SelectAccount(String name) {
System.out.println(name + "...賬戶餘額:" + count);
}
}
3.2 同步方法
者在方法的申明裡申明 synchronized 即可:
package com.demo.thread.synchronize.yes2;
/**
* 銀行賬戶 提供存錢和取錢的方法 執行緒安全
*
* @author 進擊的菜鳥
* @date: 2018年7月12日 上午10:23:00
*/
public class Account {
private int count = 0;
/**
* 存錢
* synchronized 同步方法
* @param money
*/
public synchronized void addAccount(String name, int money) {
// 存錢
count += money;
System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName());
SelectAccount(name);
}
/**
* 取錢
* synchronized 同步方法
* @param name
* @param money
*/
public synchronized void subAccount(String name, int money) {
// 先判斷賬戶現在的餘額是否夠取錢金額
if (count - money < 0) {
System.out.println("賬戶餘額不足!");
return;
}
// 取錢
count -= money;
System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName());
SelectAccount(name);
}
/**
* 查詢餘額
*/
public void SelectAccount(String name) {
System.out.println(name + "...賬戶餘額:" + count);
}
}
3.3 使用同步鎖:
account 類建立私有的 ReetrantLock 物件,呼叫 lock() 方法,同步執行體執行完畢之後,需要用 unlock() 釋放鎖。
package com.demo.thread.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 銀行賬戶 提供存錢和取錢的方法 執行緒安全
*
* @author 進擊的菜鳥
* @date: 2018年7月12日 上午10:23:00
*/
public class Account {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
/**
* 存錢
*
* @param money
*/
public void addAccount(String name, int money) {
try {
lock.lock();
// 存錢
count += money;
System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName());
SelectAccount(name);
} finally {
lock.unlock();
}
}
/**
* 取錢
*
* @param name
* @param money
*/
public void subAccount(String name, int money) {
try {
lock.lock();
// 先判斷賬戶現在的餘額是否夠取錢金額
if (count - money < 0) {
System.out.println("賬戶餘額不足!");
return;
}
// 取錢
count -= money;
System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName());
SelectAccount(name);
} finally {
lock.unlock();
}
}
/**
* 查詢餘額
*/
public void SelectAccount(String name) {
System.out.println(name + "...賬戶餘額:" + count);
}
}
執行效果如下