【原创】Java并发编程系列30 | ThreadLocal

2020/08/06 11:38
阅读数 43

  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为星标”!这样才不会错过每日进阶架构文章呀。

  

  2020年Java原创面试题库连载中

  (共18篇)

  【032期】JavaEE面试题(四)Spring(2)

  

  线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,而大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。这篇文章介绍另种解决线程安全的思路——ThreadLocal:

  介绍

  使用

  源码

  内存泄露

  总结

  1. 介绍

  线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。

  ThreadLocal提供了另一种解决思路,让每个线程拥有自己私有的内存空间,将线程私有的数据存入这个私有空间内,线程与线程之间相互隔离,这样就不会有线程安全问题。

  数据结构

  每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。

  threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。

  
2. 使用

  ThreadLocal核心方法:

  get():返回当前线程副本中该ThreadLocal对应的值。

  initialValue():返回当前线程副本中的该ThreadLocal对应对应的“初始值”。

  remove():移除当前线程副本中该ThreadLocal对应的值。

  set(T value):当前线程副本中该ThreadLocal对应的值为value。

  使用举例:

  每个线程保存一个私有的int值count,5个线程count从0加到10,线程之间互不影响。

  /**
* 每个线程保存一个私有的int值count
* 5个线程count从0加到10,线程之间互不影响
*/
public class ThreadLocalDemo {
private static ThreadLocal countLocal = new ThreadLocal(){
public Integer initialValue() {
return 0;
}
};








  public static void main(String[] args){
for (int i = 1; i <= 5; i++) {
new Thread("Thread_" + i) {
public void run() {
for (int j = 1; j <= 10; j++) {
countLocal.set(countLocal.get() + 1);
System.out.println(getName() + ": count=" + countLocal.get());
}
};
}.start();
}
}
}












  输出结果如下:

  Thread_3: count=1
Thread_5: count=1
Thread_5: count=2
Thread_4: count=1
Thread_4: count=2
Thread_2: count=1
Thread_2: count=2
Thread_2: count=3
Thread_2: count=4
Thread_1: count=1
Thread_2: count=5
Thread_4: count=3
Thread_5: count=3
Thread_5: count=4
Thread_3: count=2
Thread_5: count=5
Thread_5: count=6
Thread_5: count=7
Thread_5: count=8
Thread_4: count=4
Thread_4: count=5
Thread_4: count=6
Thread_2: count=6
Thread_2: count=7
Thread_2: count=8
Thread_2: count=9
Thread_1: count=2
Thread_2: count=10
Thread_4: count=7
Thread_5: count=9
Thread_3: count=3
Thread_3: count=4
Thread_5: count=10
Thread_4: count=8
Thread_1: count=3
Thread_4: count=9
Thread_3: count=5
Thread_4: count=10
Thread_1: count=4
Thread_3: count=6
Thread_1: count=5
Thread_3: count=7
Thread_1: count=6
Thread_3: count=8
Thread_1: count=7
Thread_3: count=9
Thread_1: count=8
Thread_3: count=10
Thread_1: count=9
Thread_1: count=10

















































  可以看到,即使5个线程并发执行,但是每个线程内部的count都是按1-10的顺序相加的。

  3. 源码 3.1 数据结构

  每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。

  threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。

  /**
* 线程局部变量threadLocals为ThreadLocal.ThreadLocalMap类型
*/
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}




  /**
* ThreadLocal$ThreadLocalMap 散列表结构
* key=ThreadLocal value=Object
*/
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference {
/** The value associated with this ThreadLocal. */
Object value;







  Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}





3.2 get()

  在理解的ThreadLocal的存储结构之后,再看get()和set()方法就很简单了。

  get():

  获取当前线程thread。

  获取当前线程thread.threadLocals,threadLocals是map结构。

  map的key是ThreadLocal类型,获取map中当前threadLocal对应的value值。

  如果map=null,就创建map并赋初值。

  public T get() {
// 获取当前线程私有的map thread.threadLocals
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 获取map的value值
if (map != null) {
// map的key是ThreadLocal类型
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map=null,初始化map,下文有讲解
return setInitialValue();
}















  /**
* 返回ThreadLocalMap类型的thread.threadLocals
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}




  
/**
* 初始化value,map=null时会初始化map
*/
private T setInitialValue() {
T value = initialValue();// 初始值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);// 将初始值存入map
else
createMap(t, value);// map=null时初始化map
return value;
}












  /**
* 返回map中value的初始值
* 默认为null,一般需要重写该方法以获得非null值
*/
protected T initialValue() {
return null;
}






set():

  获取当前线程thread。

  获取当前线程thread.threadLocals,threadLocals是map结构。

  map的key是ThreadLocal类型,设置map中当前threadLocal对应的value值。

  如果map=null,就创建map并赋值。

  public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}







4. 注意问题 4.1 每个线程最好只存一个ThreadLocal

线性探测解决Hash冲突:根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

  ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

  如下,ThreadLocalMap.set()方法:

  private void set(ThreadLocal key, Object value) {
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
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();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
/*
* key=null而value!=null,因为key是弱引用
* 用新的key-value将旧的null-value替换掉
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

















  tab[i] = new Entry(key, value);
int sz = ++size;
// 清除陈旧的Entry(key == null)
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}





  ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。

  4.2 引发内存泄露

  ThreadLocal使用中会有内存泄露问题。

  ThreadLocalMap的key是弱引用,而Value是强引用。源码如下:

  static class ThreadLocalMap {
static class Entry extends WeakReference {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}








  ThreadLocalMap的key是弱引用,发生GC时弱引用key会被回收;而value是强引用,GC时不会被回收。

  解决:

  ThreadLocalMap的set()、cleanSomeSlots()等方法中都做了相应处理,检查存在key=null而value!=null的Entry就会删掉;

  在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

  5. 总结 同步机制 VS ThreadLocal

  同步机制是通过控制线程访问共享对象的顺序,类似“时间换空间”,同一时刻共享对象只能被一个线程访问造成整体上响应时间增加,但是对象只占有一份内存。

  而ThreadLocal是为每一个线程分配一个该对象,各用各的互不影响。类似“空间换时间”,为每个线程都分配了一份对象,自然而然内存使用率增加,但整体上时间效率要增加很多。

  ThreadLocal存储结构

  每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。

  threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。

  注意问题

  ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。

  由于ThreadLocalMap的key是弱引用,ThreadLocal使用中会有内存泄露问题。在使用完ThreadLocal之后调用remove方法删除值,可避免内存泄露问题。

  并发系列文章汇总

  之前,给大家发过三份Java面试宝典,这次新增了一份,目前总共是四份面试宝典,相信在跳槽前一个月按照面试宝典准备准备,基本没大问题。

  《java面试宝典5.0》(初中级)

  《350道Java面试题:整理自100+公司》(中高级)

  《资深java面试宝典-视频版》(资深)

  《Java[BAT]面试必备》(资深)

  分别适用于初中级,中高级资深级工程师的面试复习。

  内容包含java基础、javaweb、mysql性能优化、JVM、锁、百万并发、消息队列,高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级等等。

  获取方式:点“在看”,V信关注上述Java最全面试题库号并回复【面试】即可领取,更多精彩陆续奉上。

  看到这里,证明有所收获

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部