ThreadLocal
解决数据一致性的问题通常有几种方式(笔者理解为,进程内出现线程不安全的问题也是导致了数据不一致):
- 排队,典型的案例是
synchronized
和Lock
. - 线程本地变量——
ThreadLocal
. - 投票,可以了解一下著名的
paxos
算法
ThreadLocal
可以让线程只访问自己线程的变量,避免了发生线程安全问题.同时,它对操作系统的开销更小,同步往往需要消耗操作系统的内核资源;但如果是ThreadLocal,它只需要内存进行存储即可。
代码示例
package com.tea.modules.java8.thread.threadLocal;
public class Test {
//ThreadLocal
public static ThreadLocal x = ThreadLocal.withInitial(() -> {
// 延迟加载,只在第一次get的时候进行初始化
System.out.println(Thread.currentThread().getId() + "initialValue run...");
return Thread.currentThread().getId();
});
public static void main(String[] args) {
x.get();
// ThreadLocal为每一个线程存储一个独立的变量
new Thread(() -> {
x.set(107L);
System.out.println(x.get());
}).start();
// ThreadLocal为每一个线程存储一个独立的变量
new Thread(() -> {
x.set(108L);
System.out.println(x.get());
}).start();
// 清空当前线程的ThreadLocal的值
x.remove();
}
}
- 输出结果
1initialValue run...
107
108
可以看到,同时开启了2个线程,分别对x变量进行设值,输出的都是各自设置的值,说明threadlocal是"线程隔离"的,可以保证线程安全.
ThreadLocal实现原理
Thread中的threadLocals
在Thread类中,有个属性叫threadLocals
,它的类型是ThreadLocal.ThreadLocalMap
.ThreadLocalMap是定制化的HashMap,它负责存储ThreadLocal设置的值.
也就是说,实际上ThreadLocal的set过程是这样的:
这里,ThreadLocal充当的是Key的作用,也就是引用,真正的值存放线程对象的内存空间里面.
- java.lang.ThreadLocal#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) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- 首先获取当前线程对象
- 获取对应的threadLocals
- 根据ThreadLocal获取map里面的值
- 如果获取到返回对象
- 如果获取不到,会根据当前threadLocals是否为空觉得是否进行初始化.
remove过程
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
- 首先获取当前线程对象
- 获取对应的threadLocals
- 清空当前ThreadLocal引用的值
ThreadLocal容易引发的问题
内存泄露
ThreadLocal其实是操作Thread中的threadLocals,如果当前线程不消亡,那么这些本地变量会一直存在,可能会造成内存溢出,因此最好的建议是,每次用完ThreadLocal我们都手动执行remove操作。
内存泄漏,程序申请内存后,没有释放已申请的内存空间,这部分空间的堆积终将导致内存溢出。
这里会涉及到ThreadLocalMap的设计,我们来看看它的Entry:
- java.lang.ThreadLocal.ThreadLocalMap.Entry
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
WeakReference,是弱引用的意思,当一个对象仅仅被WeakReference指向, 而没有任何其他strongReference指向的时候, 如果GC运行, 那么这个对象就会被回收。如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象。
假设我们没有主动调用remove方法, 那么回收过程可能是这样的:
GC回收只回收了ThreadLocal引用,而value值仍未从内存空间中清理出去
因此,最妥当的方法还是手动调用remove方法. 因为我们的项目往往采用线程池(如果是tomcat容器也有所谓的工作线程),线程往往是循环利用的。
在多线程环境下,不支持继承性
有这么一个应用场景,如果我们希望在两个线程之间去使用ThreadLocal进行传值,ThreadLocal是不支持的.
public static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("hello world");
new Thread(() -> {
System.out.println("thread:" + threadLocal.get());
}).start();
System.out.println("main:" + threadLocal.get());
}
因为ThreadLocal
只绑定当前线程,那么在这种情况下,我们又希望有个东西能支持多线程之间去共享值,怎么做?——InheritableThreadLocal
.
public static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("hello world");
new Thread(() -> {
System.out.println("thread:" + threadLocal.get());
}).start();
System.out.println("main:" + threadLocal.get());
}
InheritableThreadLocal
提供了子线程去访问父线程ThreadLocal的能力.
public class InheritableThreadLocal extends ThreadLocal {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
*
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
从类的定义上看,InheritableThreadLocal
绑定的,是Thread类的inheritableThreadLocals
属性.
那么值是什么时候设置进这个inheritableThreadLocals
变量的,我们继续看看Thread类的代码:
- java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
Thread parent = currentThread();
// 省略
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 省略
}
init方法的触发时机是在每个线程创建的阶段,他首先会获取当前线程对象(父线程),然后判断当前线程对象的inheritableThreadLocals
是否为空,如果不为空,将父线程的inheritableThreadLocals
包装为Map结构赋值给即将创建的线程的inheritableThreadLocals
变量.
结束了么
学而不思则罔,我们学习了技术,看到了JDK的一些底层实现逻辑,但是实际上项目应用,我们是否真正能用到这些东西,以下是我经常遇到的场景:
- 多线程环境下,线程会被复用,
InheritableThreadLocal
是否也有问题,比如说,他只是存储了创建线程的那一刻的快照值,那么在后面的事件中,如果值发生变化,我们怎么去跟踪? - 分布式环境下,ThreadLocal能给我们怎样的参考,比如在rpc中我们需要将一些信息进行传递,这些信息能否也有一个
rpcContext
的东西进行传递? - 中间件框架对于ThreadLocal的一些用法,比如Spring的是怎么处理事务的、Mybatis是如何复用连接的,等等.
博主目前还没能完全参透这些,但是有一些参考资料,希望能给到大家一些灵感:
- TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了
- 吊打 ThreadLocal!
- ThreadLocal系列之——父子线程传递线程私有数据(四)