1. 程式人生 > >Java-多執行緒-ThreadLocal

Java-多執行緒-ThreadLocal

Java-多執行緒-ThreadLocal

0x01 摘要

本文簡單分析下ThreadLocal實現原理,再附上小例子。

0x02 ThreadLocal是什麼

ThreadLocal提供執行緒級別的私有區域性變數。這些變數和普通變數不同之處在於,通過get或set方法訪問這類變數的每個執行緒都擁有一份獨立初始化的變數副本

ThreadLocal通常用private static修飾,可以將狀態與該執行緒建立一對一的關係。

下面這個小例子,當第一次呼叫ThreadId.get()時會為每個執行緒生成一個全域性唯一的、以1為步長自增的識別符號,往後都會保持不變。

import
java.util.concurrent.atomic.AtomicInteger; public class ThreadId { // Atomic integer containing the next thread ID to be assigned private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID private static final ThreadLocal<
Integer>
threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId.getAndIncrement(); } }; // Returns the current thread's unique ID, assigning it if necessary public static int get() { return
threadId.get(); } }

在上面的例子中,只要執行緒存活且該ThreadLocal例項可訪問,那麼每個執行緒都會擁有一個隱式引用,指向自己擁有的ThreadLocal變數副本值。

0x03 原理

ThreadLocal原理

3.1 ThreadLocal的屬性

3.1.1 HashCode

private final int threadLocalHashCode = nextHashCode();

// 兩個連續的hashcode差值
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
}

private static AtomicInteger nextHashCode = new AtomicInteger();

ThreadLocals依賴於附屬於每個執行緒的線性探測雜湊對映(Thread.threadLocals和inheritableThreadLocals,都是ThreadLocalMap型別)
ThreadLocal物件充當鍵,通過threadLocalHashCode搜尋。 這是一個自定義雜湊程式碼(僅在ThreadLocalMaps中有用),它消除了在相同執行緒使用連續構造的ThreadLocals的常見情況下的衝突,同時在不太常見的情況下保持良好行為。

3.2 ThreadLocal構造方法

 public ThreadLocal() {}

這裡什麼都沒做。

3.3 成員方法

3.3.1 initialValue

protected T initialValue() {
        return null;
}

這個方法會在第一次使用get方法呼叫時執行,除非在此之前呼叫了set方法就不會呼叫initialValue。一般來說該方法只會被呼叫一次,但如果使用remove清空隨後又呼叫get方法時,又會再次呼叫initialValue

如果我們想設一個初值,一般就是用匿名內部類的方式重寫該方法實現自定義初值,比如:

ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };

3.3.2 get

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
}

get方法很重要,他被用來獲取當前thread私有的threadlocal的變數副本。 如果變數沒有當前執行緒的值,則首先將其初始化為呼叫initialValue方法的返回值。

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}

這個是剛剛get方法裡使用的,可以看到他會先呼叫initialValue方法去拿到初始值,然後獲取當前執行緒的ThreadLocalMap。如果map不存在就以當前threadvalue建立ThreadLocalMap;如果已經存在就以當前ThreadLocal例項為key,value為值放入該ThreadLocalMap。

3.3.3 set

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

跟前面提到的setInitialValue方法差不多,只不過這裡指定了value。

3.3.4 remove

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
}

獲取當前執行緒的ThreadLocalMap,然後從其中移除當前ThreadLocal例項為key的Entry

3.4 內部類

3.4.1 SuppliedThreadLocal

 static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
}

這個類的主要作用是配合withInitial設定初始值,示例如下:

 private static ThreadLocal<Integer> ti1 = ThreadLocal.withInitial(new Supplier<Integer>() {
        @Override
        public Integer get()
        {
            return 33;
        }
    });
    private static ThreadLocal<Integer> ti2 = ThreadLocal.withInitial(() -> 44);

3.4.2 ThreadLocalMap

ThreadLocalMap是一個自定義的hasmap,他只適合維護threadlocal values,沒有向外暴露任何方法。

為了應對長期和高負荷的使用,所以採用了WeakReference來修飾該map的key。也就是說當這些key無其他強引用時,GC會將他們回收。注意,

0x05 坑

5.1 記憶體洩露

因為每個執行緒都有一個map,指向使用的ThreadLocal物件,而且這是一個強引用。也就是說,當一個ThreadLocal強引用持續存在時,使用了該ThreadLocal的執行緒的ThreadLocalMap裡的Entry之key雖是弱引用指向該ThreadLocal物件,但是因為還有強引用存在的關係就一直不會被回收,該entry也會隨著執行緒持續存在而存在,造成記憶體洩露。所以我們應該在每個執行緒使用完ThreadLocal物件後呼叫remove方法,手動移除該entry。

5.2 執行緒池

Thread物件就那麼幾個,都是複用的。也就是說,他們的ThreadLocalMap是不會變的,就會導致其他Runnable的ThreadLocal值交叉混用,出現問題。

0x06 例子

6.1 DateFormat

SimpleDateFormat並不是執行緒安全的,所以在阿里的Java開發規範裡推薦了用ThreadLocal保證執行緒安全的做法:

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    } };

  String startTimeStr = "2018-01-11 02:17:02.806";
        try {
            Date startTime = df.get().parse(startTimeStr);
            System.out.println("startTime=" + startTime);
            String dfStr = df.get().format(new Date());
            System.out.println("dfStr=" + dfStr);
		}catch (ParseException e) {
            e.printStackTrace();
        }

0xFF 參考文件

JavaDoc-java.lang.ThreadLocal

徹底理解ThreadLocal