ThreadLocal详解

文章结构如下:


ThreadLocal思维导图

简介

ThreadLocal是为了解决线程安全而产生的。它解决线程安全的思路不同于synchronized:使多个线程对于共享资源的访问串行化,只有一个线程能够获取到对象锁,其他线程进入同步队列等待。也不同于volatile,通过lock指令生成内存屏障来使得其他线程访问变量时需要从主内存加载最新值,在线程写入值时能够立刻刷新到主内存,但是volatile不能保证原子性,因此使用时具有一定局限。ThreadLocal的解决思路是线程封闭,那么无论线程什么时候,在哪个方法里面访问ThreadLocal变量,都只会访问到自己线程的ThreadLocal值。避免了将参数通过方法进行传递,也无需担心其他线程会访问到本线程的变量值。

源码理解

我们经常使用的api是ThreadLocal的get(),set(),remove()方法,通过get方法切入,可以发现对于每个Java线程,都维护了一个ThreadLocalMap。

    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();
    }

ThreadLocalMap

想要读懂源码,就绕不开对于ThreadLocalMap的理解,ThreadLocalMap本质结构跟HashMap差不多,只不过Entry的组成不同,解决冲突的方式不同。

  1. 数据结构
    通过Thread获取到ThreadLocalMap,然后每个ThreadLocalMap的Entry存储着ThreadLocal对象与value的对象关系,当设置了多个ThreadLocal变量时,对于每一个ThreadLocal对象,会在每一个ThreadLocalMap中保存一份。


    image.png
       static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  1. 为什么Entry中的ThreadLocal对象是弱引用?
    在Java中,定义了四种引用:
  • 强引用:Object obj = new Object(),对于这种引用,除非显示将obj=null,否则虚拟机不会将其回收
  • 软引用:用来描述一些还有用,但非必须的对象。在系统将要内存溢出之前,会把软引用对象列入回收范围进行第二次回收。
  • 弱引用:引用强度比软引用更弱,只能生存到下一次虚拟机垃圾回收之前。
  • 虚引用


    image.png

    我们在代码中实例化的对象引用是保存在虚拟机栈上,和Entry的key引用同一个对象,之所以要将Entry的key设置为弱引用的原因就是如果我们将外部引用设置为null,那么ThreadLocal的实例不再有强引用,只有弱引用,在下次虚拟机进行垃圾回收时就可以将其回收了。但是依然还存在内存泄漏问题,因为Entry不会被回收。

  1. ThreadLocalMap解决冲突的方式
    解决Hash冲突的方式主要有拉链法、开放地址法、二次hash法、建立公共溢出区。ThreadLocalMap解决冲突的方式是开放地址法,如果通过Hash函数算出下标已经存储过Entry了,它会线性环形搜索没有被使用的位置。我理解使用线性检测的原因是只要线程中的ThreadLocal对象不多,那么根据扩容因子算出的ThreadLocalMap的数组大小也不会很大,所以即使退化到最差的o(n),对性能的影响也不大,而且这种线性搜索相比链式方式而言更加节省空间。
    对于线性探测的结点增删、扩容可以参考:线性探测解决Hash冲突

内存泄漏

上面说到ThreadLocal设置为弱引用是为了防止内存泄漏,所谓内存泄漏就是指堆中已经不再使用的对象没有被回收,造成空间 的浪费,而且积累下去很可能会造成内存溢出。当Entry中的key被回收时,整个Entry就没有用了,但是由于value还持有虚拟机栈上的强引用,所以不会被回收,这样就还是会造成内存泄漏。但是ThreadLocal中的get()、set()、remove()方法都会调用replaceStaleEntry、cleanSomeSlots、expungeStaleEntry方法进行回收

  1. 清理方法:cleanSomeSlots


    搜索清除

