ThreadLocal 和神奇的数字 0x61c88647

这篇文章会详细阐述ThreadLocal的内部结构及其原理,以及神奇的0x61c88647

在Java 1.4之前,ThreadLocals会产生线程间的竞争,无法写出高性能的代码. Java 1.5改变了它的实现,下面详细阐述ThreadLocal 的内部结构和原理, 并分析为了解决散列表的冲突而引入的神奇的hash code: 0x61c88647

1. ThreadLocal 应用场景

先举个在平时工作中经常用到的场景, 一个web应用供登录用户通过浏览器访问,后台应用会获取用户的登录信息(如用户名),并对每个用户的访问做记录. 这是一个并发场景,每次请求都分配一个线程去处理这个请求,web容器一般都会有一个线程池,每次请求都会分配其中的一个空闲线程去处理用户的这次请求, 处理完毕后,线程归还线程池等待后续访问的线程分配.

当然,用户登录信息可以从当前请求request中获取,但是后台应用的多个地方可能都会需要用户登录信息, 一个解决办法是向这些所有用到的地方传递request参数,显然是麻烦的。另外一个办法就是利用ThreadLocal, 获取登录信息后把它放到当前线程中的ThradLocal变量中,任何需要的时候从当前线程中取就可以了,是不是很方便呢?

因此ThreadLocal的应用场景应该是实现在不同的线程存储不同的上下文信息的场合中,这样的场合最多的可能就是webapp中,引用stackoverflow中的一个回答:

ThreadLocal is most commonly used to implement per-request global variables in app servers or servlet containers where each user/client request is handled by a separate thread.

2. ThreadLocal 原理

Java docs api说:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread.

因此,ThreadLocal只是提供一个thread-local变量,这个变量于当前线程所独有, 每一个线程都有一个隶属与当前线程的thread-local变量

下面是ThreadLocal对外提供的四个方法:

protected T initialValue() 设置并返回当前线程变量的一个初始值

set(T value) 将信息value放到当前线程的thread-local变量中

T get() 是获取set(T value)设置的值,如果没有则返回初始值

remove() 移除线程中的这个thread-local变量

thread-local变量是怎么与当前线程Thread关联的呢? 看一下Thread源码,它有一个实例属性:

/** * ThreadLocal values pertaining to this thread. * This map is maintained by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals =null;是的,就是ThreadLocal.ThreadLocalMap对象(Thread和ThreadLocal类属于相同的包java.lang). 看来它是用ThreadLocalMap实现的,此时能看出ThreadLocalMap是ThreadLocal类中的一个静态内部类, 也可以看出上面说的thread-local变量其实就是这个threadLocals对象, 下面就看下这个ThreadLocalMap到底长什么样staticclassThreadLocalMap {staticclassEntryextendsWeakReference {/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal k,Objectv) {super(k);            value = v;        }    }/**    * The initial capacity -- MUST be a power of two.    */privatestaticfinal int INITIAL_CAPACITY =16;/**    * The table, resized as necessary.    * table.length MUST always be a power of two.    */privateEntry[] table;/** * Get the entry associated with key. */privateEntry getEntry(ThreadLocal key) {...}/** * Set the value associated with key. */privatevoidset(ThreadLocal key,Objectvalue) {...}// æ��é� å�1⁄2æ�°å��å�¶ä»�ä ̧�äo�å�1⁄2æ�°ç��ç�¥}

可以看出ThreadLocalMap确实是一个map, 通过它的属性Entry[] table实现,而Entry的key是ThreadLocal对象,value是要设置的值,

注意两点:

具体的ThreadLocalMap实例并不是ThreadLocal保持,而是每个Thread持有,且不同的Thread持有不同的ThreadLocalMap实例, 因此它们是不存在线程竞争的(不是一个全局的map), 另一个好处是每次线程死亡,所有map中引用到的对象都会随着这个Thread的死亡而被垃圾收集器一起收集

Entry的key是对ThreadLocal的弱引用,当抛弃掉ThreadLocal对象时,垃圾收集器会忽略这个key的引用而清理掉ThreadLocal对象, 防止了内存泄漏

当向thread-local变量中设置value时(set(T value)),获取当前Thread中的ThreadLocalMap,如果此时是null,则用ThreadLocal实例和value构建一个map设置到当前线程的属性threadLocals中, 否则通过ThreadLocal对象作为key直接将ThreadLocal实例和value放到当前Thread已存在的map中(可能产生冲突,后面介绍)

当从ThreadLocal变量中获取value时(get()), 获取当前Thread中的ThreadLocalMap, 如果为null则通过initialValue()构建初始值同时利用这个初始值构建一个map到当前Thread中,最后返回这个初始值,否则从map中获取对应的Entry并返回value

