ThreadLocal使用

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  -- 子线程

可以看出在主线程和子线程中 threadLocalNamethreadLocalAge 对应的值是不一样的,单纯看上面的代码和打印情况有点反直觉,下面用一张图展示当前两个线程的情况。

ThreadLocal关系

附上一段查看各个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如何做到线程间数据隔离的效果,我们需要先了解ThreadThreadLocalThreadLocalMapEntry 四者之间的关系。

类名描述
ThreadThreadLocalMap的载体,每个Thread内部维护一个ThreadLocalMap类型的threadLocals 属性。
ThreadLocal其工作线程的ThreadLocalMap数据操作的入口。
ThreadLocalMapThreadLocal的内部类,其内部维护一个Entry数组,ThreadLocal使用的 get / set 操作的数据都是来自这里。
EntryThreadLocalMap的内部类其继承自WeakReference<ThreadLocal<?>>,一个 key / value 结构,这个 key 就是我们所使用的ThreadLocal实例,使用弱引用包裹。

ThreadLocal存储数据的工作过程:

  1. 首先调用ThreadLocal.set设置数据后,检查当前线程.threadLocals是否已创建ThreadLocalMap。
  2. 如果ThreadLocalMap未创建则调用 ThreadLocal.createMap 方法在当前线程创建ThreadLocalMap并赋值给 当前线程.threadLocals 字段。
  3. 如果 当前线程.threadLocals 已实例化,将数据封装成一个 Entry 对象,存入到ThreadLocalMap内部的 Entry[] tabel 中。

因为每个线程都有自己的ThreadLocalMap,ThreadLocal在各线程中保存和获取数据所操作的目标都不一样,所以ThreadLocal可以实现各线程间数据隔离的效果。

常见问题

ThreadLocal的内存泄漏

Entry的key是对ThreadLocal的弱引用,手动设置ThreadLocal为null且系统触发gc后,Entry的key就被回收掉了,但是这个Entry还存活着,这种无效数据会长期占用内存,一直持续到线程退出。

解决的方法是触发ThreadLocalMap内部的 expungeStaleEntry 方法,该方法会将key为null的Entry赋值为null,它通常在setremoveget 等方法中被调用,现在存在两种情况:

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是可以被清除的。
  • 软引用需要内存不足的情况下才会考虑回收,显然也不适合。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值