Java ThreadLocal詳解
ThreadLocal,執行緒本地變數,作為多執行緒程式設計中很重要的一個元件,有很多的應用。比如資料庫連線池的實現,以及自定義分散式鎖的實現等。今天,就來介紹一下ThreadLocal的內部原理以及不規範的使用會導致的問題--記憶體洩漏。
本文的內容結構如下:
- ThreadLocal 的原理和原始碼
- ThreadLocal帶來的問題
- ThreadLocal的例項應用 - 基於Redis的分散式自旋鎖
一、ThreadLocal
- ThreadLocal核心方法
說到一個類,必然先檢視這個類的構造方式。ThreadLocal採用函數語言程式設計,定義一個靜態方法,傳入一個函式,從而構造ThreadLocal。例如:
ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() ->
{
return new SimpleDateFormat("yyyy-MM-dd");
});
// 或者簡寫為
ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"))
ThreadLocal中最核心的兩個方法,get 和 set。下面從原始碼角度分別對這兩個方法進行解析。
public T get() { Thread t = Thread.currentThread(); // 獲取當前執行緒 ThreadLocalMap map = getMap(t); // ThreadLocalMap,ThreadLocal中的重要的內部類 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); // 否則直接設定初始值 } public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
從原始碼中可以看出,get 和 set的方法都很簡單,其中涉及到的核心為 ThreadLocal的內部類ThreadLocalMap以及 核心方法getMap和createMap。
- ThreadLocalMap
和HashMap類似,內部也有一個節點類 Entry,該Entry繼承自弱引用。使用ThreadLocal作為key,要設定的值作為value。Entry的原始碼如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在 Thread 中,有 ThreadLocalMap 型別的欄位。
Thread中的 ThreadLocalMap型別欄位
接下來,我們來看下 getMap 和 createMap 方法的實現,其實很簡單。
public class ThreadLocal<T> {
//...... 省略其他方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// 傳入的key 為當前的ThreadLocal物件 this
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
二、ThreadLocal帶來的問題
由於 Entry繼承了弱引用,在不顯示的刪除 key 的時候,可能會造成 記憶體洩露。原因是,沒有顯示刪除key時,在發生GC的時候,弱引用會直接被回收掉,而對應的 value 卻還在記憶體中,這樣會造成記憶體洩漏。避免記憶體洩漏,需顯示呼叫 remove 方法。
弱引用(WeakReference)並不能使物件豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下物件的途徑。這就可以用來構建一種沒有特定約束的關係,比如,維護一種非強制性的對映關係,如果試圖獲取時物件還在,就使用它,否則重現例項化。它同樣是很多快取實現的選擇。
三、ThreadLocal的例項應用 - 基於Redis的分散式自旋鎖
接下來分享ThreadLocal的典型應用: 分散式自旋鎖。廢話不多說,直接上原始碼。
@Data
public class RedisLock {
private RedisTemplate<String, String> redisTemplate;
private int expireSeconds = 60; //鎖過期時間,預設1分鐘
private int retryIntervalMs = 1000; //重試加鎖間隔時間(毫秒),預設1秒
private int retryTimes = 3; //重試加鎖次數
private final Logger log = LoggerFactory.getLogger(getClass());
public RedisLock(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisLock(RedisTemplate<String, String> redisTemplate, int expireSeconds) {
this.redisTemplate = redisTemplate;
this.expireSeconds = expireSeconds;
}
public RedisLock(RedisTemplate<String, String> redisTemplate, int expireSeconds, int retryTimes, int retryIntervalMs) {
this.redisTemplate = redisTemplate;
this.expireSeconds = expireSeconds;
this.retryTimes = retryTimes;
this.retryIntervalMs = retryIntervalMs;
}
/**
* 加鎖
* @param key
* @return
*/
public boolean acquire(String key) {
int count = retryTimes;
do{
String lockValue = UUID.randomUUID().toString();
//如果setnx ok,則表示redis中沒有針對當前key的鎖
if(redisTemplate.opsForValue().setIfAbsent(key, lockValue, expireSeconds, TimeUnit.SECONDS)) {
ThreadLocalContext.put(key, lockValue);
if(log.isDebugEnabled()) {
log.debug("key [" + key + "] locked success!");
}
return true;
}
count--;
if(count >= 0) {
try {
Thread.sleep(retryIntervalMs);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}
} while(count >= 0);
return false;
}
/**
* 釋放鎖
* @param key
*/
public void release(String key) {
//只允許釋放當前執行緒自己加的鎖
//某些特定情況下,可能導致加鎖成功後的業務處理耗時過長或者邏輯卡死,這時候鎖已經失效,可能被其它執行緒對該碼再次加鎖成功,
//這時候便不能直接對其粗暴解鎖,還需要根據value值進行判斷當前鎖是否為自己加上去的,以防影響其它執行緒的處理過程。
Object lockValue = ThreadLocalContext.get(key);
try{
if(lockValue!=null && lockValue.toString().equals(redisTemplate.opsForValue().get(key))) {
redisTemplate.delete(key);
if(log.isDebugEnabled()) {
log.debug("key [" + key + "] released success!");
}
}
}finally {
ThreadLocalContext.remove(key);
}
}
}
public class ThreadLocalContext {
private static ThreadLocal<Map<String, Object>> CTX_HOLDER = new ThreadLocal();
public ThreadLocalContext() {
}
public static void put(Map<String, Object> map) {
Map<String, Object> ctxMap = (Map)CTX_HOLDER.get();
if (ctxMap == null) {
ctxMap = new HashMap();
CTX_HOLDER.set(ctxMap);
}
((Map)ctxMap).putAll(map);
}
public static void put(String key, Object value) {
Map<String, Object> ctxMap = (Map)CTX_HOLDER.get();
if (ctxMap == null) {
ctxMap = new HashMap();
CTX_HOLDER.set(ctxMap);
}
((Map)ctxMap).put(key, value);
}
public static Map<String, Object> get() {
return (Map)CTX_HOLDER.get();
}
public static boolean contains(String key) {
Map<String, Object> ctxMap = (Map)CTX_HOLDER.get();
return ctxMap != null ? ctxMap.containsKey(key) : false;
}
public static void clean() {
CTX_HOLDER.set((Object)null);
}
public static void remove(String key) {
((Map)CTX_HOLDER.get()).remove(key);
}
public static Object get(String key) {
Object value = null;
Map<String, Object> ctxMap = (Map)CTX_HOLDER.get();
if (ctxMap != null) {
value = ctxMap.get(key);
}
return value;
}
static {
CTX_HOLDER.set(new HashMap());
}
}