共计 8637 个字符,预计需要花费 22 分钟才能阅读完成。
1. 是什么?
首先 ThreadLocal
类是一个线程数据绑定类, 有点类似于HashMap<Thread, 你的数据 >
(但实际上并非如此), 它所有线程共享, 但读取其中数据时又只能是获取线程自己的数据, 写入也只能给线程自己的数据
2. 怎么用?
public class ThreadLocalDemo {private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {threadLocal.set("zhazha" + Thread.currentThread().getName());
String s = threadLocal.get();
System.out.println("threadName =" + Thread.currentThread().getName() + "[ threadLocal =" + threadLocal + "\t data =" + s + "]");
}, "threadName" + i).start();}
}
}
从他的输入来看, ThreadLocal
是同一个, 数据存的是线程自己的名字, 所以和 threadName
是一样的名称
threadName = threadName9 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName9]
threadName = threadName3 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName3]
threadName = threadName7 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName7]
threadName = threadName0 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName0]
threadName = threadName6 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName6]
threadName = threadName1 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName1]
threadName = threadName2 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName2]
threadName = threadName4 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName4]
threadName = threadName5 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName5]
threadName = threadName8 [threadLocal = java.lang.ThreadLocal@43745e1f data = zhazhathreadName8]
3. 有什么使用场景
我们使用获取到一个保存数据库请求, tomcat 会有一个线程去操作数据库保存数据和响应数据给客户, 而操作数据库需要存在一个数据库链接 Connection
对象, 只要是同一个数据库链接, 就可以得到同一个事务
但一个线程是如何获取同一个 Connection
从而获取同一个事务 ?
方法其实很简单, 使用 ThreadLocal
绑定在线程中, 类似于 Map<Thread, Connection>
去存储
4. 底层源码分析
get
方法分析
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 ThreadLocalMap
ThreadLocal.ThreadLocalMap map = getMap(t);
// map 不为 null
if (map != null) {
// 根据 this 获取我们的 entry
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果 map 获取为空, 则初始化
return setInitialValue();}
根据上面源码分析发现 ThreadLocal
底层使用的不是类似 Map<Thread, Data>
这种结构而是
每个线程都有一个属于自己的 ThreadLocalMap
结构
而他的结构是这样的
其中的 table
数组在上面的 setInitialValue()
方法创建详细源码在这
private T setInitialValue() {
// 这个方法在我们的用例中没写, 所以默认放回 null
T value = initialValue();
Thread t = Thread.currentThread();
// 获取线程单独的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {// 如果我们初始化了 initialValue() 方法, 那么它默认初始化的值会被设置到这里,
// 但是实际上我们用例为 null, 所以不会执行这段代码
map.set(this, value);
} else {
// 线程 ThreadLocalMap 没被创建, 需要创建出来,
// 其中的 table 数组在这里被创建
createMap(t, value);
}
// 这里我没分析, 忽略了
if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
他会在 ThreadLocalMap
中调用构造方法初始化
// 其中 firstValue 是我们的值
void createMap(Thread t, T firstValue) {
// 关注下 this , 它是 ThreadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 我们的 table 在这里被创建, INITIAL_CAPACITY == 16
table = new Entry[INITIAL_CAPACITY];
// 获取不超过 16 的 hashCode
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 根据计算出来的 HashCode 设置到对应的 table 数组中, 这里 key 是 ThreadLocal, value 是我们的值
table[i] = new Entry(firstKey, firstValue);
// 初始时, 已经有一个值了, 所以 size = 1
size = 1;
// 设置扩容阈值加载因子 threshold = len * 2 / 3; 默认为长度的三分之二
setThreshold(INITIAL_CAPACITY);
}
从这段代码可以发现, firstKey
其实是我们 ThreadLocalMap
中的 key
, 而firstKey
就是我们的 ThreadLocal
, 而value
就是我们 initialValue()
方法返回的值, 这里默认为null
, 所以我们可以得出这样一幅图
总结下
每个线程都有一个属于自己的ThreadLocalMap
类, 他用于关联多个以ThreadLocal
对象为key
, 以你的数据
为value
的Entry
对象, 且该对象的key
是一个弱引用对象
接下来我们分析下这个类Entry
, 它继承了弱引用类WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
// ThreadLocal 被设置为弱引用
super(k);
// 保存 value
value = v;
}
}
发现 ThreadLocal
被设置为弱引用
存在什么问题?
为什么前面的 Entry
需要继承弱引用类 WeakReference
呢?
首先了解下什么是引用
简单了解下强、软、弱和虚引用
- 强引用: 如果引用变量没被指向 null 则, 引用对象将被停留在堆中, 无法被虚拟机回收
Object obj = new Object()
- 软引用: 如果虚拟机堆内存不够用了 (在发生内存溢出之前), 虚拟机可以选择回收软引用对象, 虚拟机提供
SoftReference
类实现软引用, 一般用于相对比较重要但又可以不用的对象, 比如: 缓存 - 弱引用: 生于系统回收之前, 死于系统回收完毕之后, 弱引用需要依附于强引用或者软引用才能够防止被虚拟机回收, 比如放到一个引用队列 (
ReferenceQueue
) 中或者对象中, 比如:ThreadLocalMap
的Entry
对象, 需要依附于ThreadLocal
才能够不被删除掉 - 虚引用: 可以理解为跟强引用对象没了引用变量一样, 随时可以被回收, 只要依附于引用队列中才不会被回收, 通常用于网络通讯的
NIO
上, 用于引用直接内存, java 提供类PhantomReference
来实现虚引用
为何 Entry
对象需要为弱引用?
答案很明显, 防止内存泄漏 [1], 我们来详细分析分析
首先, 我们知道 ThreadLocalMap
中存放的是一个一个 Entry
对象, 而 Entry
对象中的 key
(ThreadLocal
) 被设计成弱引用如果 key
被设置成 null
(比如: 外部的测试用例中的private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
这个对象被设置为 threadLocal = null
) 则, 你会发现此时 Entry
的对象 key = null value = xxxx
(此时这个Entry
实质上是没有用的, 连 key
都给设置成 null
, 它的value
还有什么用?) 而 ThreadLocalMap
中存储的还是 Entry
对象的地址, 此 Entry
不会被回收, 但 Entry
对象的 key
被设置成弱引用, 就不一样了, 直接会被回收掉它
[1]内存泄漏: 程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
那么这样就没有问题了么???(打脸篇)
再次强调, 下面这段话别信, 仔细看到最后, 你会发现这被打脸了
其实应该是没什么问题了 (__被自己打脸了, 别信这句话__), 只不过很多网友觉得Entry
中的 key
虽然是弱引用, 但 Entry
可能不会被回收, 因为 entry
的value
是强引用, 可能导致线程下的 entry
无法被回收掉, 最好推荐使用 threadLocal.remove
方法删除掉, 前面说的 threadLocal = null
方法不推荐使用, 那么为了以防万一吧, 还是手动调用下 remove
方法比较好一点
下面是我对 threadLocal = null
方式的代码测试:
public class ThreadLocalDemo {private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
@Override
protected String initialValue() {return "1";}
@Override
protected void finalize() throws Throwable {super.finalize();
System.out.println("threadLocal1 被回收");
}
};
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
@Override
protected String initialValue() {return "2";}
@Override
protected void finalize() throws Throwable {super.finalize();
System.out.println("threadLocal1 被回收");
}
};
public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
// 获取 ThreadLocalMap
Thread thread = Thread.currentThread();
Class<? extends Thread> clazz = thread.getClass();
Field threadLocals = clazz.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object threadLocalsObj = threadLocals.get(thread);
// 获取 ThreadLocalMap 下的 table 数组
Class<?> threadLocalsMapClass = threadLocalsObj.getClass();
Field tableField = threadLocalsMapClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] tableObj = (Object[]) tableField.get(threadLocalsObj);
threadLocal1.set("zhazha");
threadLocal2.set("xixi");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
// 在这里下一个断点看看 ThreadLocal 被回收, Entry 是否被回收
threadLocal1 = null;
threadLocal2 = null;
System.gc();
Thread.sleep(5000);
System.out.println(tableObj);
System.out.println("主线程结束");
}
}
输出是这样的:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.zhazha.threadlocal.ThreadLocalDemo (file:/D:/program/codes/java/Concurrentcy/reviewjuc/target/classes/) to field java.lang.Thread.threadLocals
WARNING: Please consider reporting this to the maintainers of com.zhazha.threadlocal.ThreadLocalDemo
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
zhazha
xixi
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@aecb35a
主线程结束
如果上面的代码不调用 gc
方法, 很长一段时间内不会被回收, 应该是 jvm gc
还没开始被动回收
但!!! 但!!! 但!!! 看调试代码
数组中的 referent
字段还是存在的, 下图是 gc
回收之前查看数组中的元素发现, 字段referent
(也就是ThreadLocal
) 它还在
在 gc
方法执行完毕后, referent
被回收掉了, referent = null
了
但是那个对象怎么回事??? 没被回收掉?? 打脸了??? 求助广大网友给我看看
那让我们试试 remove
方法试试?
好了, 直接没了, 找不到那两个属性了
这 An illegal reflective access operation has occurred
这个问题怎么帮? 这回真不知道了, 应该不影响我们的代码么?
算了为了把这个红色的字改没掉, 改了改源码
public class ThreadLocalDemo {private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
@Override
protected String initialValue() {return "1";}
@Override
protected void finalize() throws Throwable {super.finalize();
System.out.println("threadLocal1 被回收");
}
};
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
@Override
protected String initialValue() {return "2";}
@Override
protected void finalize() throws Throwable {super.finalize();
System.out.println("threadLocal1 被回收");
}
};
private static Unsafe unsafe;
static {
Class<Unsafe> unsafeClass = Unsafe.class;
Unsafe unsafe = null;
try {Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
ThreadLocalDemo.unsafe = (Unsafe) unsafeField.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException, NoSuchFieldException {Thread thread = Thread.currentThread();
long threadLocalsFieldOffset = unsafe.objectFieldOffset(Thread.class.getDeclaredField("threadLocals"));
Object threadLocalMapObj = unsafe.getObject(thread, threadLocalsFieldOffset);
long tableOffset = unsafe.objectFieldOffset(threadLocalMapObj.getClass().getDeclaredField("table"));
Object tableObj = unsafe.getObject(threadLocalMapObj, tableOffset);
threadLocal1.set("zhazha");
threadLocal2.set("xixi");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal1 = null;
threadLocal2 = null;
// threadLocal1.remove();
// threadLocal2.remove();
System.gc();
System.out.println(tableObj);
System.out.println("主线程结束");
}
}
好了没这个问题了
zhazha
xixi
threadLocal1 被回收
threadLocal1 被回收
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@7dc222ae
主线程结束
与目标 VM 断开连接, 地址为: ''127.0.0.1:58958',传输: '套接字'', 传输: '{1}'
进程已结束, 退出代码 0