封神总结!京东大佬整理的这篇ThreadLocal手册,成功助我涨薪 5K

184 阅读7分钟

Hello,今天给各位童鞋们分享ThreadLocal,赶紧拿出小本子记下来吧!

image.png

ThreadLocal 解析

一 基本了解

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

1.1 作用

  • 线程并发: 在多线程并发的场景下
  • 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  • 线程隔离: 每个线程的变量都是独立的,不会互相影响

1.2 基本方法

image.png

  • public void set( T value) 设置当前线程绑定的局部变量
  • public T get() 获取当前线程绑定的局部变量
  • public void remove() 移除当前线程绑定的局部变量
  • protected T initialValue() 初始化值

1.3 入门案例

需求:模拟多个线程设置值并取出该线程设置的值

synchronized版

image.png

image.png

ThreadLocal版

image.png

image.png 比较

以上两种方法看似都可以解决问题,但是我们来考虑一下效率问题?

synchronized:同步机制采用以时间换空间的方式, 只提供了一份变量,让不同的线程排队访问,侧重于多个线程之间访问资源的同步

ThreadLocal:ThreadLocal采用以空间换时间的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰,侧重于多线程中让每个线程之间的数据相互隔离

当然每个方法的适用场景不一样

二 原理探究

2.1 内部结构

JDK1.8之前:每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。

JDK1.8之后:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。

image.png

2.2 ThreadLocalMap源码分析

image.png

  • 重要参数

image.png 存储结构 - Entry

//规定死是弱引用 WeakReference

static class Entry extends WeakReference<ThreadLocal<?>> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal<?> k, Object v) {

    super(k);

    value = v;

}

}

  • hash冲突的解决

Hash冲突基本知识

哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值。这时候就产生了哈希冲突。

  • 开放地址方法

​ (1)线性探测

按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。

(2)再平方探测

按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。

(3)伪随机探测

按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。

  • 链式地址法(HashMap的哈希冲突解决方法)

对于相同的值,使用链表进行连接。使用数组存储每一个链表。

优点:

(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

  缺点:

指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。

  • 建立公共溢出区

建立公共溢出区存储所有哈希冲突的数据。

  • 再哈希法

对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。

image.png

image.png

总结

  • 计算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],以此类推,直到可以插入。

2.3基本方法详解

  • public void set( T value) 设置当前线程绑定的局部变量
  • public T get() 获取当前线程绑定的局部变量
  • public void remove() 移除当前线程绑定的局部变量
  • protected T initialValue() 初始化值

set方法

image.png get方法

image.png

image.png remove方法

/**

 * 删除当前线程中保存的ThreadLocal对应的实体entry

 */

 public void remove() {

    // 获取当前线程对象中维护的ThreadLocalMap对象

     ThreadLocalMap m = getMap(Thread.currentThread());

    // 如果此map存在

     if (m != null)

        // 存在则调用map.remove

        // 以当前ThreadLocal为key删除对应的实体entry

         m.remove(this);

 }

initialValue方法

/**

  • 返回当前线程对应的ThreadLocal的初始值

  • 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时

  • 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。

  • 通常情况下,每个线程最多调用一次这个方法。

  • 这个方法仅仅简单的返回null {@code null};

  • 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,

  • 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法

  • 通常, 可以通过匿名内部类的方式实现

  • @return 当前ThreadLocal的初始值

*/

protected T initialValue() {

return null;

}

三 面试

3.1 为什么ThreadLocalMap的key要设计成弱引用?

  • 内存泄漏相关概念

Memory overflow:内存溢出,没有足够的内存提供申请者使用。

Memory leak: 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

  • 引用相关概念

强引用 只要引用存在,垃圾回收器永远不会回收

软引用 非必须引用,内存溢出之前进行回收

弱引用 第二次垃圾回收时回收

虚引用 垃圾回收时回收,无法通过引用取到对象值

弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

好啦,今天的文章就到这里,希望能帮助到屏幕前迷茫的你们