Java併發程式設計—ThreadLocal

擁抱心中的夢想發表於2019-03-01

一、什麼是ThreadLocal?

多執行緒共享變數的維護是非常頭痛的問題,採用樂觀悲觀策略,悲觀策略簡單地做法我們可以對共享變數加鎖實現,但是鎖的開銷是比較大的,因此我們也可以通過樂觀策略,採用類似CAS(Compare And Set)的方法進行維護,當然,在讀多寫少的情況下,我們還可以採用Copy-On-Write寫時複製的來控制共享變數,其中最經典的實現那就是JDK的CopyOnWriteArrayList了。

好了,說了這麼多,下面正式進入正題。我們可以想這麼一個問題,在多執行緒環境中如何對每一條執行緒的生命週期進行跟蹤,跟具體地說,在web專案中,我們通常會對某一個請求從進入系統到退出系統的整個生命週期進行日誌記錄,又或者說是對一個事務的全過程進行跟蹤記錄,通常的做法是採用為每個執行緒建立一個叫transactionId的值,用於標識每個執行緒並程式跟蹤,下圖紅圈即為transactionId的值。

Java併發程式設計—ThreadLocal

那麼你可能會說,我也可以不使用transactionId來跟蹤啊,我採用執行緒名不就好了嗎?道理是這樣,但是當你想對改值進行自定義呢?比如獲取客戶端的ip地址、被呼叫的介面名呢???

好了,假設我們採用transactionId來跟蹤執行緒,那麼如果這個transactionId是個共享變數的話,那我們不就是得對其做相關執行緒同步的操作,執行緒那麼多,那不煩死,笨死!嗯,TheadLocal就是在這種情況下而生了,一個TheadLocal代表一個變數,你千萬不要以為它代表一個執行緒,不然我會暈死!這個變數天生就是執行緒安全的。為什麼呢?因為它的工作原理是會為每條執行緒做一份變數的拷貝,各個執行緒的變數不會相互影響,自然就不會有執行緒安全問題咯!

醬紫,我畫張圖吧!

Java併發程式設計—ThreadLocal

TheadLocal的底層實現原理是通過ThreadLocalMap實現,時間匆忙,其原理不在本文講解範圍內,讀者自行學習原始碼!還有一點思想值得學習,多執行緒一般的同步機制採用了“以時間換空間”的方式,比如定義一個static變數,同步訪問,而ThreadLocal則採用了“以空間換時間”的方式。

二、例項解析

package com.wokao66.concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalTest {
	//執行緒本地變數
	private static ThreadLocal<Integer> sessionId = new ThreadLocal<>();
	public static void main(String[] args) {
		//執行緒1
		Thread thread1 = new Thread(new Runnable() {
			@Override
			public void run() {
				//我對ThreadLocal的值進行修改,自定義
				sessionId.set(1);
				try {
					Thread.sleep(1000);
					//模擬一個執行緒的生命週期多次獲取ThreadLocal變數的值,驗證是否在當前執行緒有做一份拷貝
					for (int i = 0; i < 5; i++) {
						System.err.println("[" + Thread.currentThread().getName() + "]" + sessionId.get());
					}
				} catch (InterruptedException e) {}
			}
		});
		//執行緒2
		Thread thread2 = new Thread(new Runnable() {
			@Override
			public void run() {
				//我對ThreadLocal的值進行修改,自定義
				sessionId.set(2);
				try {
					Thread.sleep(1000);
					//模擬一個執行緒的生命週期多次獲取ThreadLocal變數的值,驗證是否在當前執行緒有做一份拷貝
					for (int i = 0; i < 5; i++) {
						System.err.println("[" + Thread.currentThread().getName() + "]" + sessionId.get());
					}
				} catch (InterruptedException e) {}
			}
		});
		//執行緒1
		Thread thread3 = new Thread(new Runnable() {
			@Override
			public void run() {
				//我對ThreadLocal的值進行修改,自定義
				sessionId.set(3);
				try {
					Thread.sleep(1000);
					//模擬一個執行緒的生命週期多次獲取ThreadLocal變數的值,驗證是否在當前執行緒有做一份拷貝
					for (int i = 0; i < 5; i++) {
						System.err.println("[" + Thread.currentThread().getName() + "]" + sessionId.get());
					}
				} catch (InterruptedException e) {}
			}
		});
		ExecutorService service = Executors.newFixedThreadPool(30);
		service.execute(thread1);
		service.execute(thread2);
		service.execute(thread3);
	}
}
複製程式碼

控制檯輸出

[pool-1-thread-1]1
[pool-1-thread-2]2
[pool-1-thread-1]1
[pool-1-thread-2]2
[pool-1-thread-1]1
[pool-1-thread-1]1
[pool-1-thread-2]2
[pool-1-thread-2]2
[pool-1-thread-2]2
[pool-1-thread-1]1
[pool-1-thread-3]3
[pool-1-thread-3]3
[pool-1-thread-3]3
[pool-1-thread-3]3
[pool-1-thread-3]3
複製程式碼

可以看到每條執行緒對應的ThreadLocal是一樣的,證明了確實是有做拷貝操作!

三、ThreadLocal的應用場景

使用ThreadLocal首先一點那就是對變數的訪問不需要同步控制,常用的場景如下:

  • 1、對多執行緒進行跟蹤,常用於介面訪問日誌記錄,可以參考MDC機制
  • 2、用來解決資料庫連線、Session管理(其實也是一種跟蹤用途)

需要注意的是,既然TreadLocal是以採用空間換取時間的思想,所以可以想象TreadLocal並不適合來定義大物件,因為大物件的話每個執行緒都有拷貝,執行緒一多,效能必定受到牽連,甚至JVM丟擲ERROR

相關文章