参考:
- https://blog.csdn.net/v123411739/article/details/78698834
- https://blog.csdn.net/u010687392/article/details/50549236
- https://www.jianshu.com/p/56f64e3c1b6c
- https://www.jianshu.com/p/377bb840802f
- https://www.cnblogs.com/alex-mh/p/6761233.html
1 简介
ThreadLocal是线程本地变量的一种实现方式;线程本地变量线程自己私有,不同线程的本地变量互不影响,不存在线程安全问题;
下面是ThreadLocal简单使用
//ThreadLocal简单使用
static ThreadLocal<Object> threadLocal = new ThreadLocal<> ();//必须设置为静态属性,避免无意义的多实例
threadLocal.set(obj);//为当前线程设置一个本地变量
Object obj2 = threadLocal.get();//获取threadLocal作为key对应的本地变量
threadLocal.set(obj2);//覆盖之前的Value obj
threadLocal.remove();//移除这个本地变量,防止内存泄漏
2 存储结构
首先我们来聊一聊 ThreadLocal 在多线程运行时,各线程是如何存储变量的,假如我们现在定义两个 ThreadLocal 实例如下:
static ThreadLocal<User> threadLocal_1 = new ThreadLocal<>();
static ThreadLocal<Client> threadLocal_2 = new ThreadLocal<>();
我们分别在三个线程中使用 ThreadLocal,伪代码如下:
// thread-1中
threadLocal_1.set(user_1);
threadLocal_2.set(client_1);
// thread-2中
threadLocal_1.set(user_2);
threadLocal_2.set(client_2);
// thread-3中
threadLocal_2 .set(client_3);
这三个线程都在运行中,那此时各线程中的存数数据应该如下图所示:
![](http://ww1.sinaimg.cn/large/dbad1381ly1g3harfu2zcj20rs0jtjrj.jpg)
下图是ThreadLocal的存储结构:
![](http://ww1.sinaimg.cn/large/dbad1381ly1g3ham6u9e1j20rs0chdfq.jpg)
3. 源码分析
3.1 ThreadLocalMap简介
ThreadLocalMap 是ThreadLocal 的一个内部类,然而它并没有继承Map类,因为它只供ThreadLocal 内部使用,数据结构采用 数组 + 开方地址法;它的默认变长是16;
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry 是ThreadLocalMap 的内部类,继承自 WeakReference,所以Entry存储的key是一个ThreadLocal弱引用;所以只能自动回收弱引用的key,而强引用 value 的需要手动回收(用expungeStaleEntry()方法)。
3.2 ThreadLocalMap 之 key 的 hashCode
class ThreadLocal{
//...
//hashcode,实例化时执行
private final int threadLocalHashCode = nextHashCode();
// AtomicInteger类型,从0开始
private static AtomicInteger nextHashCode = new AtomicInteger();
// hash code每次增加1640531527
private static final int HASH_INCREMENT = 0x61c88647;
//实例化时调用
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
每生成一个ThreadLocal对象,新的hashcode就增加1640531527
;
3.3 set方法
3.3.1 ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap对象
if (map != null) // 判断map是否存在
map.set(this, value); // 调用 map 的 set 方法
else
createMap(t, value); // 创建map并插入<this,value>
}
第一次调用set时map为空,需要creatMap并插入这个键值对实体,创建方式比较简单,不详解;这里重要的还是 ThreadLocalMap 的 set 方法。
3.3.2 ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算索引位置
// hash冲突时,使用开放定址法解决冲突
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();//当前位置的key
if (k == key) { // 若当前key与传入key相同,则覆盖value
e.value = value;
return;
}
if (k == null) { // key = null,说明当前Entry是个过期Entry(key为null的Entry)
//向后找下一个插入位置并清理过期的Entry
replaceStaleEntry(key, value, i);
return;
}
}
// 当前位置为空,生成一个Entry并插入该位
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的entry,并判断是否需要扩容
rehash(); // 扩容
}
在set一个线程本地变量的过程中,先根据ThreadLocal对象的hash值,定位到Entry table中的位置i,使用开放定址法处理Hash冲突,过程如下:
- 如果当前位置Entry为空,生成一个Entry并插入该位置;
- 如果当前位置Entry不为空且当前key与传入的key相同,用新value覆盖旧value;
- 如果当前位置Entry不为空但key不同,说明hash冲突,那么只能向后找下一个位置;
3.4 get方法
3.4.1 ThreadLocal 的get() 方法
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);// 获取当前线程的 ThreadLocalMap对象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //调用map的getEntry方法
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;//获取本地变量
return result;
}
}
return setInitialValue(); //如果没有set过,返回本地变量默认值(可自定义)
}
3.4.2 ThreadLocalMap的getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);//获取key对应的索引位置
Entry e = table[i];//当前位置的Entry
if (e != null && e.get() == key) //若当前key与传入key相同,则找到目标Entry(无hash冲突情况)
return e;
else
return getEntryAfterMiss(key, i, e); //若不同,查找下一个位置(有hash冲突情况),这个方法中有清除过期Entry的操作
}
在get一个线程本地变量的过程中,先根据ThreadLocal对象的hash值,定位到Entry table中的位置i;
- 若当前key与传入key相同,则找到目标Entry(无hash冲突情况);
- 若不同,查找下一个位置(有hash冲突情况);
3.5 remove方法
3.5.1 ThreadLocal 之 remove() 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程的ThreadLocalMap对象
if (m != null)
m.remove(this); // 调用ThreadLocalMap的remove方法
}
先获取当前线程的ThreadLocalMap对象,然后调用ThreadLocalMap的remove方法移除指定threadLocal键对应的Entry
3.5.2 ThreadLocalMap的remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 根据hashCode计算出当前ThreadLocal的索引位置
int i = key.threadLocalHashCode & (len-1);
// 从位置i开始遍历,直到Entry为null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) { // 如果找到相同的key
e.clear(); // 调用clear方法, 先清空key
expungeStaleEntry(i);//后调用expungeStaleEntry方法清理过期实体
return;
}
}
}
remove指定key本地变量的过程是一个查找清理的过程,先计算当前ThreadLocal作为key对应的索引位置i,从i开始往后遍历,如果找到key相同Entry,清理掉;
remove过程也会调用expungeStaleEntry(i)
方法清理过期Entity;
set、get、remove方法,都会调用expungeStaleEntry(i)
方法清理过期Entry(key=null);
4 hash冲突
当出现不同的key相同的hashcode时就会出现hash冲突;ThreadLocalMap处理冲突的方法是开放定址法,di使用的是线性探测;
5 内存泄露及解决办法
![](http://ww1.sinaimg.cn/large/dbad1381gy1g3iegu6tbbj21280n27a2.jpg)
- 为什么?Thread的生命周期可能会比较长;value是强引用,key是弱引用生命周期短;key被GC后是空,value可能长时间(直到当前Thread运行结束)处于无法使用也无法回收的状态;
- 怎么解决?手动回收,每次使用完ThreadLocal后调用remove;
6 举例
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String> (){
@Override
protected String initialValue() {
return "not be set !";
}
};
static class MyRunnable implements Runnable{
private int num;
public MyRunnable(int num){
this.num = num;
}
@Override
public void run() {
threadLocal.set(String.valueOf(num));
System.out.println(Thread.currentThread().getName()+"'s local Value is "+threadLocal.get());
threadLocal.remove();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new MyRunnable(1));
Thread thread2=new Thread(new MyRunnable(2));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(Thread.currentThread().getName()+"'s local Value is "+threadLocal.get());
}
}
Thread-0's local Value is 1
Thread-1's local Value is 2
main's local Value is not be set !
输出验证了不同线程的本地变量互不影响;
7 Android中的ThreadLocal的不同之处
相对于原生Java的ThreadLocal,Android进行了一些细节优化,但是大致思想和使用步骤是一致的。
7.1 数据结构上的区别
- Java中:一个线程对应一个ThreadLocalMap对象,一个ThreadLocalMap对象有一个Entry数组,存放不同的key-value,key是ThreadLocalMap类型的弱引用,而value就是本地变量。
- Android中:一个线程对应一个Values对象,一个Values对象有一个Object数组,将ThreadLocalMap类型的弱引用和本地变量都存在这个数组里;ThreadLocalMap的弱引用和本地变量在数组中相邻存储。
7.2 不存在内存泄漏问题
8 总结
-
ThreadLocal是线程本地变量;不同线程的本地变量互不影响,不存在线程安全问题;
-
ThreadLocal简单使用:
//ThreadLocal简单使用
static ThreadLocal<Object> threadLocal = new ThreadLocal<> ();//必须设置为静态属性,避免无意义的多实例
threadLocal.set(obj);//为当前线程设置一个本地变量
Object obj2 = threadLocal.get();//获取threadLocal作为key对应的本地变量
threadLocal.set(obj2);//覆盖之前的Value obj
threadLocal.remove();//移除这个本地变量,防止内存泄漏
- ThreadLocal的存储结构:
![](http://ww1.sinaimg.cn/large/dbad1381ly1g3ham6u9e1j20rs0chdfq.jpg)
每一个线程都有一个ThreadLocalMap类型的threadLocals属性;而ThreadLocalMap对象持有一个Entry数组的引用,每一个Entry存储一个键值对,Key是一个ThreadLocal的弱引用(实际上是ThreadLocal的hashcode),Value是Object对象,就是本地变量。
-
ThreadLocalMap 是ThreadLocal 的一个内部类,Entry 是ThreadLocalMap 的内部类;
-
set一个本地变量的过程:先根据ThreadLocal对象的hash值,定位到Entry table中的位置i,使用开放定址法处理Hash冲突,过程如下:
- 如果当前位置Entry为空,生成一个Entry并插入该位置;
- 如果当前位置Entry不为空且当前key与传入的key相同,用新value覆盖旧value;
- 如果当前位置Entry不为空但key不同,说明hash冲突,那么只能向后找下一个位置;
-
set、get、remove方法,都会调用expungeStaleEntry(i)方法清理过期Entry(key=null);
-
hash冲突处理办法:ThreadLocalMap处理冲突的方法是开放定址法,di使用的是线性探测;
-
内存泄漏:
- 为什么?Thread的生命周期可能会比较长;value是强引用,key是弱引用生命周期短;key被GC后是空,value可能长时间(直到当前Thread运行结束)处于无法使用也无法回收的状态;
- 怎么解决?手动回收,每次使用完ThreadLocal后调用remove;
- ThreadLocal变量一定要设置为static,避免创建不必要的重复对象;
![](http://ww1.sinaimg.cn/large/dbad1381gy1g3iegu6tbbj21280n27a2.jpg)