下标i用来控制访问的范围,如果没有找到key为null的Entry,那么会遍历log2(n),i的下标环形递增。如果找到一个key不为null的位置,n会置为len,相当于是增大了范围

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
  1. expungeStaleEntry的清除逻辑
    cleanSomeSlots函数,在key为null的结点进入expungeStaleEntry方法,将当前槽位的value和Entry都设为null,并且还继续往下环形搜索,一直到table[i]为null才退出,搜索过程中,遇到key为null的结点就进行清除,如果key不为null,就对结点进行rehash,rehash的目的就是为了让结点离hash函数的下标更近,这样查找的时候就不会在线性搜索浪费时间了。remove和get时,都会调用该方法进行清理。
 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            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;
        }
  1. replaceStaleEntry方法
    这个方法在set()过程,当key为null时调用。从i开始首先前环向搜索脏 entry,一直到table[i]=null结束。然后从下标i开始向后搜索,如果有key相同的就覆盖,并和脏entry交换。根据不同情况,设置cleanSomeSlots清除节点的范围
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
//向前找到第一个key为null的entry
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
////如果在向后环形查找过程中发现key相同的entry就覆盖并且和脏entry进行交换
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

         //如果在查找过程中还未发现脏entry,那么就以当前位置作为cleanSomeSlots
            //的起点
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
       //如果向前未搜索到脏entry,则在查找过程遇到脏entry的话,后面就以此时这个位置
        //作为起点执行cleanSomeSlots
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
//如果在查找过程中没有找到可以覆盖的entry,则将新的entry插入在脏entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

对于下面这个例子,插入的位置是4,从下标4向前搜索到3就停止,更新slotExpunge为3.再从4向后遍历寻找可覆盖的entry,当前例子未找到,于是以slotExpunge为下标调用cleanExpunge清理脏entry。


image.png

最佳实践

使用场景

  1. 数据库连接
    Hibernate的数据库连接池就是将connection放进threadlocal实现的
  2. 用户Session等信息
  3. 对请求的requestBody,requestUrl等进行处理
    代码中ThreadLocal修饰了requestBody变量,因为服务器使用的是Tomcat,所以一个请求会交给一个线程来处理,那么requestBody的get()和set方法设置的就是当前线程的请求体的值,跟其他线程互不影响。
public class ReqLogInterceptor implements HandlerInterceptor {
ThreadLocal<String> requestBody = new ThreadLocal<String>();
  @Override
  public boolean preHandle(HttpServletRequest httpServletRequest,
      HttpServletResponse httpServletResponse, Object o) throws Exception {
    requestBody.set("");
    return true;
  }
  @Override
  public void postHandle(HttpServletRequest httpServletRequest,
      HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView)
      throws Exception {
    if (httpServletResponse instanceof ContentCachingResponseWrapper) {
      responseBody.set("");
      byte[] body = ((ContentCachingResponseWrapper) httpServletResponse).getContentAsByteArray();
      responseBody.set(new String(body, httpServletResponse.getCharacterEncoding()));
    }
  }

  }

及时remove

及时调用ThreadLocal的remove方法,可以避免内存泄漏问题,更重要的是防止造成业务逻辑的错乱,因为通常会使用线程池管理线程,如果一个用户登录之后的name相关的ThreadLocal对象,没有及时remove,那么其他用户登录进来之后,会发现自己的用户名显示错误。

父子线程通信

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

推荐阅读更多精彩内容

  • 介绍 顾名思义这个类提供线程局部变量每个线程(通过其get或set方法)都有自己独立初始化的变量副本 Thread...
    Ray昱成阅读 230评论 0 0
  • 1 ThreadLocal简介 多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的...
    爱健身的兔子阅读 396评论 0 0
  • ThreadLocal是什么? ThreadLocal是一个关于创建线程局部变量的类。 通常情况下,我们创建的变量...
    chenjieping1995阅读 2,204评论 1 3
  • 来自:https://www.cnblogs.com/fsmly/p/11020641.html 一、Thread...
    dinel阅读 362评论 0 0
  • 1. 概念 ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变...
    zly394阅读 1,577评论 0 1