Java 源码分析-ThreadLocal

  今晚重新看了一下Android 消息传递的机制,其中发现了每一个Looper的对象放在了一个ThreadLocal对象里面,当时就非常的好奇,ThreadLocal这个类是怎么实现的。于是结合了Java 8的源代码和网上的资料对这个类有了一定的理解,在此记录一下,如果有错误之处,请各位指正!
  本文参考资料:
  1.深入分析 ThreadLocal 内存泄漏问题
  2.【Java 并发】详解 ThreadLocal
  3.周志明老师的《深入理解Java虚拟机》
  其实,大家可以看看上面两篇文章,上面的两篇文章比我写的好。我这里写的都是我自己对ThreadLocal的理解!哈哈!

1.初识ThreadLocal

  这里就不对ThreadLocal类的基本使用进行展开了,我们直接从源码入手,来理解ThreadLocal实现的原理。

(1).set方法

  当我们在使用ThreadLocal类时,通常会使用set方法来给当前的线程设置一个变量。我们来看看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);
    }

  set方法的代码非常短,至少比我之前看的其他代码短得多。
  首先我们是获取的是当前的线程,然后调用getMap方法来获得TreadLocalMap对象,再来看看getMap方法获取的是什么:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

  哎呀,这个也太简单了吧。这里简单的介绍一下threadLocals表示的含义,threadLocals其实就是Thread类的一个成员变量,也就是说每个Thread对象里面都有一个这个变量,这样就能保证这个变量在线程之间是保持独立,线程之间不会相互的影响。
  回到我们的set方法里面来,然后调用map的set方法来进行赋值。请注意这里,第一个参数传入的是this,也就是说是当前的ThreadLocal,而这个参数是干嘛的呢?就是key。也就是说,这里是ThreadLocal对象来作为key的。
  map的set方法又在干嘛呢?这里我不解释,待会在讲解ThreadLocalMap类时在详细解释set方法的作用。

(2).get方法

  在使用ThreadLocal类时,get方法也是不可避免的,通常我们调用get方法来获取在ThreadLocal里面保存的变量。

    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方法也是非常的简单,最终还是到了ThreadLocalMap里面去了。看来不得不去ThreadLocalMap是一个什么东西了。

2.ThreadLocalMap

(1).成员变量

  在理解ThreadLocalMap之前,我们还是来看看这类的里面有那些成员变量:

        //默认的容量
        private static final int INITIAL_CAPACITY = 16;
        //Entry数组,用来保存每个键值对的
        private Entry[] table;
        //map的size
        private int size = 0;
        //阈值,用来扩容的
        private int threshold; // Default to 0

  这个成员变量也是非常的少,之前在看HashMap源代码的时候,那家伙!!!

(2).Entry

  在理解代码之前,我们必须还说Entry这个东西:

        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类,表示是一个弱引用。这里弱引用是什么东西呢?其实这个就能扯到JVM的GC那一块了,由于不是本文的重点,所以就不讲解(其实是自己太菜了!!!!)。这里就简单的介绍一下Java中的四种引用,摘抄自周志明老师的《深入理解Java虚拟机》。

1.强引用:在程序代码中普遍存在的,类似 Object obj = new Object()。这类的引用便是强应用。只要强引用存在,GC是永远不会回收该引用的对象。

2.软引用:用来描述一些有用但是并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收对象范围之中,进行第二次的回收。如果这次回收还没有足够的内存的话,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

3.弱引用:也是用来描述非必须的对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前。当GC工作时,无论当前内存是否足够,都会回收掉纸杯弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

4.虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被GC回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

  回到Entry里面来,我们需要注意的是key被弱引用关联了。这就是因为这一点,出现了ThreadLocal的内存泄露问题。详细请看:深入分析 ThreadLocal 内存泄漏问题

(3).set方法

  准备的差不多了,我们开始研究ThreadLocalMap的set方法了。这里先贴出全部的代码,然后逐一的分析。

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

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

  这里先简单的概括一下,这段代码主要实现了的是什么。set方法的目的,我们都知道,就是将一个键值对成功保存在Entry数组里面,而保存的index不是指定,而是通过key的hashCode来计算的。将一个key的hashCode通过一个hash函数得到一个index值,而这个值就是这个键值对在数组里面的位置。
  但是理想是美好的,现实是残酷的,当两个不同的key而产生相同的index时,就出现了hash碰撞。这种情况下,通常来说有两种解决的办法:1.开放地址法;2.链地址法。由于这两种方法在数据结构中是比较重要的内容,大家应该都学过,所以这里就不解释这两种方法的区别。
  在ThreadLocalMap里面使用的是开放地址法,也就是说当出现了哈希碰撞时,会从当前的位置往后找一个为null的位置来保存键值对。
  接下来详细的分析一下set方法的代码,首先是:

