一、概述
ThreadLocal是线程本地变量,解决多线程环境下成员变量共享存在的问题。ThreadLocal为每个线程创建独立的的变量副本,他的特性是该变量的引用对全局可见,但是其值只对当前线程可用,每个线程都将自己的值保存到这个变量中而各线程不受影响。
二、原理
1.原理说明
ThreadLocal的大致原理是这样的:
- 每个线程里面就有一个ThreadLocalMap的对象引用,ThreadLocalMap对象的数据结构是Entry组成的Map,其key值是ThreadLocal,而value值才是存放在该线程需要保存的value;
- 调用ThreadLocal的set()方法就往ThreadLocalMap里里面存值,value就是存入的值;
- 调用ThreadLocal的get()方法就是从ThreadLocalMap里面根据key值来取value;
ThreadLocal在JVM详细的逻辑图见下:
我的理解是ThreadLocal其实是为每一个线程创建一个副本变量,对于每个线程来说其变量引用相同,但其值不同。最简单的思路是创建一个Map,其key值是线程id,其值是各个线程的副本value,但是由于已经明确变量引用相同,所以该Map的key即为ThreadLocal本身,将其key和Value组合成一个实体Entry,放到其特定的ThreadLocalMap中,这就是ThreadLocal的原理。简而言之就是存放副本的数据结构ThreadLocalMap存在双头领导,一头已经释放,但另一头由于存活时间很长,所以很难释放对ThreadLocalMap的引用,从而导致ThreadLocalMap无法被及时GC。
2.代码分析
Thread中的成员变量threadLocals:
//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap的数据结构:
//ThreadLocal.ThreadLocalMap
static class ThreadLocalMap {
//实际保存数据的数据结构为Entry,其key为ThreadLocal的引用,value为各个线程需要保存的值
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
//成员变量
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
//省略...
}
总的来说,ThreadLocalMap是保存多线程多副本的数据结构,Thread会保存ThreadLocalMap的引用,ThreadLocal也会保存ThreadLocalMap的引用。
三、实践
1.常用方法
ThreadLocal种最常见的呃是set()和get()方法,下面讲解下这两个方法;
- set()方法
set()方法,顾名思义就是往ThreadLocal种塞值的,每个线程塞的值只对自己的线程可见;
- get()方法
get()方法和set()方法对应,从ThreadLocal种取值出来;
- initialValue()方法
设置ThreadLocal中变量的默认值;
- remove()
移除该线程在ThreadLocal中的变量副本;
2.使用场景
ThreadLocal的使用场景包括多个线程要调用同一个资源,但该资源内部要为每一个线程分配不同资源的情况,比如数据库连接,在建立数据库连接时候,只需要建立一个datasource,通过这一个datasource可以为不同的请求建立connection连接;
3.代码实践
创建多个线程,分别验证通过set()方法设置值和get()获取值,并打印给ThreadLocal设置的默认值。
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected Integer initialValue() {
return 0;
}
};
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(1);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(2);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}
});
t1.start();
t2.start();
t3.start();
}
}
四、常见问题
1.碰到get()方法返回null的处理方法?
可以通过extends ThreadLocal并且重写initialValue()来实现初始化;
2.ThreadLocal内存泄露的解决方法?
从上面的逻辑图可以看到,ThreadLocalMap是在Thread里面的,所以ThreadLocalMap和Thread的生命周期是一样长的,但是ThreadLocalMap又被ThreadLocal引用,即使ThreadLocal已经使用完,但由于Thread没有将其引用释放,所以ThreadLocalMap还是不会被GC掉,这样很容易导致OOM,处理方法是线程在不用ThreadLocal的时候记得remove掉;
参考资料
- ThreadLocal就是这么简单:https://juejin.cn/post/6844903586984361992
- Java 并发 - ThreadLocal详解:https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html
- 对ThreadLocal实现原理的一点思考:https://www.jianshu.com/p/ee8c9dccc953
- Java多线程编程-(8)-多图深入分析ThreadLocal原理:https://blog.csdn.net/xlgen157387/article/details/78297568
- 《Java多线程编程核心技术》
- 《码出高效java代码》