1. 程式人生 > >java執行緒安全問題以及同步的幾種方式

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);
	}

}

執行效果如下