int i = key.threadLocalHashCode & (len-1);

  可能有些老哥在看到这段代码时,就有点懵逼了,这特么是什么鬼东西?如果我说这个其实就是取模运算,老哥们会不会what fuck?
  我们详细的解释一下这段代码。当len为2的n次方时,key.threadLocalHashCode & (len-1)就相当于是一个取模运算;例如,当len为16时,上面的代码相当于就是对16取模。这个是为什么呢?因为16的二进制是:10000,当减1变成15时,二进制变为:1111。这样len - 1与任何一个数字进行与运算时,最终剩下来的都是那个数字的低4位,而低4位就是对16取模的结果。如果还不懂的话,看图:



  如图,相当于是20%16的结果。但是,需要注意的是,len必须是2的n次方,这样才能保证-1之后,低位全为1。这个也是Map为什么是2倍扩容的原因之一。
  通过上一步的hash操作,算是找到一个index来存储我们键值对,但是必须考虑到hash碰撞的情况。其中当hash碰撞了之后,是通过这个方法来获取下一个index位置:

        i = nextIndex(i, len)
        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

  当获取的inedx对应的数组中为null时,表示这个位置没有被占据。如果当前key已经在Entry数组中里面存在,直接替换值即可:

                if (k == key) {
                    e.value = value;
                    return;
                }

  当时k为null这种情况怎么里面?这个就得引出之前需要注意的Entry中key为WeakReference关联,也就是说,在Entry数组里面,每个Entry对象的key可能随时都会被GC回收,从而导致k为null,由于Entry是一个对象,虽然Entry里面的key被回收了,但是key对应value并没有回收,从而导致这个value不可能再被get到。由于value是被一个强引用关联,除非value所在的Entry对象被回收,value才会被回收,由于这个Entry在数组中有一个强引用,所以除非收到将数组的相应位置置为nulll,否则这个强引用会一直存在。
  由于以上原因,导致Entry对象不能回收,从而导致value内存泄露。
  卧了个槽,怎么去分析内存了,跑题了跑题了。回到主题,根据上面的解释,我们知道了k为null是因为相应k被释放导致的,此时为了防止内存泄露,会去清理垃圾对象。如下:

                if (k == null) {
                    //将旧的对象替换程新的Entry对象,并且清理垃圾对象
                    replaceStaleEntry(key, value, i);
                    return;
                }

  当然如果找到了空位置的话,就占了。

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();

  添加数据之后,必不可少的就是判断是否达到了扩容的条件。
  这个就是整个set的过程。这里,简单的总结一下。

  1.在set方法里面,将key的hashCode对len进行取模运算来获取index。这里需要注意的是len必须是2的n次方。
  2.如果发生了哈希碰撞了,set方法采用的是开发地址方法来解决的。
  3.在进行开放地址方法时,有可能会出现key被回收的情况,这里会可能会导致内存泄露的问题。官方的手段是,对key为null的Entry进行清理。

(3).getEntry方法

  看完了set方法,我们再来看看get方法。在ThreadLocal的get方法里面,是通过调用getEntry来获取一个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);
        }

  从这段代码里面,我们可以看出来,如果index能获取得到的Entry的key与想要找到的key是一样的话,那么直接就返回,否则的话,就通过开放地址法来循环遍历寻找。

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

  同时我们发现, 在getEntryAfterMiss方法里面是通过循环遍历找一个Entry。

3.总结

  这个ThreadLocal感觉也不是很难,可能是自己太菜了吧,很多的问题都没有发现。在这里对ThreadLocal做一个总结。

   1. ThreadLocal实现线程的局部变量是通过Thread的一个ThreadLocalMap成员变量,因为每个线程对象都持有自己的ThreadLocalMap对象,所以线程之间不会有影响的。
   2.ThreadLocalMap使用的存储结构与普通的Map结构非常相似,只是ThreadLocalMap使用的开放地址法来解决哈希碰撞的。
   3.ThreadLocalMap的key使用的是WeakReference对象关联,所以会出现key为null,但是value不能被释放的情况。官方在每次的set和get方法里面,会对Entry进行清理。

  关于内存泄露的问题,大家可以看看这篇文章:深入分析 ThreadLocal 内存泄漏问题

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