ThreadLocal使用
ThreadLocal是一项可为每个线程存储本地变量的技术,它在Java和Android中都有非常广泛的应用,例如Android开发者最熟悉不过的Handler,内部就使用到了ThreadLocal。本文主要讲述在Java8环境中ThreadLocal的使用方法、工作原理和常见问题。
使用方法
ThreadLocal在使用时每个线程都使用同一个ThreadLocal实例进行 get
/ set
操作,但是其获取和修改的数据在每个线程中都是不一样的。
fun main(args: Array<String>) {
val threadLocalName= ThreadLocal<String>()
val threadLocalAge = ThreadLocal<Int>()
threadLocalName.set("张三")
threadLocalAge.set(18)
println("name:${threadLocalName.get()} -- 主线程")
println("age:${threadLocalAge.get()} -- 主线程")
// 子线程中
thread {
threadLocalName.set("李四")
threadLocalAge.set(22)
println("name:${threadLocalName.get()} -- 子线程")
println("age:${threadLocalAge.get()} -- 子线程")
}
}
// 运行后的打印信息
name:张三 -- 主线程
age:18 -- 主线程
name:李四 -- 子线程
age:22 -- 子线程
可以看出在主线程和子线程中 threadLocalName
和 threadLocalAge
对应的值是不一样的,单纯看上面的代码和打印情况有点反直觉,下面用一张图展示当前两个线程的情况。
附上一段查看各个Thread内ThreadLocalMap数据的代码,因为ThreadLocalMap、Entry都是内部类,且Thread和ThreadLocal都没有提供直接获取ThreadLocalMap数据的api,所以这里使用反射的方法获取到Entry[]的数据。(这个方法在Java8环境下可正常运行,在Java9及以上环境上运行会有警告,故以下代码不建议在生产环境中使用)
fun main(args: Array<String>) {
val threadLocalName = ThreadLocal<String>()
val threadLocalAge = ThreadLocal<Int>()
// 打印出两个ThreadLocal对象
println("name: $threadLocalName")
println("age: $threadLocalAge")
// 主线程设置为: 张三 18岁
threadLocalName.set("张三")
threadLocalAge.set(18)
printThreadLocalsInfo()
// 子线程设置为: 李四 22岁
thread(name = "sub") {
threadLocalName.set("李四")
threadLocalAge.set(22)
printThreadLocalsInfo()
}
}
fun printThreadLocalsInfo() {
val curThread = Thread.currentThread()
printGray("\n\n-------- 工作线程 ${curThread.name} --------\n")
// 先获取到线程内部的threadLocals
val threadLocalsField = Thread::class.java.getDeclaredField("threadLocals")
threadLocalsField.isAccessible = true
val threadLocals = threadLocalsField.get(curThread)
printBrown("threadLocals = $threadLocals\n")
// 获取ThreadLocalMap的Entry[] table
val tableField = threadLocals.javaClass.getDeclaredField("table")
tableField.isAccessible = true
// 因为Entry是个内部类,所以强转的时候转为Object
val table = tableField.get(threadLocals) as Array<Object?>
// 循环打印出所有不为空的Entry
table.forEachIndexed { index, entry ->
if (entry != null) {
println("table[$index] = $entry")
// 获取出Entry的key和value
val entryClass = entry.javaClass
// Entry是一个继承WeakReference<ThreadLocal<?>>的类
// get方法获取到的就是我们的ThreadLocal对象
val keyGetMethod = entryClass.getMethod("get")
val valueField = entryClass.getDeclaredField("value")
valueField.isAccessible = true
val key = keyGetMethod.invoke(entry)
val value = valueField.get(entry)
printGreen("key = $key\nvalue = $value")
println()
}
}
printGray("-------- 工作线程 ${curThread.name} --------")
}
打印信息分析:
name: java.lang.ThreadLocal@e580929 // threadLocalName对象
age: java.lang.ThreadLocal@1cd072a9 // threadLocalAge对象
工作原理
想要弄清楚ThreadLocal如何做到线程间数据隔离的效果,我们需要先了解Thread
、ThreadLocal
、ThreadLocalMap
和 Entry
四者之间的关系。
类名 | 描述 |
---|---|
Thread | ThreadLocalMap的载体,每个Thread内部维护一个ThreadLocalMap类型的threadLocals 属性。 |
ThreadLocal | 其工作线程的ThreadLocalMap数据操作的入口。 |
ThreadLocalMap | ThreadLocal的内部类,其内部维护一个Entry数组,ThreadLocal使用的 get / set 操作的数据都是来自这里。 |
Entry | ThreadLocalMap的内部类其继承自WeakReference<ThreadLocal<?>>,一个 key / value 结构,这个 key 就是我们所使用的ThreadLocal实例,使用弱引用包裹。 |
ThreadLocal存储数据的工作过程:
- 首先调用
ThreadLocal.set
设置数据后,检查当前线程.threadLocals
是否已创建ThreadLocalMap。 - 如果ThreadLocalMap未创建则调用
ThreadLocal.createMap
方法在当前线程创建ThreadLocalMap并赋值给当前线程.threadLocals
字段。 - 如果
当前线程.threadLocals
已实例化,将数据封装成一个Entry
对象,存入到ThreadLocalMap内部的Entry[] tabel
中。
因为每个线程都有自己的ThreadLocalMap,ThreadLocal在各线程中保存和获取数据所操作的目标都不一样,所以ThreadLocal可以实现各线程间数据隔离的效果。
常见问题
ThreadLocal的内存泄漏
Entry的key是对ThreadLocal的弱引用,手动设置ThreadLocal为null且系统触发gc后,Entry的key就被回收掉了,但是这个Entry还存活着,这种无效数据会长期占用内存,一直持续到线程退出。
解决的方法是触发ThreadLocalMap内部的 expungeStaleEntry
方法,该方法会将key为null的Entry赋值为null,它通常在set
、remove
、get
等方法中被调用,现在存在两种情况:
1、ThreadLocal手动赋值为null后,无法调用 set、get、remove
2、ThreadLocal使用后既不置为null,也不调用 remove
这两种情况都不会主动触发 expungeStaleEntry
方法的调用,所以为避免内存泄漏的发生,最好是在确保不再需要ThreadLocal之后及时调用其remove
方法,让系统正常回收掉Entry。
为什么Entry的key使用弱引用
- 因为如果Key使用弱引用指向ThreadLocal,在发生gc后系统如果将ThreadLocal回收掉,那么entry.get()将为null,这代表这个Entry是无效数据需要被清除掉。
- 强引用的话,外部将ThreadLocal置为null时除非通知每个线程内的ThreadLocalMap其指向的Entry的key无效了并手动清除掉数据,否则Entry中的key一直不会被回收掉,无法标识出哪个Entry是可以被清除的。
- 软引用需要内存不足的情况下才会考虑回收,显然也不适合。