30. 線程本地存儲模式:沒有共享,就沒有傷害 - 併發設計模式 -待完善


多個線程同時讀寫同一共享變量存在併發問題, 避免共享,就能避免併發問題。

1. ThreadLocal的使用方法

	static class ThreadId {
		static final AtomicLong nextId = new AtomicLong(0);
		// 定義ThreadLocal變量
		static final ThreadLocal<Long> tl = 
			ThreadLocal.withInitial(() -> nextId.getAndIncrement());

		// 此方法會爲每個線程分配一個唯一的Id
		static long get() {
			return tl.get();
		}
	}
  • 一個線程前後兩次調用ThreadId的get()方法,兩次的返回值是相同的;
  • 兩個線程分別調用ThreadId的get()方法,那麼兩個線程看到的get()方法的返回值是不同的。

SimpleDateFormat不是線程安全的,在併發場景下可以用ThreadLocal解決線程安全問題。不同線程並不共享SimpleDateFormat,就像局部變量一樣,所以線程安全。

static class SafeDateFormat {
	// 定義ThreadLocal變量
	static final ThreadLocal<DateFormat> tl = ThreadLocal
			.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

	static DateFormat get() {
		return tl.get();
	}
}

2. ThreadLocal的工作原理

2.1 自己實現的ThreadLocal

在這裏插入圖片描述

class MyThreadLocal<T> {
	Map<Thread, T> locals = new ConcurrentHashMap<>();

	// 獲取線程變量
	T get() {
		return locals.get(Thread.currentThread());
	}

	// 設置線程變量
	void set(T t) {
		locals.put(Thread.currentThread(), t);
	}
}

直接創建一個Map,線程爲key,線程擁有的變量爲value。

2.2 java實現ThreadLocal

在這裏插入圖片描述

class Thread {
	// 內部持有ThreadLocalMap
	ThreadLocal.ThreadLocalMap threadLocals;
}

class ThreadLocal<T> {
	public T get() {
		// 首先獲取線程持有的
		// ThreadLocalMap
		ThreadLocalMap map = Thread.currentThread().threadLocals;
		// 在ThreadLocalMap中
		// 查找變量
		Entry e = map.getEntry(this);
		return e.value;
	}

	static class ThreadLocalMap {
		// 內部是數組而不是Map
		Entry[] table;

		// 根據ThreadLocal查找Entry
		Entry getEntry(ThreadLocal key) {
			// 省略查找邏輯
		}

		// Entry定義
		static class Entry extends WeakReference<ThreadLocal> {
			Object value;
		}
	}
}

Java的實現裏面也有一個Map,叫做ThreadLocalMap,不過持有ThreadLocalMap的不是ThreadLocal,而是Thread。Thread這個類內部有一個私有屬性threadLocals,其類型就是ThreadLocalMap,ThreadLocalMap的Key是ThreadLocal。

我們的設計裏面Map屬於ThreadLocal,而Java的實現裏面ThreadLocalMap則是屬於Thread。這兩種方式哪種更合理呢?很顯然Java的實現更合理一些。在Java的實現方案裏面,ThreadLocal僅僅是一個代理工具類,內部並不持有任何與線程相關的數據,所有和線程相關的數據都存儲在Thread裏面,這樣的設計容易理解。而從數據的親緣性上來講,ThreadLocalMap屬於Thread也更加合理

還有一個原因,不容易產生內存泄露. 在我們的設計方案中,ThreadLocal持有的Map會持有Thread對象的引用,這就意味着,只要ThreadLocal對象存在,那麼Map中的Thread對象就永遠不會被回收。ThreadLocal的生命週期往往都比線程要長,所以這種設計方案很容易導致內存泄露。而Java的實現中Thread持有ThreadLocalMap,而且ThreadLocalMap裏對ThreadLocal的引用還是弱引用(WeakReference),所以只要Thread對象可以被回收,那麼ThreadLocalMap就能被回收。Java的這種實現方案雖然看上去複雜一些,但是更加安全。

3. ThreadLocal與內存泄露

在線程池中使用ThreadLocal可能導致內存泄露,
// TODO 作者寫的不是很好理解,待補

4. InheritableThreadLocal與繼承性

不建議使用。

5. 總結

線程本地存儲模式本質上是一種避免共享的方案。

6. 課後思考

實際工作中,有很多平臺型的技術方案都是採用ThreadLocal來傳遞一些上下文信息,例如Spring使用ThreadLocal來傳遞事務信息。我們曾經說過,異步編程已經很成熟了,那你覺得在異步場景中,是否可以使用Spring的事務管理器呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章