前言:
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 适用于如下两种场景
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
1. 类的内部方法和属性
2. 简单地使用场景
ThreadLocal<T>其实是与线程绑定的一个变量。所以我们的使用方式是,通过切面登录,然后就把比如用户信息放到redis中。
然后进来的请求可以根据令牌信息然后从缓存拿用户信息,放到ThreadLocal中,方便后面直接获取这个线程所拥有的的用户信息。
public class DemoThreadLocal {
private static final ThreadLocal<HashMap> threadLocal = ThreadLocal.withInitial(() -> new HashMap());
// public static void setMap(HashMap map) {
// threadLocal.set(map);
// }
private static HashMap getMap() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
public static String getCurGlobalRequestId() {
return null == getMap().get("globalRequestId") ? "" : (String) getMap().get("globalRequestId");
}
public static void setCurGlobalRequestId(String globalRequestId) {
getMap().put("globalRequestId", globalRequestId);
}
}
其实就是相当于吧ThreadLocal当做一个线程隔离的变量 map来使用。
public class Test {
private static final ThreadLocal<HashMap> threadLocal = ThreadLocal.withInitial(() -> new HashMap());
public static void main(String[] args) {
threadLocal.get();
threadLocal.get().put("a","a");
threadLocal.get().put("b","b");
System.out.println(threadLocal.get().get("a"));
Thread t = Thread.currentThread();
}
}
一般来说有remove,get 和set比较常用。
这边也是用了HashMap来存取一些比如用户信息之类的。
ThreadLocal本身是跟并发有关的,但是很多情况确实如上述为了方便传参使用的。
每一个ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。
ThreadLocal应该尽量设计在一个全局的设计上,不应该是一种打补丁的间接方法。
比如:
Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了当中的getConnection()方法,或者说该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。
为什么要放在ThreadLocal里面呢?由于Spring在AOP后并不能向应用程序传递參数。应用程序的每一个业务代码是事先定义好的,Spring并不会要求在业务代码的入口參数中必须编写Connection的入口參数。此时Spring选择了ThreadLocal,通过它保证连接对象始终在线程内部,不论什么时候都能拿到,此时Spring很清楚什么时候回收这个连接,也就是很清楚什么时候从ThreadLocal中删除这个元素
向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。(下面源码分析时候发现set其实会把Thread中的ThreadLocalMap属性当做一个副本)
一般的Web应用划分为控制层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
所以场景中,可以:
// 非线程安全
public class TopicDao {
//①一个非线程安全的变量
private Connection conn;
public void addTopic(){
//②引用非线程安全变量
Statement stat = conn.createStatement();
…
}
}
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
//①使用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
//②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
//并将其保存到线程本地变量中。
if (connThreadLocal.get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}else{
//③直接返回线程本地变量
return connThreadLocal.get();
}
}
public void addTopic() {
//④从ThreadLocal中获取线程对应的
Statement stat = getConnection().createStatement();
}
}
可以让多个Dao共用一个Connection,同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
交给线程来管理,那么这个也是Spring对有状态类线程安全化的解决思路。
3. 源码分析
3.1 set(T value)
这里面涉及到了Thread源码,还有ThreadLocalMap作为ThreadLocal的内部类。后面再读一下。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. 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 the value to be stored in the current thread's copy of
* this thread-local.
* 常用可能用于让线程存储value
*/
public void set(T value) {
//找到当前线程
Thread t = Thread.currentThread();
//从ThreadLocalMap中 获取这个t,也可以说这个是每个Thread类都会保存的一个ThreadLocalMap
ThreadLocalMap map = getMap(t);
//没有从Thread类中取得到map
if (map != null)
map.set(this, value);
else
//初始化这个map
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
/**
* 这个Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;
* */
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* 初始化操作
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.2 get()
返回当前线程所对应的线程局部变量。
如果一个线程第一次调用threadLocal.get()方法时,此时拿到的map是null,会调用setInitialValue()
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//当前线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//map的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获得result
T result = (T)e.value;
return result;
}
}
//如果为空
return setInitialValue();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
//这边应该是返回一个空值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
/**
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
/**
* 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;
}
这边读的时候有一个很困惑的点,如果get它发现没有初始化这个ThreadLocalMap的话,它会放入一个空值,
然后放到ThreadLocalMap里面并初始化。
原因是因为,这个value是可以重写的,如果我们没有重写,那么会返回一个null。
T value = initialValue();
比如(借用了深入学习java源码之ThreadLocal.get()()与ThreadLocal.initialValue()的代码):
public class test {
public static void main(String[] args) throws InterruptedException {
new A().start();
new A().start();
new A().start();
new A().start();
}
static class A extends Thread {
static List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<List<Integer>>() {
@Override
protected List<Integer> initialValue() {
return list;
}
};
@Override
public void run() {
List<Integer> threadList = threadLocal.get();
threadList.add(threadList.size());
System.out.println(threadList.toString());
}
}
}
输出结果是:
[1, 2, 3, 4, 5, 5]
[1, 2, 3, 4, 5, 5, 6]
[1, 2, 3, 4, 5, 5, 6, 7]
[1, 2, 3, 4, 5, 5, 6, 7, 8]
所以,这个方法其实更多是为了让我们去重载这个get的初始化的。
3.3 remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用。
更为了防止内存泄漏。
当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
//获取map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 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(ThreadLocal的内部类)类中的remove方法:
/**
* Remove the entry for key.
*/
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();
expungeStaleEntry(i);
return;
}
}
}
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;
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;
}
相当于移除一个,ThreadLocal<?> key。
一个线程可以有多个ThreadLocal,根据不同的key作为区分。
expungeStaleEntry当在搜索过程中遇到了脏entry的话就会调用该方法去清理掉脏entry。
具体可以读读:从源码深入详解ThreadLocal内存泄漏问题
3.4 initialValue()
protected T initialValue() {
return null;
}
4. 问题?
4.1 子线程能否访问父线程的threadlocal呢?
threadLocal可以做线程级的数据隔离,那如何在子线程中获取父线程的值呢? 可以使用InheritableThreadLocal
说明在子线程和孙线程中可以获取到父线程的 inheritableThreadLocal 的值。修改inheritableThreadLocal 的值后,子线程和孙线程中同样可以获取到父线程的inheritableThreadLocal 的值。
但是threadLocal却不行,获取到的值为空。
public class Test {
public static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
public static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
inheritableThreadLocal.set("inheritableThreadLocal hello");
threadLocal.set("threadLocal world");
new Thread(()->{
System.out.println(String.format("子线程可继承值:%s", inheritableThreadLocal.get()));
System.out.println(String.format("子线程值:%s", threadLocal.get()));
new Thread(()->{
System.out.println(String.format("孙线程可继承值:%s", inheritableThreadLocal.get()));
System.out.println(String.format("孙线程值:%s", threadLocal.get()));
}).start();
}).start();
}
}
执行结果:
子线程可继承值:inheritableThreadLocal hello
子线程值:null
孙线程可继承值:inheritableThreadLocal hello
孙线程值:null
4.2 内存泄漏?
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生 命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收, Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key,无论key是强引用与弱引用,都会导致内存泄漏。但是使用弱引用可以多一层保障:弱引用能保证ThreadLocal对象能够保证被回收
内存泄露问题解决办法:
存在内存泄露问题,每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
参考: