浅谈ThreadLocal(三)

123 阅读6分钟

承接上篇:浅谈ThreadLocal(二)

本篇我们解决上篇遗留问题:

  • Q1:为什么说ThreadLocal可以解决线程安全问题?
  • Q2:Threadlocal为什么要用弱引用?
  • Q3:ThreadLocal有哪些使用场景,要注意哪些问题?

一:ThreadLocal如何解决线程安全问题

线程安全问题:指多个线程同时访问同一个共享数据时,可能产生的数据冲突、竞争和不一致的情况。

通常我们解决这类问题都是通过给共享资源加锁,如synchronizedLock。而ThreadLocal另辟蹊径,因为它持有的变量是由线程独占,不跟别人共享的。如下是ThreadLocal解决线程安全问题的思路:

  1. 假设此时有个篮子(共享资源)
  2. 此时来了个张三(线程一),它对着这个篮子用3D打印技术复刻了个一样的,拿走去用了。
  3. 又来了李四(线程二),它也对着篮子用3D打印技术复刻了个一样的,拿去用了
  4. ....

对比synchronized解决线程安全问题

  1. 假设此时有个篮子(共享资源),上锁状态
  2. 此时来了个张三(线程一),张三持有锁,把篮子拿去用了。
  3. 又来了个李四(线程二),锁被张三拿走了,无法使用篮子。一直等到张三把锁还回来,才可以使用篮子。
  4. ...

对比一看,ThreadLocal解决共享资源的方式是每个线程拷贝一份共享资源,线程之间压根不存在资源竞争关系,我们常看网上说变量副本,就是这里提到的共享资源的拷贝。而加锁的方式是把多线程从并发的变成串行的,一个一个来访问。

二:Threadlocal为什么要用弱引用?

引言:大部分同学都模糊的知道ThreadLocal用了弱引用;ThreadLocal存在内存泄漏的问题。为什么我说是模糊的知道呢,如果你能回答“ThreadLocal的弱引用是谁与谁之间的关系、为什么要用弱引用?”;“为什么都说内存泄漏的问题?”那就说明你比较了解ThreadLocal了。

我们先看看什么是弱引用:

当JVM进行垃圾回收时,如果一个对象只有弱引用(没有任何强引用指向它),则该对象会被回收。

此外java中还提供了其他三种引用:

  1. 强引用:指向对象的引用,只要存在强引用,GC时就不会回收该对象。
  2. 软引用:比弱引用强,如果一个对象只有软引用(没有任何强引用指向它),那么在遇到内存不足的情况下,JVM中更可能回收这个对象。
  3. 虚引用:是最弱的一种引用类型,在任何时候都可以被回收,主要作为对象被回收时的一个“通知”机制,用于跟踪对象被收集的状态。虚引用必须与ReferenceQueue一同使用。

弱引用指的是ThreadLocalMap中的key通过弱引用指向ThreadLocal实例。我们看看下面代码的引用链:

    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    threadLocal.set("hello");
  • threadLocal->new ThreadLocal在堆中开辟的空间(强引用)
  • Thread->ThreadLocalMap->key->new ThreadLocal在堆中开辟的空间(弱引用)
  • Thread->ThreadLocalMap->value->String (强引用)

Q:为什么key要用弱引用而value却用强引用呢?

A:这其实是防止ThreadLocal内存泄漏的一种机制。我们把上述代码添加一行如下:

    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    threadLocal.set("hello");
    threadLocal = null;

这时我们再分析代码的引用链:

  • threadLocal->new ThreadLocal在堆中开辟的空间(强引用)
  • Thread->ThreadLocalMap->key->new ThreadLocal在堆中开辟的空间(弱引用)
  • Thread->ThreadLocalMap->value->String (强引用)
  • 执行threadLocal = null;threadLocal->new ThreadLocal在堆中开辟的空间(强引用)

这时new ThreadLocal在堆中开辟的空间由于只剩ThreadLocalMap中key对它的弱引用,就会被gc时回收掉。这样在避免了由于ThreadLocal对象造成的内存泄漏。但是会存在新问题,就是ThreadLocalMap的key为null了后,value是强引用啊,那么就导致value无法获取也无法删除但是一直占用内存,这样的Entry多了也会导致内存泄漏的问题。而ThreadLocal的作者也为我们提供了解决这个问题方法remove(),因此要养成好习惯,value用完了及时删除。

其实ThreadLocal的作者除了提供remove(),在ThreadLocal内部也对内存溢出做了处理(可以说设计的很周到了)。如:

  • cleanSomeSlots
  • expungeStaleEntries
  • expungeStaleEntry
  • replaceStaleEntry

有了这些方法配合上key的弱引用,即使我们忘记调用remove(),内存泄漏也没有那么容易,篇幅有限就不展开细说了。想了解的小伙伴在评论区扣“想深入”,想学的同学多的话(暂定超过5个吧),我再展开细说。这里我先给出内存泄漏需要满足的条件:

  • ThreadLocal实例不存在强引用,且后续不调用set()get()remove()操作
  • 线程一直运行(线程池中的核心线程)
  • 触发了gc

回到本文,是否有同学想过既然key可以通过设置弱引用来解决由于key导致的内存溢出。那value是否也能通过设置成弱引用呢?答案是不合适,因为value本来是就是存的变量副本,如果value设置为弱引用,那么变量被回收时,变量副本在gc时也会回收。而这样的结果不是我们想要的。举个生活中的例子,你去打印店复印了份文件,结果原件不见了,导致复印件也不见了,这合理么。本来复印件就是为了跟原件隔离,你可以拿着复印件去干任何事,与原件无关。再看个实际应用的例子,线程一在操作变量副本时,突然因为变量被回收,导致变量副本也被回收了,反馈到程序中很可能就造成空指针异常了,这不是很诡异嘛。因此Value用强引用也无可厚非。

三、ThreadLocal使用场景

ThreadLocal主要用于在多线程环境下维护线程间的数据隔离。

3.1 Spring的事务处理

事务大致分为三个阶段:获取数据库连接、业务处理,事务提交。spring帮我们处理了获取连接和事务提交两个阶段,我们只需要注重业务处理就行,关键是我们如何让三个阶段共用一个数据库连接呢?答案是通过ThreadLocal,在获取数据库连接时放入ThreadLocal中,业务处理和事务提交阶段直接从ThreadLocal中取就行了。

3.2 存储用户信息

在写web项目时,避免不了保存用户的登入信息。比如在用户登入后在拦截器中把信息添加到ThreadLocal中,本次请求的后续操作要获取用户信息直接在ThreadLocal中取就行了,避免了层层之间通过参数传递用户信息。