作者:@adamhand

zybuluo.com/adamhand/note/1370920

ThreadLocal是什么

首先说明,ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。

ThreadLocal类提供了一种线程局部变量(ThreadLocal),即每一个线程都会保存一份变量副本,每个线程都可以独立地修改自己的变量副本,而不会影响到其他线程,是一种线程隔离的思想。

实现原理

ThreadLocal提供四个方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。这四种方法都是基于ThreadLocalMap的。

ThreadLocalMap

ThreadLocal内部有一个静态内部类ThreadLocalMap,该内部类是实现线程隔离机制的关键。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。该Map默认的大小是16,即能存储16个键值对,超过后会扩容。

具体源码如下:

Entry类

ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:

 static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

从上面代码中可以看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用。

set方法

 private void set(ThreadLocal<?> key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置int i = key.threadLocalHashCode & (len-1);// 采用“线性探测法”,寻找合适位置for (ThreadLocal.ThreadLocalMap.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,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了if (k == null) {// 用新元素替换陈旧的元素replaceStaleEntry(key, value, i);return;}}// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);int sz = ++size;// cleanSomeSlots 清楚陈旧的Entry(key == null)// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}

ThreadLocalMap的set方法和Map的put方法差不多,但是有一点区别是:put方法处理哈希冲突使用的是链地址法,而set方法使用的开放地址法。

set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:

private final int threadLocalHashCode = nextHashCode();

threadLocalHashCode是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():

private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}

nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量。

getEntry()

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);
}

由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:

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);elsei = nextIndex(i, len);e = tab[i];}return null;
}

这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。在Java知音公众号内回复“面试题聚合”,送你一份面试宝典

get()方法

public T get() {// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程的成员变量 threadLocalThreadLocalMap map = getMap(t);if (map != null) {// 从当前线程的ThreadLocalMap获取相对应的EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")// 获取目标值        T result = (T)e.value;return result;}}return setInitialValue();
}

首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocal的Entry,最后通过所获取的Entry获取目标值result。

getMap()方法可以获取当前线程所对应的ThreadLocalMap,如下:

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

set(T value)

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

获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,如下:

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

initialValue()

protected T initialValue() {return null;
}

该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。

注意:如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

remove()

 public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}

该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法,因为一个线程结束后,它所对应的局部变量就会被垃圾回收。

ThreadLocal使用示例

public class SeqCount {private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){// 实现initialValue()public Integer initialValue() {return 0;}};public int nextSeq(){seqCount.set(seqCount.get() + 1);return seqCount.get();}public void removeSeq(){seqCount.remove();}public static void main(String[] args){SeqCount seqCount = new SeqCount();SeqThread thread1 = new SeqThread(seqCount);SeqThread thread2 = new SeqThread(seqCount);SeqThread thread3 = new SeqThread(seqCount);SeqThread thread4 = new SeqThread(seqCount);thread1.start();thread2.start();thread3.start();thread4.start();}private static class SeqThread extends Thread{private SeqCount seqCount;SeqThread(SeqCount seqCount){this.seqCount = seqCount;}public void run() {for(int i = 0 ; i < 3 ; i++){System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());}seqCount.removeSeq();}}
}

结果如下:

Thread-1 seqCount :1
Thread-3 seqCount :1
Thread-2 seqCount :1
Thread-0 seqCount :1
Thread-2 seqCount :2
Thread-3 seqCount :2
Thread-1 seqCount :2
Thread-3 seqCount :3
Thread-2 seqCount :3
Thread-0 seqCount :2
Thread-1 seqCount :3
Thread-0 seqCount :3

ThreadLocal与内存泄漏

为什么会出现内存泄漏

首先看一下运行时ThreadLocal变量的内存图:

运行时,会在栈中产生两个引用,指向堆中相应的对象。

可以看到,ThreadLocalMap使用ThreadLocal的弱引用作为key,这样一来,当ThreadLocal ref和ThreadLocal之间的强引用断开 时候,即ThreadLocal ref被置为null,下一次GC时,threadLocal对象势必会被回收。

这样,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,比如使用线程池,线程使用完成之后会被放回线程池中,不会被销毁,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。

  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

