博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ThreadLocal实现原理
阅读量:3943 次
发布时间:2019-05-24

本文共 9455 字,大约阅读时间需要 31 分钟。

什么是ThreadLocal变量

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

ThreadLocal实现原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。ThreadLocalMap 继续了WeakReference弱引用对象,内部其实只是一个value对象,可以把ThreadLocal对象当做key,而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 set 方法:

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

get方法:

public T get() {
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) return (T)map.get(this); // Maps are constructed lazily. if the map for this thread // doesn't exist, create it, with this ThreadLocal and its // initial value as its only entry. T value = initialValue(); createMap(t, value); return value; }

createMap方法:

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

ThreadLocalMap是个静态的内部类:

static class ThreadLocalMap {
........ }

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

ThreadLocal线程私有原理

Thread对象有变量ThreadLocal.ThreadLocalMap,变量threadLocals就是ThreadLocal对象的引用,所以每个线程对象都有自己的成员变量ThreadLocal,线程私有。

public class Thread implements Runnable {
//... /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocal底层是ThreadLocalMap,获取ThreadLocal内容就是先getMap,也即是当前线程对象的成员变量threadLocals。

/**     * Get the map associated with a ThreadLocal. Overridden in     * InheritableThreadLocal.     *     * @param  t the current thread     * @return the map     */    ThreadLocalMap getMap(Thread t) {
return t.threadLocals; }

内存泄漏问题

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

在这里插入图片描述

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

建议回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用try-finally块进行回收:

objectThreadLocal.set(userInfo); try {
// ... } finally {
objectThreadLocal.remove(); }

remove()方法源码:

private void remove(ThreadLocal
key) {
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)]) {
if (e.get() == key) {
e.clear(); // 清除key expungeStaleEntry(i); // 清除value return; } }}

e.clear()用于清除Entry的key,它调用的是WeakReference中的方法:this.referent = null。这里只清楚key,但其实key是弱引用,很快也会被GC回收。expungeStaleEntry方法对value进行擦除操作。

value擦除操作

expungeStaleEntry(i)用于清除Entry对应的value。

ThreadLocal提供的get()方法举例,它调用了ThreadLocalMap#getEntry()方法,对key进行了校验和对null key进行擦除。

private Entry getEntry(ThreadLocal
key) {
// 拿到索引位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e);}

如果key为null, 则会调用getEntryAfterMiss()方法,在这个方法中,如果k == null , 则调用expungeStaleEntry(i);(ex ban chi)方法。

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

expungeStaleEntry(i)方法完成了对key=null 的key所对应的value进行赋空, 释放了空间避免内存泄漏。

同时它遍历下一个key为空的entry, 并将value赋值为null(赋值null,强引用就可能被GC), 等待下次GC释放掉其空间。

private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; // 遍历下一个key为空的entry, 并将value指向null for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal
k = e.get(); if (k == null) {
e.value = null; tab[i] = null; size--; } else {
int h = k.threadLocalHashCode & (len - 1); if (h != i) {
tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i;}

同理, set()方法最终也是调用该方法(expungeStaleEntry), 调用路径: set(T value)->map.set(this, value)->rehash()->expungeStaleEntries()

remove方法remove()->ThreadLocalMap.remove(this)->expungeStaleEntry(i)

这样做, 也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建ThreadLocal但不调用set、get、remove方法等。所以最能解决问题的办法就是用完ThreadLocal后手动调用remove().

使用场景

如上文所述,ThreadLocal 适用于如下两种场景

  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

1)存储用户Session

一个简单的用ThreadLocal来存储Session的例子:

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; }

2)解决线程安全的问题

比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:

public class DateUtil {
private static ThreadLocal
format1 = new ThreadLocal
() {
@Override protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String formatDate(Date date) {
return format1.get().format(date); }}

这里的DateUtil.formatDate()就是线程安全的了。(Java8里的 java.time.format.DateTimeFormatter是线程安全的,Joda time里的DateTimeFormat也是线程安全的)。

ThreadLocalRandom

ThreadLocalRandom使用ThreadLocal的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,从而避免了竞争。

用法:

ThreadLocalRandom.current().nextInt(100)

current()的时候初始化一个初始化种子到线程,每次nextseed再使用之前的种子生成新的种子:

UNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA);

如果你通过主线程调用一次current生成一个ThreadLocalRandom实例保存,那么其它线程来获取种子的时候必然取不到初始种子,必须是每一个线程自己用的时候初始化一个种子到线程。可以在nextSeed设置一个断点看看:

UNSAFE.getLong(Thread.currentThread(),SEED);

面试题

手动释放ThreadLocal遗留存储?你怎么去设计/实现?

这里主要是强化一下手动remove的思想和必要性,设计思想与连接池类似。

包装其父类remove方法为静态方法,如果是spring项目, 可以借助于bean的声明周期, 在拦截器的afterCompletion阶段进行调用。

弱引用导致内存泄漏,那为什么key不设置为强引用

如果key设置为强引用, 当threadLocal实例释放后, threadLocal=null, 但是threadLocal会有强引用指向threadLocalMap,threadLocalMap.Entry又强引用threadLocal(key), 这样会导致threadLocal不能正常被GC回收。

弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行擦除的补救措施, 方案上略胜一筹。

线程执行结束后会不会自动清空Entry的value?

事实上,当currentThread执行结束后, threadLocalMap变得不可达从而被回收,Entry等也就都被回收了,但这个环境就要求不对Thread进行复用,但是我们项目中经常会复用线程来提高性能, 所以currentThread一般不会处于终止状态。

Thread和ThreadLocal有什么联系呢

Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中, 来实现多线程处理。Thread有变量ThreadLocalMap,以此实现线程私有,每次都需要使用线程的这个变量,也即是ThreadLocal。

spring如何处理bean多线程下的并发问题

ThreadLocal天生为解决相同变量的访问冲突问题, 所以这个对于spring的默认单例bean的多线程访问是一个完美的解决方案。spring也确实是用了ThreadLocal来处理多线程下相同变量并发的线程安全问题。

spring 如何保证数据库事务在同一个连接下执行的

要想实现jdbc事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成。那spring 如何保证数据库事务在同一个连接下执行的呢?

DataSourceTransactionManager 是spring的数据源事务管理器, 它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成。

概要源码:

1.事务开始阶段

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource

在这里插入图片描述

2.事务结束阶段:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource

在这里插入图片描述

转载地址:http://mviwi.baihongyu.com/

你可能感兴趣的文章
centos8安装ntfs-3g后,不能自动挂载U盘(NTFS格式)
查看>>
Linux安装显卡驱动
查看>>
使用minicom
查看>>
linux常用外设-打印机指纹和蓝牙的安装管理
查看>>
记录一下安装在移动硬盘上的fedora linux v33在各种笔记本下的兼容性
查看>>
关于安装系统后不能启动的问题!
查看>>
U盘的挂载过程-先记录一下
查看>>
python程序启动过程报错的排错一般步骤
查看>>
linux下UEFI的管理
查看>>
类thinkpad笔记本安装deepinv20后启动黒屏的解决
查看>>
利用本地centos镜像升级centOS
查看>>
FreeBSD常用操作
查看>>
VC及esxi升级的必要性和步骤
查看>>
hp DL338服务器修改ilo管理地址
查看>>
vmware convert P2V 错误二三事
查看>>
让kali2020中的zsh有补完功能
查看>>
python解开压缩文件6位纯数字密码
查看>>
5620系列密码清除
查看>>
vncsever-centos&debian
查看>>
华为snmp模板
查看>>