Java併發基礎06. 執行緒範圍內共享資料

公眾號_程式設計師私房菜發表於2019-01-18

歡迎關注我的微信公眾號:程式設計師私房菜(id:eson_15)

假設現在有個公共的變數 data,有不同的執行緒都可以去操作它,如果在不同的執行緒對 data 操作完成後再去取這個 data,那麼肯定會出現執行緒間的資料混亂問題,因為 A 執行緒在取 data 資料前可能 B 執行緒又對其進行了修改,下面寫個程式來說明一下該問題:

public class ThreadScopeShareData {

	private static int data = 0;//公共的資料
	
	public static void main(String[] args) {
		for(int i = 0; i < 2; i ++) { //開啟兩個執行緒
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					int temp = new Random().nextInt();
					System.out.println(Thread.currentThread().getName() + " has put a data: " + temp); //列印出來為了看效果
					data = temp; //運算元據:賦新值

					new TestA().getData();
					new TestB().getData();
				}
			}).start();
		}
	}
	
	static class TestA {
		public void getData() {
			System.out.println("A get data from " + Thread.currentThread().getName() + ": " + data);//取出公共資料data
		}
	}
	
	static class TestB {
		public void getData() {
			System.out.println("B get data from " + Thread.currentThread().getName() + ": " + data);
		}
	}
}
複製程式碼

來看一下列印出來的結果:

Thread-0 has put a data: -1885917900
Thread-1 has put a data: -1743455464
A get data from Thread-0: -1743455464
A get data from Thread-1: -1743455464
B get data from Thread-1: -1743455464
B get data from Thread-0: -1743455464

從結果中可以看出,兩次對 data 賦的值確實不一樣,但是兩個執行緒最後列印出來的都是最後賦的那個值,說明 Thread-0 拿出的資料已經不對了,這就是執行緒間共享資料帶來的問題。

當然,我們完全可以使用 synchronized 關鍵字將 run() 方法中的幾行程式碼給套起來,這樣每個執行緒各自執行完,列印出各自的資訊,這是沒問題的,確實可以解決上面的執行緒間共享資料問題。但是,這是以其他執行緒被阻塞為代價的,即 Thread-0 在執行的時候,Thread-1 就被阻塞了,必須等待 Thread-0 執行完了才能執行。

那麼如果我想兩個執行緒同時跑,並且互不影響各自取出的值,該怎麼辦呢?這也是本文所要總結的重點,解決該問題的思想是:雖然現在都在操作公共資料 data,但是不同的執行緒本身對這個 data 要維護一個副本,這個副本不是執行緒間所共享的,而是每個執行緒所獨有的,所以不同執行緒中所維護的 data 是不一樣的,最後取的時候,是哪個執行緒,我就從哪個執行緒中取該 data。

基於上面這個思路,我再把上面的程式做一修改,如下:

public class ThreadScopeShareData {

	private static int data = 0;//公共的資料
	//定義一個Map以鍵值對的方式儲存每個執行緒和它對應的資料,即Thread:data
	private static Map<Thread, Integer> threadData = Collections.synchronizedMap(new HashMap<Thread, Integer>());
	
	public static void main(String[] args) {
		for(int i = 0; i < 2; i ++) {
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					int temp = new Random().nextInt();
					System.out.println(Thread.currentThread().getName() + " has put a data: " + temp); //列印出來為了看效果					
					threadData.put(Thread.currentThread(), temp); //向Map中存入本執行緒data資料的一個副本
					data = temp; //運算元據:賦新值
					new TestA().getData();
					new TestB().getData();
				}
			}).start();
		}
	}
	
	static class TestA {
		public void getData() {
			System.out.println("A get data from " + Thread.currentThread().getName() + ": " 
				+ threadData.get(Thread.currentThread())); //取出各執行緒維護的那個副本
		}
	}
	
	static class TestB {
		public void getData() {
			System.out.println("B get data from " + Thread.currentThread().getName() + ": " 
				+ threadData.get(Thread.currentThread()));
		}
	}
}
複製程式碼

上面程式中維護了一個 Map,鍵值對分別是執行緒和它的資料,那麼在操作 data 的時候,先把各自的資料儲存到這個 Map 中,這樣每個執行緒儲存的肯定不同,當再取的時候,根據當前執行緒物件作為 key 來取出對應的 data 副本,這樣不同的執行緒之間就不會相互影響了。這個 HashMap 也需要包裝一下,因為 HashMap 是非執行緒安全的,上面的程式中,不同的執行緒有對 HashMap 進行寫操作,就有可能產生併發問題,所以也要包裝一下。最後來看一下執行結果:

Thread-0 has put a data: 1817494992
Thread-1 has put a data: -1189758355
A get data from Thread-0: 1817494992
A get data from Thread-1: -1189758355
B get data from Thread-0: 1817494992
B get data from Thread-1: -1189758355

就是執行緒範圍內共享資料,即同一個執行緒裡面這個資料是共享的,執行緒間是不共享的。

這讓我聯想到了學習資料庫的時候用到的 ThreadLocal,運算元據庫需要 connection,如果當前執行緒中有就拿當前執行緒中存的 connection,否則就新建一個放到當前執行緒中,這樣就不會出現問題,因為每個執行緒本身共享了一個 connection,它不是執行緒間共享的。這也很好理解,這個 connection 肯定不能共享,假設 A 和 B 使用者都拿到這個 connection 並開啟了事務,現在 A 開始轉賬了,但是錢還沒轉好,B 轉好了關閉了事務,那麼A那邊就出問題了。

執行緒範圍內共享資料的問題就總結這麼多吧~如果有問題,歡迎指正,我們一起進步!

也歡迎大家關注我的微信公眾號:程式設計師私房菜。我會持續輸出更多文章。

公眾號

相關文章