通过原理分析可以看出,在使用ThreadLocal是应该将它声明为public static, 即所有线程共用一个ThreadLocal实例,而不是每一个线程来临时都要新创建一个ThreadLocal对象, Java Doc也建议,ThreadLocal应当声明为public static.

3. 碰撞解决与神奇的 0x61c88647

既然ThreadLocal用map就避免不了冲突的产生

3.1 碰撞避免和解决

这里碰撞其实有两种类型

只有一个ThreadLocal实例的时候(上面推荐的做法),当向thread-local变量中设置多个值的时产生的碰撞,碰撞解决是通过开放定址法, 且是线性探测(linear-probe)

多个ThreadLocal实例的时候,最极端的是每个线程都new一个ThreadLocal实例,此时利用特殊的哈希码0x61c88647大大降低碰撞的几率, 同时利用开放定址法处理碰撞

3.2 神奇的 0x61c88647

注意 0x61c88647 的利用主要是为了多个ThreadLocal实例的情况下用的

从ThreadLocal源码中找出这个哈希码所在的地方

/** * ThreadLocals rely on per-thread linear-probe hash maps attached * to each thread (Thread.threadLocals and inheritableThreadLocals). * The ThreadLocal objects act as keys, searched via threadLocalHashCode. * This is a custom hash code (useful only within ThreadLocalMaps) that * eliminates collisions in the common case where consecutively * constructed ThreadLocals are used by the same threads, * while remaining well-behaved in less common cases. */privatefinalintthreadLocalHashCode = nextHashCode();/** * The next hash code to be given out. Updated atomically. * Starts at zero. */privatestaticAtomicInteger nextHashCode =newAtomicInteger();/** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */privatestaticfinalintHASH_INCREMENT =0x61c88647;/** * Returns the next hash code. */privatestaticintnextHashCode(){returnnextHashCode.getAndAdd(HASH_INCREMENT);}

注意实例变量threadLocalHashCode, 每当创建ThreadLocal实例时这个值都会累加 0x61c88647, 目的在上面的注释中已经写的很清楚了:为了让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table

下面来看一下ThreadLocal怎么使用的这个 threadLocalHashCode 哈希码的,下面是ThreadLocalMap静态内部类中的set方法的部分代码:

// Set the value associated with key.privatevoidset(ThreadLocal key, Objectvalue){    Entry[] tab = table;intlen = tab.length;inti = key.threadLocalHashCode & (len-1);for(Entry e = tab[i]; e !=null;      e = tab[i = nextIndex(i, len)]) {...}    ...key.threadLocalHashCode & (len-1)这么用是什么意思? 先看一下table数组的长度吧:/** * The table, resized as necessary. * table.length MUST always be a power of two. */privateEntry[] table;

哇,ThreadLocalMap 中 Entry[] table 的大小必须是2的N次方呀(len = 2^N),那 len-1 的二进制表示就是低位连续的N个1, 那 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位, 这样就能均匀的产生均匀的分布? 我用python做个实验吧

>>> HASH_INCREMENT =0x61c88647>>> defmagic_hash(n):... foriinrange(n):... nextHashCode = i * HASH_INCREMENT + HASH_INCREMENT... printnextHashCode & (n -1),... print... >>> magic_hash(16)7145123101815613411290>>> magic_hash(32)714212831017243161320272916233051219261815222941118250

产生的哈希码分布真的是很均匀,而且没有任何冲突啊, 太神奇了,

This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.

以及

We established thus that the HASH_INCREMENT has something to do with fibonacci hashing, using the golden ratio. If we look carefully at the way that hashing is done in the ThreadLocalMap, we see why this is necessary. The standard java.util.HashMap uses linked lists to resolve clashes. The ThreadLocalMapsimply looks for the next available space and inserts the element there. It finds the first space by bit masking, thus only the lower few bits are significant. If the first space is full, it simply puts the element in the next available space. The HASH_INCREMENT spaces the keys out in the sparce hash table, so that the possibility of finding a value next to ours is reduced.

这与fibonacci hashing(斐波那契散列法)以及黄金分割有关,具体可研究中的 6.4 节Hashing部分

4. 线程池时使用 ThreadLocal

web容器(如tomcat)一般都是使用线程池处理用户到请求, 此时用ThreadLocal要特别注意内存泄漏的问题, 一个请求结束了,处理它的线程也结束,但此时这个线程并没有死掉,它只是归还到了线程池中,这时候应该清理掉属于它的ThreadLocal信息,

remove()

线程结束时应当调用ThreadLocal的这个方法清理掉thread-local变量

最后


本号专注Java源码分析。喜欢底层源码的朋友可以来交流探讨。交流群:818491202 验证:88

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

推荐阅读更多精彩内容