1. 程式人生 > >多執行緒併發庫高階應用 之 執行緒範圍內共享資料

多執行緒併發庫高階應用 之 執行緒範圍內共享資料

筆記摘要:

 所謂執行緒範圍內共享資料,即對於相同的程式程式碼,多個模組在同一個執行緒中執行時要共享一份資料,而在另外執行緒中執行時又共享另外一份資料,

             API中為我們提供了一個操作執行緒範圍內共享資料的類ThreadLocal,對於執行緒範圍內共享資料的應用,在ThreadLocal的應用場景中進行了介紹,然後

             主要對它的使用進行講解,演示了由單一資料的共享到將多個數據封裝到一個物件中,然後進行共享。在開始先用一個Map集合簡單實現執行緒範圍內資料的共享

一、使用Map實現執行緒範圍內資料的共享

原理:

    將執行緒物件作為map的鍵存入,這樣就保證了map物件的唯一,也就保證了執行緒內資料的唯一

關鍵:  

    明確一點,把當前執行緒物件作為map集合的鍵存進去

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class ThreadScopeShareData {

	private static int data = 0;	//定義一個全域性的成員變數
	private static Map<Thread, Integer> threadData = 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 data = new Random().nextInt();	//準備一個數據
					System.out.println(Thread.currentThread().getName() 
							+ " has put data :" + data);
					//把當前執行緒物件作為鍵,就可以保證map物件的唯一,即保證執行緒內的資料唯一
					threadData.put(Thread.currentThread(), data);
					new A().get();
					new B().get();
				}
			}).start();
		}
	}
	//定義一個類模擬獲取資料
	static class A{
		public void get(){
			int data = threadData.get(Thread.currentThread());
			System.out.println("A from " + Thread.currentThread().getName() 
					+ " get data :" + data);
		}
	}
	
	static class B{
		public void get(){
			int data = threadData.get(Thread.currentThread());			
			System.out.println("B from " + Thread.currentThread().getName() 
					+ " get data :" + data);
		}		
	}
}

列印結果

Thread-0 has put data:-49248136

Thread-1 has put data:311124475

A from Thread-0 get data:-49248136

A from Thread-1 get data:311124475

B from Thread-0 get data:-49248136

B from Thread-1 get data:311124475


二、ThreadLocal類

1、ThreadLocal的作用和目的:

     用於實現執行緒內的資料共享,即對於相同的程式程式碼,多個模組在同一個執行緒中執行時要共享一份資料,而在另外執行緒中執行時又共享另外一份資料。

2、 每個執行緒呼叫全域性ThreadLocal物件的set方法,就相當於往其內部的map中增加一條記錄,key分別是各自的執行緒,value是各自的set方

      法傳進去的值。線上程結束時可以呼叫ThreadLocal.clear()方法,這樣會更快釋放記憶體,不呼叫也可以,因為執行緒結束後也可以自動釋放

      相關的ThreadLocal變數。

三、ThreadLocal的應用場景

1、訂單處理包含一系列操作:減少庫存量、增加一條流水臺賬、修改總賬,這幾個操作要在同一個事務中完成,通常也即同一個執行緒中進行處理,

      如果累加公司應收款的操作失敗了,則應該把前面的操作回滾,否則,提交所有操作,這要求這些操作使用相同的資料庫連線物件,而這些操作

      的程式碼分別位於不同的模組類中。

2、 銀行轉賬包含一系列操作:把轉出帳戶的餘額減少,把轉入帳戶的餘額增加,這兩個操作要在同一個事務中完成,它們必須使用相同的資料庫連

        接物件,轉入和轉出操作的程式碼分別是兩個不同的帳戶物件的方法。

3、例如Strut2的ActionContext,同一段程式碼被不同的執行緒呼叫執行時,該程式碼操作的資料是每個執行緒各自的狀態和資料,對於不同的執行緒來說,getContext方

       法拿到的物件都不相同,對同一個執行緒來說,不管呼叫getContext方法多少次和在哪個模組中getContext方法,拿到的都是同一個。

執行緒範圍內共享資料示意圖


四、實現對ThreadLocal變數的封裝,讓外界不要直接操作ThreadLocal變數

      由於對基本型別的資料的封裝,這種應用相對很少見。而對物件型別的資料的封裝,比較常見,即讓某個類針對不同執行緒分別建立一個獨立的例項物件。      所以我們要對資料進行封裝。

實現方式一

