ThreadLocal源码分析

ThreadLocal,线程变量,是一个以ThreadLocal对象为键,任意对象为值 的存储 结构。该结构附着于线程之上。每个线程都保存一份原始变量的副本,每个线程对ThreadLocal变量的修改互不影响。可以通过接口get()返回当前线程中保存的ThreadLocal变量的值,通过接口set()设置当前线程中保存的ThreadLocal变量的值,只要当前线程一直存活,该ThreadLocal变量在当前线程中的副本就会一直存在。所以ThreadLocal和线程安全没有关系,并且适合在后台记录一个线程连续的行为。
ThreadLocal的几个主要接口

接口 说明
protected T initialValue() 返回当前线程中该ThreadLocal变量的初始值,当一个线程首次获取该变量时,会触发该方法。
public T get() 返回ThreadLocal变量在当前线程副本的值,如果当前线程的变量没有值,会调用initialValue进行初始化并返回该值
public void set(T value) 设置ThreadLocal变量在当前线程的副本的值
public void remove() 移除当前线程中该ThreadLocal变量的值

JavaDoc给的一个例子:

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的实现原理。
以ThreadLocal的get方法为例:

 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }
       private void set(ThreadLocal key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
//初始化当前线程的ThreadLocalMap
   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方法的步骤如下:

  • 获取当前调用的线程
  • 获取线程拥有的ThreadLocalMap变量
  • ThreadLocalMap内部有一个Entry数组,以ThreadLocal变量的hash值计算Entry数组的下标,得到对应Entry;否则需要初始化该线程的ThreadLocalMap。

set方法的步骤余get类似,只是做的是相反的操作。

二.ThreadLocal内部数据结构ThreadLocalMap

ThreadLocalMap是ThreadLocal的一个内部类,该结构也是一个专门用于维护线程局部变量的hash map,每个线程都有一个ThreadLocalMap。

ThreadLocal.ThreadLocalMap threadLocals = null;

每个ThreadLocalMap有一个Entry数组,默认大小为16。Entry数组的下标值是通过ThreadLocal变量的hashCode对数组长度取余的结果。如下示:

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

同时,该map是通过碰撞法解决hash冲突的问题,map内部元素数量超过阈值会进行rehash操作。
有一点需要注意,ThreadLocalMap的Entry数组里的元素是WeakReference。

private Entry[] table;
      static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

对于Entry为什么是WeakReference,可以通过WeakReference的作用来知晓。WeakReference作为Java中的四种Reference类型之一,WeakReference引用的实例的存在是不会影响其引用的对象的回收行为。被WeakReference引用的对象只能生存到下一次垃圾收集发生前。当垃圾收集发生时,如果该WeakReference引用的对象除了被该WeakReference引用外,没有别的在GC Roots引用链的引用实例引用该对象的情况下,无论内存是否足够,该对象一定会被回收掉。

Java的引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),这四种引用强度依次减弱。
1.强引用:代码中普遍存在的,类似Object obj = new Object()的这类引用,只要强引用还在,垃圾回收器永远不会回收掉被引用的对象。
2.软引用:用来描述一些还有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常,JDK提供了SoftReference来实现软引用。
3.弱引用:用来描述非必须对象,但它的强度比软引用更弱一些,被弱引用引用的对象只能生存到下一次垃圾收集发生之前。当垃圾收集发生时,无论内存是否足够,都会回收掉只被弱引用关联的对象。JDK提供了WeakReference
4.虚引用:它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个通知。JDK提供了一个PhantomReference类来实现虚引用。

根据这些结构,我们可以描绘出这些对象之间的一些引用关系:



根据前面提到的WeakReference的特性,我们可以考虑一种情况,如果一个ThreadLocal对象没有外部强引用的引用,则发生GC时,该ThreadLocal对象会被回收清除,此时ThreadLocalMap中就会出现key为null的Entry,我们就无法访问这些key为null的val,如果该线程一直不退出(线程池中的线程),于是就会一直存在一条强引用链:Thread Ref->Thread Object->ThreadLocalMap->Entry->val,从而导致该val的对象无法回收,发生内存泄漏。
而实际上,ThreadLocalMap使用ThreadLocal变量的WeakReference作为Entry的key是考虑到这种情况了的。在ThreadLocalMap的getEntry与set方法中对key引用对象为nul的Entry都进行了擦除处理。源码如下:

private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
        private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            while (e != null) {
                ThreadLocal k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
//
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // 清除staleSlot处的Entry
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        //Unlike Knuth 6.4 Algorithm R, we must scan until
                        //null because multiple entries could have been stale
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

在调用getEntry的过程中,如果key为null,就会进行Entry擦除操作,保证该Entry内的val不会存在强引用链,于是下次GC该对象就可以被回收。同样的set函数中也存在类似的擦除逻辑。但是这依赖于对ThreadLocalMap中Entry数组元素的获取与设置必须通过这两个函数的保证。所以很多时候我们需要主动调用ThreadLocal的remove函数,这会调用ThreadLocalMap的remove方法,主动擦除一个ThreadLocal变量对应的Entry元素。

 private void remove(ThreadLocal key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

三.ThreadLocal的使用场景

  • 在一次接口调用中传递log参数
private static ThreadLocal<Map<String, Object>> logParamMap = new ThreadLocal<Map<String, Object>>();
 
    public static Object getLogParamMap(String key) {
        if (logParamMap.get() == null)
            return null;
        return logParamMap.get().get(key);
    }
 
    public static void setLogParamMap(String key, Object obj) {
        if (logParamMap.get() == null) {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put(key, obj);
            logParamMap.set(map);
        } else {
            logParamMap.get().put(key, obj);
        }
    }
    public static void removeLogParamMap() {
        logParamMap.remove();
    }

再一次接口调用中,中间用到的各个方法都可以调用setLogParamMap方法添加日志参数,而不必到处传递String参数。
调用处:

            StatisticsLogs.setLogParamMap("var1", var);
            StatisticsLogs.setLogParamMap("var2", var2);
            StatisticsLogs.setLogParamMap("var3", var3);
            StatisticsLogs.setLogParamMap("var4", var4);
  • 减少实例创建
public class SafeDateFormatter { 
    private static ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
        @Override
        public SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
     
    public static SimpleDateFormat getDateFormat(){
        return dateFormat.get();
    }
}

SimpleDateFormat不是线程安全的,所以很多时候我们使用的时候没办法共享该变量,而是在每次使用时都会创建。通过这种方法获取SimpleDateFormat变量,每个线程拥有一个SimpleDateFormat实例。。这在站点后台大量使用线程池的情况下十分划算,只要线程不退出,就会一直拥有该实例。而不需要每次使用时重新new一个。

总结

通过源码分析,我们可以清楚知道,ThreadLocal并不是像很多人误解的那样,能够解决共享变量的并发访问问题。实际上,ThreadLocal变量使每个线程都保存一份该变量的副本,各线程对副本的操作互不影响(这也不符合我们对多线程访问下的共享变量的期待-即正确的同步机制能够保证共享变量的修改对各个线程的可见性)。因此,ThreadLocal最适合每个线程都需要持有某个实例,且该实例使用很频繁的场景。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容