ThreadLocal学习
官方介绍:
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
总结: 在多线程并发的场景下使用 可以通过ThreadLocal在同一线程,不同组件中传递公共变量 每个线程的变量都是独立的,不会互相影响(线程隔离)
常用方法
方法声明
描述
ThreadLocal()
创建ThreadLocal对象
public void set( T value)
设置当前线程绑定的局部变量
public T get()
获取当前线程绑定的局部变量
public void remove()
移除当前线程绑定的局部变量
可能的运行结果:
这是由于多个线程(0~4)公用一个变量(对象demo)导致的异常,因为线程之间数据没有隔离。
以上有两种解决方法,一种方法是利用synchronized同步方式,另一种就是ThreadLocal类
运行结果:
从结果来看,确实可以解决问题,但是,在这里们强调的是线程数据隔离的问题,并不是多线程共享数据的问题, 在这个案例中使用synchronized关键字是不合适的。
由上可知,创建了一个存储String类型的ThreadLocal对象(tl),在setContent方法中使用ThreadLocal中的set()方法将content存进tl,getContent方法中使用ThreadLocal中的get()方法从tl中获取content。
synchronized
ThreadLocal
原理
同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问
ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点
多个线程之间访问资源的同步
多线程中让每个线程之间的数据相互隔离
在案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。
jdk8.0之前的结构:

每个<code>ThreadLocal</code>都创建一个<code>Map</code>,然后用线程作为<code>Map</code>的<code>key</code>,要存储的局部变量作为<code>Map</code>的<code>value</code>,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的<code>ThreadLocal</code> 确实是这样设计的,但现在早已不是了。
jdk8.0之后的结构:
每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。
具体的过程是这样的:
每个Thread线程内部都有一个Map (ThreadLocalMap)
Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
这样设计之后每个Map存储的Entry数量就会变少(降低哈希碰撞的概率)。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
set方法
上述代码的执行流程:
首先获取当前线程,并根据当前线程获取其Map 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key) 如果Map为空,则给该线程创建 Map,并设置初始值
get方法
首先获取当前线程, 根据当前线程获取其Map 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry ,否则转到D 如果e不为null,则返回e.value,否则转到D Map为空或者e为空,则通过initialValue方法获取值为初始值的value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
remove方法
上述代码的执行流程
首先获取当前线程,并根据当前线程获取一个Map 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
initialValue方法
此方法的作用是 返回该线程局部变量的初始值。 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。 这个方法缺省实现直接返回一个null。 如果想要一个除null之外的初始值,可以重写此方法。 (备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。 另外,Entry继承了WeakReference类,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Memory leak: 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
Java中的引用有4种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
假设在业务代码中使用完ThreadLocal ,ThreadLocalRef被回收了。 但是因为threadLocalMap的Entry强引用了ThreadLocal,造成ThreadLocal无法被回收。 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 CurrentThreadRef--->CurrentThread->threadLocalMap--->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。 也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
同样假设在业务代码中使用完ThreadLocal ,ThreadLocalRef被回收了。由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向ThreadLocal实例, 所以ThreadLocal就可以顺利被gc,此时Entry中的key=null。 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 CurrentThreadRef--->CurrentThread->threadLocalMap--->entry ---> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
在以上两种内存泄漏的情况中,都有两个前提:
没有手动删除这个Entry ThreadLocalRef结束后,CurrentThread依然运行
第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
第二点稍微复杂一点, 由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal之后,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。
综上,ThreadLocal内存泄漏的根源是:
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
事实上,在ThreadLocalMap中的set和getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
使用完ThreadLocal,调用其remove方法删除对应的Entry 使用完ThreadLocal,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
源码:
重点: <code>int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)</code>。
关于<code>firstKey.threadLocalHashCode</code>:
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。
关于<code>& (INITIAL_CAPACITY - 1)</code>
计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述,可以把Entry[] table看成一个环形数组。