ThreadLocal
ThreadLocal(线程变量).每个线程获取与填充都操作当前线程对应的变量副本. 不同线程间的变量副本是隔离开的.
csharp
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("main");
new Thread(()->{
threadLocal.set("thread1");
}).start();
new Thread(()->{
threadLocal.set("thread2");
System.out.println("thread2:"+threadLocal.get());
}).start();
System.out.println("main:"+threadLocal.get());
}
控制台:
thread2:thread2
main:main
从功能上来ThreadLocal或许是一个容器,key是线程id,value是线程的拥有的对象数据. 如果我设计的话可能ThreadLocal会设计成上述这种.
MyThreadLocal:
ini
public class MyThreadLocal<T>{
Map<Thread, T> local= new HashMap<Thread, T>();
T get(){
Thread thread = Thread.currentThread();
T o = local.get(thread);
return o;
}
void set(T t){
Thread thread = Thread.currentThread();
local.put(thread, t);
}
}
csharp
public static void main(String[] args) throws InterruptedException {
MyThreadLocal<String> threadLocalUtil = new MyThreadLocal<>();
threadLocalUtil.set("main");
System.out.println(threadLocalUtil.get());
new Thread(()->{
threadLocalUtil.set("thread2");
System.out.println("thread2:"+threadLocalUtil.get());
}).start();
}
来看看这种实现的缺陷:
-
ThreadLocal就是为多线程所设计的,所以一定涉及并发问题.HashMap线程并不是安全的,所以如果这样实现也要用ConcurrentHashMap<Thread, String> map = new ConcurrentHashMap<>();
-
ThreadLocal的思想是每个线程持有自己的副本,不存在竞争关系. 上面的实现无论是用hashMap,还是ConcurrentHashMap,或者是锁 都会造成资源的竞争.
-
如果在主线程中new MyThreadLocal();. 只要主线程一直存在,MyThreadLocal引用就一直在,即使使用过MyThreadLocal的子线程已经结束了 .map容器中子线程持有的内容也不会被GC掉. local对象会越来越大.
点进原码看看真正的ThreadLocal是怎么设计的:
- ThreadLocalMap: ThreadLocalMap是ThreadLocal的静态内部类,继承了WeakReference的一个map结构.(WeakReference:弱引用只要发生的gc 对象就会被回收.具体可以看下blog.csdn.net/javaphpsqlm...)
- threadLocals: ThreadLocal的私有对象,类型是ThreadLocalMap. 数据都存放在各自线程的私有对象的这个map里边,所有称之为数据副本,线程间互相隔离.
从set方法开始,线程首次调用ThreadLocal的set方法时threadLocals是空的,走到createMap. new了一个ThreadLocalMap.
用ThreadLocal对象set值:
scss
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
获取当前线程的私有对象threadLocals
javascript
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
创建一个map 并将值存到这个map中
javascript
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
new Entry(firstKey, firstValue)可以看到这个map的key就是ThreadLocal对象本身. value就是需要保存的值 key是ThreadLocal对象本身是因为每个线程可以使用多个ThreadLocal去保存值(
- ThreadLocal threadLocal0 = new ThreadLocal<>();
- ThreadLocal threadLocal1 = new ThreadLocal<>();
- threadLocal0.set(int)
- threadLocal0.set(string)
)
ini
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
set方法搞清楚了 get方法也都能想到.就是去thread的私有对象threadLocalMap中通过当前调用的threadLocal对象作为key取值.
总体了解了ThreadLocal我们再回顾看下实现细节
为什么要继承弱引用WeakReference.
scala
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
那我们反过来看下强引用会发生什么,我们系统中可能会持续运行多个线程,系统中的线程又大部分来源于线程池的复用,线程的周期很长.每个线程中都运行如下代码ThreadLocalMap会存放了大量数据.
csharp
public void systemOut() {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("BIG_DATA");
System.out.println(threadLocal.get());
}
方法执行结束了 jvm的栈中threadLocal对象已经没了. 但是ThreadLocalMap仍然持有key和"BIG_DATA"的强引用, 所有不会被GC掉.ThreadLocalMap数据就会越来越大
Entry继承了WeakReference就是为了解决这个问题,把key设置成弱引用.一旦threadLocal的强引用消失,下一次垃圾回收ThreadLocalMap中的key就会被gc掉.注意这里只有key是弱引用. value不是,value不会被gc.
后续get set时会触发一些清除操作,将key为null的value remove掉. 但是这个并不是固定的,在使用ThreadLocal时大家还是要在不需要的时候手动remove掉,尽可能的阻止内存溢出.