为什么要使用弱引用?

使用弱引用,是为了更好地对ThreadLocal对象进行回收。如果使用强引用,当ThreadLocal ref = null的时候,意味着ThreadLocal对象已经没用了,ThreadLocal对象应该被回收,但由于Entry中还存着这对ThreadLocal对象的强引用,导致ThreadLocal对象不能回收,可能会发生内存泄漏。

为什么不将value也设置成弱引用?

为什么呢?

如何避免内存泄漏?

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

ThreadLocal与脏读

前面说了,ThreadLocal中的set()、get()和remove()方法都会对key==null的value进行处理,其中set()和get()方法是将key==null的value置为null。但是如果ThreadLocal是static类型的,并且配合线程池使用,线程池会重用Thread对象,同时会重用与Thread绑定的ThreadLocal变量。倘若下一个线程不调用set()方法重新设置初始值,也不调用remove()方法处理旧值,直接调用get()方法获取,就会出现脏读问题。

例子如下。

public class DirtyDataInThreadLocal {public static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {//使用固定大小为1的线程池,说明上一个线程属性会被下一个线程属性复用ExecutorService pool = Executors.newFixedThreadPool(1);for(int i = 0; i < 2; i++){MyThread thread = new MyThread();pool.execute(thread);}}private static class MyThread extends Thread{private static boolean flag = true;@Overridepublic void run() {if(flag){//第一个线程set后,没有remove,第二个线程也没有进行set操作threadLocal.set(this.getName() + ", session info.");flag = false;}System.out.println(this.getName() + " 线程是 " + threadLocal.get());}}
}

打印结果如下:

Thread-0线程是 Thread-0, session info.
Thread-1线程是 Thread-0, session info.

ThreadLocal使用场景

数据连接和Session管理

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

如:

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {public Connection initialValue() {return DriverManager.getConnection(DB_URL);}
};
public static Connection getConnection() {return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {Session s = (Session) threadSession.get();try {if (s == null) {s = getSessionFactory().openSession();threadSession.set(s);}} catch (HibernateException ex) {throw new InfrastructureException(ex);}return s;
}

参考

【死磕Java并发】—–深入分析ThreadLocal
深入分析 ThreadLocal内存泄漏问题
Java并发编程:深入剖析ThreadLocal
Java多线程编程-(8)-多图深入分析ThreadLocal原理
ThreadLocal类详解与源码分析
ThreadLocal解决什么问题
对ThreadLocal实现原理的一点思考
ThreadLocalMap的enrty的key为什么要设置成弱引用
《码出高效 Java开发手册》

END

Java面试题专栏

【61期】MySQL行锁和表锁的含义及区别(MySQL面试第四弹)

【62期】解释一下MySQL中内连接,外连接等的区别(MySQL面试第五弹)

【63期】谈谈MySQL 索引,B+树原理,以及建索引的几大原则(MySQL面试第六弹)

【64期】MySQL 服务占用cpu 100%,如何排查问题? (MySQL面试第七弹)

【65期】Spring的IOC是啥?有什么好处?

【66期】Java容器面试题:谈谈你对 HashMap 的理解

【67期】谈谈ConcurrentHashMap是如何保证线程安全的?

【68期】面试官:对并发熟悉吗?说说Synchronized及实现原理

【69期】面试官:对并发熟悉吗?谈谈线程间的协作(wait/notify/sleep/yield/join)

【70期】面试官:对并发熟悉吗?谈谈对volatile的使用及其原理

我知道你 “在看”

Java并发之ThreadLocal相关推荐

  1. JAVA并发之多线程基础(2)

    除了我们经常用的synchronized关键字(结合Object的wait()和notify()使用)之外,还有对应的上篇文章讲到的方法JAVA并发之多线程基础(1)之外,我们日常中使用到最多的也就是 ...

  2. JAVA并发之多线程基础(5)

    上面介绍了并发编程中的栅栏等JAVA并发之多线程基础(4) .通过唯一的一个终点线来帮助确定线程是多晚开始执行下一次操作. LockSupport 提供了一个比较底层的线程挂起操作.有点类似于susp ...

  3. 我的Java开发之路

    最近有一位小伙伴通过公众号给我留言, "我参加工作没多久,看着圈里的技术大牛,特别羡慕,也渴望成为技术大牛,想让您分享一下从小白到大牛是怎样练成的,我该如何提高自己" 首先,谢谢这 ...

  4. java并发之SynchronousQueue实现原理

    前言 SynchronousQueue是一个比较特别的队列,由于在线程池方面有所应用,为了更好的理解线程池的实现原理,笔者花了些时间学习了一下该队列源码(JDK1.8),此队列源码中充斥着大量的CAS ...

  5. 你真的弄明白了吗?Java并发之AQS详解

    你真的弄明白了吗?Java并发之AQS详解 带着问题阅读 1.什么是AQS,它有什么作用,核心思想是什么 2.AQS中的独占锁和共享锁原理是什么,AQS提供的锁机制是公平锁还是非公平锁 3.AQS在J ...

  6. Java中的ThreadLocal的使用--学习笔记

    ThreadLocal直译为"线程本地"或"本地线程",如果你真的这么认为,那就错了!其实它就是一个容器,用于存放线程的局部变量,我认为应该叫做ThreadLo ...

  7. 面试:你说你精通Java并发,给我讲讲Java并发之J.U.C

    转载自 面试:你说你精通Java并发,给我讲讲Java并发之J.U.C J.U.C J.U.C即java.util.concurrent包,为我们提供了很多高性能的并发类,可以说是java并发的核心. ...

  8. 4问教你搞定java中的ThreadLocal

    摘要:ThreadLocal是除了加锁同步方式之外的一种保证规避多线程访问出现线程不安全的方法. 本文分享自华为云社区<4问搞定java中的ThreadLocal>,作者:breakDra ...

  9. java并发之CopyOnWriteArraySet

    java并发之CopyOnWriteArraySet CopyOnWriteArraySet是基于CopyOnWriteArrayList实现的,持有CopyOnWriteArrayList的内部对象 ...

最新文章

  1. numpy zeros矩阵_零矩阵使用numpy.zeros()| 使用Python的线性代数
  2. matlab 二维高斯滤波 傅里叶_机器视觉 03.2 频域低通滤波
  3. 左手手机右手智慧屏 华为9月要搞大事情
  4. docker探索-在centos6.5中安装docker(三)
  5. OpenWrt开发必备软件模块——网络管理(CWMP、SSH、QoS、SMTP、NTP、uHTTPd)
  6. 【PEST++】03 水文模型不确定性和灵敏度分析
  7. RFID射频卡分类及标准
  8. ubuntu ffmpeg 录制系统音频
  9. tcp 握手失败_TCP三次握手四次挥手总结(流程、常见问题、会发生的攻击、防范方法)...
  10. LuoguP4313 BZOJ3894 文理分科——最小割
  11. 【CVE-2021-4043】Linux本地提权漏洞复现
  12. OpenCV--011:像素归一化
  13. 为什么流量过万转化率却很低?
  14. 3、原币金额和本币金额
  15. 如何寻找数组中最大值与最小值(取双元素法)
  16. MATLAB 学习心得(3) 定积分和双重积分,三重积分的求法
  17. pyqt 多窗口之间的相互调用方法
  18. 死亡搁浅运送系统服务器,死亡搁浅车辆怎么解锁 死亡搁浅载具获取方法一览...
  19. Mapper的XML文件(一)
  20. JAVA开发与运维(docker运维常规操作)

热门文章

  1. 荣耀20 Pro 5000元最强拍照机翻车?官方怒放样张辟谣
  2. 科创板鸣锣开市 一图带你了解首批25家公司
  3. 朋友圈终于对利诱打卡行为动手了!多款英语学习类软件中枪
  4. Django中的request和response
  5. 程序员不知道怎么和女生约会?进来看看这篇文章
  6. 开发中常用的加密算法大全初步总结
  7. 嵌入式Linux入门7:kernel移植
  8. Linux移植随笔:又遇困难
  9. 固态和机械硬盘组raid_电脑是固态硬盘好还是机械硬盘
  10. sql 查询 tag_Askgit:给git增加个翅膀,用sql挖掘仓库的信息