引言
java.lang
包中的 ThreadLocal
使我们能够为当前线程单独存储数据, 并将其简单地包装在一种特殊类型的对象中. 本文借着在业务中使用了一次 ThreadLocal 的机会, 记录一下 ThreadLocal 的使用, 以及深入探讨一下它的实现.
ThreadLocal 的使用
TheadLocal
构造允许我们存储只能由特定线程访问的数据.
假设我们想要一个与特定线程捆绑在一起的整数值:
ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
接下来, 当我们想从线程中使用这个值时, 我们只需要调用 get()
或 set()
方法. 简单来说, 我们可以想象 ThreadLocal
将数据存储在一个以线程为 key
的 map
内部.
因此, 当我们在 threadLocalValue
上调用 get()
方法时, 我们将获得请求线程的 Integer
值:
threadLocalValue.set(1);
Integer result = threadLocalValue.get();
get
返回此线程局部变量的当前线程副本中的值. 如果变量没有当前线程的值, 则首先将其初始化为 initialValue()
方法调用返回的值.
我们可以通过使用 withInitial()
静态方法并将供应商传递给它来构造 ThreadLocal
的实例:
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
要从 ThreadLocal
中删除值, 我们可以调用 remove()
方法:
threadLocal.remove();
remove
会删除此线程局部变量的当前线程值. 如果这个线程局部变量随后 被当前线程读取, 它的值将通过调用它的 initialValue()
方法重新初始化, 除非它的值是由当前线程在中间设置的. 这可能会导致在当前线程中多次调用 initialValue
方法.
注意事项
ThreadLocal 和线程池
ThreadLocal 提供了一个易于使用的 API 来限制每个线程的一些值. 这是在 Java 中实现线程安全的合理方式. 但是, 当我们同时使用 ThreadLocal 和线程池时, 我们应该格外小心 .
为了更好地理解这个可能的警告, 让我们考虑以下场景:
- 首先, 应用程序从池中借用一个线程.
- 然后它将一些线程限制的值存储到当前线程的 ThreadLocal 中.
- 当前执行完成后, 应用程序将借用的线程返回到池中.
- 过了一会儿, 应用程序借用同一个线程来处理另一个请求.
- 由于应用程序上次没有执行必要的清理, 它可能会为新请求重新使用相同的 ThreadLocal 数据.
这可能会在高度并发的应用程序中导致令人惊讶的后果.
解决此问题的一种方法是在使用完每个 ThreadLocal
后手动删除它. 因为这种方法需要严格的代码审查, 所以很容易出错.
扩展 ThreadPoolExecutor
可以扩展 ThreadPoolExecutor
类并为 beforeExecute()
和 afterExecute()
方法提供自定义钩子实现. 线程池将在使用借用线程运行任何内容之前调用 beforeExecute()
方法. 另一方面, 它会在执行我们的逻辑后调用 afterExecute()
方法.
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
System.out.println("hello, world");
}
}
class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {
// 省略必须的构造函数
/**
* 在执行给定线程的给定的 Runnable 之前会先执行这个方法.
* 这个方法是将被执行任务 Runnable r 的方法调用, 可以用来重新初始化 ThreadLocal 变量或者记录日志.
* <p>
* 这里的实现什么也没有做, 但是可以在子类中定制.
* 注意, 为了在多重继承也能正确使用, 子类应该在最后执行 super.beforeExecute 方法.
*
* @param t 将要执行任务的线程
* @param r 将要执行的任务
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
// TODO 在这里写定制代码
super.beforeExecute(t, r);
}
/**
* 在任务结束之后执行的方法.
* 这个方法是被执行任务的线程所调用.
*
* 注意要在定制代码之前执行 super.afterExecute(r, t); 避免多重继承时顺序问题.
*
* <p><b>Note:</b> When actions are enclosed in tasks (such as
* {@link FutureTask}) either explicitly or via methods such as
* {@code submit}, these task objects catch and maintain
* computational exceptions, and so they do not cause abrupt
* termination, and the internal exceptions are <em>not</em>
* passed to this method. If you would like to trap both kinds of
* failures in this method, you can further probe for such cases,
* as in this sample subclass that prints either the direct cause
* or the underlying exception if a task has been aborted:
*
* <pre> {@code
* class ExtendedExecutor extends ThreadPoolExecutor {
* // ...
* protected void afterExecute(Runnable r, Throwable t) {
* super.afterExecute(r, t);
* if (t == null && r instanceof Future<?>) {
* try {
* Object result = ((Future<?>) r).get();
* } catch (CancellationException ce) {
* t = ce;
* } catch (ExecutionException ee) {
* t = ee.getCause();
* } catch (InterruptedException ie) {
* Thread.currentThread().interrupt(); // ignore/reset
* }
* }
* if (t != null)
* System.out.println(t);
* }
* }}</pre>
*
* @param r 已经完成的任务
* @param t 任务完成时可能抛出的异常
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
}
}
ThreadLocal 与 Tomcat
有时候常常会忽略 Tomcat 也是自带线程池的, 上一个请求在 ThreadLocal 中留下的东西, 如果不清楚的话, 那么下一个请求会读到 ThreadLocal 中已经存在的东西, 千言万语一句话, 用完了就及时 remove
吧.
JDK ThreadLocal 底层原理
Thread 类里面有个属性 ThreadLocal.ThreadLocalMap threadLocals = null;
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
// ......
}
ThreadLocalMap
是 ThreadLocal
的内部类, 定制了一个 hash map, key 是 ThreadLocal 的实例对象, 而 value 就是 ThreadLocal 中存储的值.
ThreadLocal#get
方法实际上就是先通过 currentThread 获取 threadLocals 这个 map, 然后通过 ThreadLocal 实例对象作为 key 来查询.
/**
* 返回这个线程局部变量在当前线程中的拷贝【名词】的值
* 如果当前线程并没有这个值, 则调用 initialValue 来初始化这个值.
*
* @return 当前线程的线程局部变量的值
*/
public T get() {
// 获取当前线程,【问题:Java 中的线程和操作系统中的线程如何对应?】
Thread t = Thread.currentThread();
// 获取当前线程存放线程局部变量的值的 Map
ThreadLocalMap map = getMap(t);
// 如果 Map 不为空, 且线程局部变量的 entry 也不为空采取拿值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果 Map 为空或者 Entry 为空则先要创建这个 Map 以及 Entry
return setInitialValue();
}
/**
* 设置当前线程的线程局部变量的值.
* Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.【这句话没看懂】
*
* @param value 存储在线程局部变量中的值
*/
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 方法读取这个线程局部变量的值, 则会重新通过 initialValue 进行初始化.
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
为什么要用 WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 和 ThreadLocal 关联的值 */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
WeakReference 和 Reference 这块我还真不熟.
import java.lang.ref.WeakReference;
public class Main {
static class SodaWater {
private String label;
public SodaWater(String label) {
this.label = label;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println( "【" + label + "】好家伙我被回收了......");
}
}
private static SodaWater sodaWater = null;
public static void main(String[] args) {
// 强引用
sodaWater = new SodaWater("static");
// 弱引用
WeakReference<SodaWater> staticWeakReference = new WeakReference<>(sodaWater);
WeakReference<SodaWater> localWeakReference = new WeakReference<>(new SodaWater("local"));
System.gc();
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("staticWeakReference.get() = " + staticWeakReference.get());
System.out.println("localWeakReference.get() = " + localWeakReference.get());
}
}
运行之后的输出为:
【local】好家伙我被回收了......
staticWeakReference.get() = Main$SodaWater@74a14482
localWeakReference.get() = null
Linux 中的线程局部变量如何实现的?
Linux's thread local storage implementation
__thread Foo foo;
How is "foo" actually resolved? Does the compiler silently replace every instance of "foo" with a function call? Is "foo" stored somewhere relative to the bottom of the stack, and the compiler stores this as "hey, for each thread, have this space near the bottom of the stack, and foo is stored as 'offset x from bottom of stack'"?
It's a little complicated (this document explains it in great detail), but it's basically neither. Instead the compiler puts a special .tdata section in the executable, which contains all the thread-local variables. At runtime, a new data section for each thread is created with a copy of the data in the (read-only) .tdata section, and when threads are switched at runtime, the section is also switched automatically.
The end result is that __thread
variables are just as fast as regular variables, and they don't take up extra stack space, either.
上面的地址可能失效了, 用这个: https://akkadia.org/drepper/tls.pdf
参考
开源框架是如何使用 ThreadLocal?
Quartz
【判断当前线程是否已经持有了这个锁】. 如果持有了, 那就直接跳到最后 return true 了. 因为同一个线程, 可能有多个程序片段会调用这个获取锁的方法.
可以看到, 使用 ThreadLocal 可以非常高效地判断当前线程的状态, 可以快速检测出当前线程是否已经获取了锁, 避免了后续锁的检测和争用.
MyBatis
应用程序使用 MyBatis, 可能会在多个程序片段去访问数据库, 做一些增删改查的操作. 它们可能需要在同一个事务里面.
举个例子, 我们修改完订单状态后, 可能还需要修改积分, 它们应该在同一个事务里.
Mybatis 使用 SqlSessionManager 保证了我们同一个线程取出来的连接总是同一个. 它是如何做到的呢? 其实很简单, 就是内部使用了一个 ThreadLocal.
然后所有的创建连接, 取连接都是通过这个 ThreadLocal 变量的 get/set 方法进行操作.
来源: https://developer.aliyun.com/article/972188
shiro
使用 ThreadLocal 是用来保存当前登录对象
我使用 ThreadLocal 的场景
主要是对 Service 层进行一个入口控制, 很多的 Controller 都会调用同一个 Service, 但是只想对其中的一些 Controller 进行控制, 然后就在 Controller 中设置了一些状态变量, 在 Service 中检测是否有这些状态变量.
检测完了之后, 就将其从 ThreadLocal 中移除.
这样做主要是不影响原来的业务, 当然也可以通过 HTTP 请求的参数控制.
ThreadLocal为什么要用WeakReference
内部结构
先上一张图看一下 ThreadLocal 的内部结构,每个Thread对象内部都维护了一个 ThreadLocal.ThreadLocalMap
我们在上图看到的就是三个Thread对象内部格子的ThreadLocalMap
这里要说的不是ThreadLocal,是ThreadLocal为什么要用WeakReference
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
弱引用WeakReference
弱引用只要发生了gc就会被回收,但前提是只有弱引用,没有强引用(这点本身也不容易做到)
WeakReference<Apple> weakReference = new WeakReference<>(new Apple("1"));
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(weakReference.get());//1
System.gc();//遇到gc就回收了
System.out.println(weakReference.get());//null
引用传递的特殊情况
- 传递的入参是null,方法内new了,也不会影响到外面null的对象,还是null的
- 传递的入参不是null,方法内=null了,也不会影响到外面不是null的对象
- 传递的入参不null,传递到线程中,线程运行中,外面把入参设置为null,线程内继续不是null 总结,别管里面外面 null变!null !null变null 都不会隔层影响原来啥样还啥样
//我们在这里只插入一段模拟线程运行的情况 ……
obj = new ObjectPassing();
obj.setName("name");
MyThread myThread = new MyThread(obj);//obj不是空的传入新线程
new Thread(myThread).start();//线程不停的打印obj的name字段值
Thread.sleep(1000);
obj = null;//将外面的obj置为空后,线程打印的仍然不为空,这点需要先明确
System.out.println("2:"+obj);
Thread.sleep(1000000);
}
public void testNullPassFunc(ObjectPassing obj){
obj = new ObjectPassing();
obj.setName("zxp");
System.out.println("2:"+obj);
}
public void testNullPassFunc2(ObjectPassing obj){
obj = null;
System.out.println("2:"+obj);
}
@Data
static class ObjectPassing{
private String name;
}
运行结果:
1:null
2:ParameterPassing.ObjectPassing(name=zxp)
3:null
=======================
1:ParameterPassing.ObjectPassing(name=name)
2:null
3:ParameterPassing.ObjectPassing(name=name)
=======================
1:name
1:name
2:null
1:name
1:name
1:name
……
线程
public class MyThread implements Runnable {
ParameterPassing.ObjectPassing objectPassing = null;
public MyThread(ParameterPassing.ObjectPassing objectPassing){
this.objectPassing = objectPassing;
}
@Override
public void run() {
while (true){
System.out.println("1:"+objectPassing.getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
分析ThreadLocal与WeakReference
分析一下为什么ThreadLocal要用WeakReference 不用有什么问题
ThreadLocal local = new ThreadLocal();
local.set("当前线程名称:"+Thread.currentThread().getName());//将ThreadLocal作为key放入threadLocals.Entry中
Thread t = Thread.currentThread();//注意断点看此时的threadLocals.Entry数组刚设置的referent是指向Local的,referent就是Entry中的key只是被WeakReference包装了一下
local = null;//断开强引用,即断开local与referent的关联,但Entry中此时的referent还是指向Local的,为什么会这样,当引用传递设置为null时无法影响传递内的结果
System.gc();//执行GC
t = Thread.currentThread();//这时Entry中referent是null了,被GC掉了,因为Entry和key的关系是WeakReference,并且在没有其他强引用的情况下就被回收掉了
//如果这里不采用WeakReference,即使local=null,那么也不会回收Entry的key,因为Entry和key是强关联
//但是这里仅能做到回收key不能回收value,如果这个线程运行时间非常长,即使referent GC了,value持续不清空,就有内存溢出的风险
//彻底回收最好调用remove
//即:local.remove();//remove相当于把ThreadLocalMap里的这个元素干掉了,并没有把自己干掉
System.out.println(local);
结束语
ThreadLocal 的掌握不难
文章评论