从图来看会清晰一点,看下弱引用的作用就是为了当threadLocal为null也就是强引用1不存在的时候.能够吧key1垃圾回收起来
来思考个小问题
value不会被gc可能会造成内存溢出, 为什么不参考key把key、value都搞成弱引用.没有强引用时都给Gc掉
csharp
public static void main(String[] args) {
//假设key value都是弱引用
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("main");
new Thread(()->{
threadLocal.set("thread1");
}).start();
//模拟jvm发生了垃圾回收
System.gc();
System.out.println("main:"+threadLocal.get());
}
控制台:
main:null
上面的代码如果value也是弱引用的话,threadLocal.get()取出来的就是空的了. 因为main方法没结束ThreadLocal threadLocal对象的引用还在栈中,key不会被回收. 但是value也就是字符串"main"没强引用指向他了直接被垃圾回收了!
下面我们继续看下用于储存的数据结构ThreadLocalMap中的细节
ThreadLocalMap的内部类:Entry
scala
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
ThreadLocalMap的构造函数
-
new一个初始大小为Entry16数组
-
计算key的hashCode与数组长度与运算得到下标. threadLocalHashCode里边的内容大致和.hashCode差不多,只不过threadLocalHashCode的散列性更好
-
设置大小为初始值1
-
设置阀值,调整大小阈值以在最坏情况下保持2/3的负载系数。
ini
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
arduino
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
hash算法大概是每次使用AtomicInteger原子性的的增加0x61c8864.能够使生成的哈希值足够分散
arduino
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocal的set方法
- Entry e = tab[i]; e != null;命中了这个条件说明尽管hash足够分散仍然发生了hash碰撞.
- e = tab[i = nextIndex(i, len)]通过线性寻址法找到下一个空的位置.把数据存进去 e.value = value;
- 线性寻址set和get都会用到.在寻址的过程中会识别到null key的数据,就是前边提到的弱引用.将key为空的元素删除,其余数据向前移动.
ini
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
线性寻址:当前位置有冲突,就去按顺序依次寻找当前位置+1、+2、+3的地方是否是空的, 如果到了数组的末尾就从0开始直到找到空余位置. 线性寻址是最为简单直接的hash冲突解决方式.其他还有二次寻址、链表、双重散列等.
arduino
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
ThreadLocal没有继承关系,也就是子线程拿不到父线程的副本. InheritableThreadLocal补充了这个功能
csharp
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("main");
new Thread(()->{
System.out.println("thread2:"+inheritableThreadLocal.get());
}).start();
控制台输出 main
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("main");
new Thread(()->{
//覆盖父线程内容
inheritableThreadLocal.set("thread2");
System.out.println("thread2:"+inheritableThreadLocal.get());
}).start();
控制台输出 thread2
InheritableThreadLocal继承了ThreadLocal 重写了createMap等方法.inheritableThreadLocals取代了ThreadLocal中的对象
javascript
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
看一眼Thread的构造函数 ,其中有段逻辑是判断当前线程inheritThreadLocals是否有值有的话调用ThreadLocal.createInheritedMap批量复制过去
csharp
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
ini
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
因为是创建线程时复制进去的,在子线程new出来之后,父线程向inheritableThreadLocal录入的数据子线程是不会拿到的
csharp
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("thread2:"+inheritableThreadLocal.get());
}).start();
inheritableThreadLocal.set("main");
控制台:
thread2:null
InheritableThreadLocal的应用:
- 子线程需要父线程的登录态信息
- 日志链路id等