示例說明:

       1、 該示例包含了對基本型別資料的共享和物件型別資料的共享

       2、定義一個全域性共享的ThreadLocal變數,然後啟動多個執行緒向該ThreadLocal變數中儲存一個隨機值,接著各個執行緒呼叫另外其他多個類

            的方法,這多個類的方法中讀取這個ThreadLocal變數的值,就可以看到多個類在同一個執行緒中共享同一份資料。

       3、但這裡每次儲存資料時,都是使用同一個ThreadLocal物件,只是重新賦值而已

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class ThreadLocalTest {
	
	private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
	//建立一個儲存封裝類物件的ThreadLocal
	private static ThreadLocal<MyThreadScopeData> myThreadScopeData = new  ThreadLocal<MyThreadScopeData>();
	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 data = new Random().nextInt();
			System.out.println(Thread.currentThread().getName()+"has put data : "+data);
			x.set(data);	
			
			//共享多個數據
			//將資料封裝在myData物件中,並將myData作為myThreadScopeData的鍵
			MyThreadScopeData myData = new MyThreadScopeData();
			myData.setName("name "+data);
			myData.setAge(data);
			myThreadScopeData.set(myData);
			
			new A().get();
			new B().get();
		  	}
	     }).start();
      }
   }
	
	static class A{
		public void get(){
			int data = x.get();
			System.out.println("A from "+Thread.currentThread().getName()+" get data :"+data);
		
			//從myData中取出資料,並獲取當前執行緒名,資料
			MyThreadScopeData myData = myThreadScopeData.get();
			System.out.println("A from "+Thread.currentThread().getName()+" getMyData: " + 
					myData.getName() + "," +myData.getAge());
		}
	}
		
	static class B{
		public void get(){
			int data = x.get();
			System.out.println("B from "+Thread.currentThread().getName()+" get data :"+data);
			MyThreadScopeData myData = myThreadScopeData.get();
			System.out.println("B from "+Thread.currentThread().getName()+" getMyData: " + 
					myData.getName() + "," +myData.getAge());
		}
	}
}

//封裝資料的類
class MyThreadScopeData{
	
	private String name;
	private int age;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

實現方式二

 示例說明:

這裡模擬原始的單例模式,它們的區別是:單例模式中只有唯一的一個例項,而這裡是每個執行緒擁有

    自己唯一的例項,只要是已經建立,就直接返回,保證每個執行緒擁有自己的唯一一份例項

優點:

     這裡可以返回每個執行緒自己唯一的例項物件,所以不必在外面定義,當在程式碼中的任意地方想獲取到一個可以儲存自己資料的執行緒例項的時候直接去呼叫

     getThreadInstance方法即可,直接定義在資料物件的內部,和資料關係更緊密,而方式一,則每次想存入資料的時候都需要在外面建立一個ThreadLocal物件用

    於儲存資料。所以方式二更具封裝性。

package cn.itcast.heima2;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class ThreadLocalTest {
	
	//建立一個ThreadLocal物件
	private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
	
	public static void main(String[] args) {
		for(int i=0;i<2;i++){
			new Thread(new Runnable(){
				@Override
				public void run() {
					int data = new Random().nextInt();
					System.out.println(Thread.currentThread().getName() 
							+ " has put data :" + data);
					x.set(data);	//往當前執行緒存入一條資料
					
					//獲取與當前執行緒繫結的例項並設定值
					MyThreadScopeData.getThreadInstance().setName("name:" + data);
					MyThreadScopeData.getThreadInstance().setAge(data);
					new A().get();
					new B().get();
				}
			}).start();
		}
	}
	
	static class A{
		public void get(){
			int data = x.get();		//獲取當前執行緒中的資料
			System.out.println("A from " + Thread.currentThread().getName() 
					+ " get data :" + data);
			
			//獲取與當前執行緒繫結的例項
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("A from " + Thread.currentThread().getName() 
					+ " getMyData: " + myData.getName() + "," +
					myData.getAge());
		}
	}
	
	static class B{
		public void get(){
			int data = x.get();			
			System.out.println("B from " + Thread.currentThread().getName() 
					+ " get data :" + data);
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("B from " + Thread.currentThread().getName() 
					+ " getMyData: " + myData.getName() + ",age: " +
					myData.getAge());			
		}		
	}
}

//一個綁定當前執行緒的類
class MyThreadScopeData{

	private MyThreadScopeData(){}	//構造方法私有化
	private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();
	
	//定義一個靜態方法,返回各執行緒自己的例項 
	//這裡不必用同步,因為每個執行緒都要建立自己的例項,所以沒有執行緒安全問題。
	public static /*synchronized*/ MyThreadScopeData getThreadInstance(){
		MyThreadScopeData instance = map.get();		//獲取當前執行緒繫結的例項
		if(instance == null){		
			instance = new MyThreadScopeData();
			map.set(instance);	//建立完之後,將例項物件存進去
		}
		return instance;
	}

	private String name;
	private int age;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

總結:

       一個ThreadLocal代表一個變數,故其中只能放一個數據,有兩個變數都要執行緒範圍內共享,則要定義兩個ThreadLocal物件,如果資料更多

      就很麻煩,可以先定義一個物件封裝變數,然後在ThreadLocal中儲存這一個物件,而這些操作都在提供執行緒資料類中完成。