聊聊ThreadLocal
多线程带来了性能的提升,但是在读写共享变量时也带来了线程安全问题。你可以对共享变量加锁,包括 synchronized 内置锁、ReentrantLock、读写锁等等,这样可以实现多个线程的读写安全。如果每个线程可以独立访问自己的数据,那么就不存在线程安全的问题,前提是可以为每个线程创建副本,副本之间保持独立,这也就是 ThreadLocal 的实现思路。
共享变量和ThreadLocal的区别
如前面所述,ThreadLocal 并不是解决多线程共享变量带来的线程安全问题,相反,它是避免了多线程间的数据共享,也就不存在竞争的问题。下面看下这两种方式之间的区别:
共享变量
当使用共享变量时,所有线程访问的都是同一份数据,如下图所示。如果所有线程都只是读取共享变量内容,那么就不存在数据竞争。相反,如果有多个线程对共享变量进行读+写操作,那么就需要一些额外的手段保证结果的正确性,根据具体的场景,可以对共享变量加锁或者使用 CAS 原子操作。
ThreadLocal
每个线程会单独保存一份 ThreadLocal 变量对应数据的副本,这个副本仅对单个线程可见,逻辑上它是跟线程绑定到一起的。
ThreadLocal的用法
先来回顾下它的用法。在一个线程内可以对一个 ThreadLocal 变量数据进行多次操作(通常是 get、set)这些操作可以在多个方法中,只要最终是在一个线程中执行即可。如下是一个示例,用于在一个线程内统计耗时
1 | public class Profiler { |
这里主要关注以下几个点:
ThreadLocal 变量的定义。主要它是一个静态类型的变量,它的泛型是 Long,用于保存 Long 类型的变量
1
public static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>();
set 操作。通过 set 方法设置变量值,只需要传入目标数据即可,不需要指定线程或其他参数。
1
TIME_THREADLOCAL.set(System.currentTimeMillis())
get 操作。返回在这之前设置的值
1
TIME_THREADLOCAL.get()
可以看到 ThreadLocal 的使用还是比较简单的,它不涉及太多的参数,只需要关心要保存的数据即可。
ThreadLocal的内部结构
如下是 ThreadLocal 的内部结构。可以看到,为了保存变量副本,每个线程内部都有一个 ThreadLocalMap 对象,它的 key 为 ThreadLocal 对象,value 为要保存的变量。
ThreadLocal 变量可以有多个,相应地 ThreadLocalMap 的 key 也是多个。
get操作
当要访问一个 ThreadLocal 变量的值时,首先要找到 ThreadLocalMap 对象,因为它是跟线程绑定的,所以可以通过当前线程变量去获取
1 | // 获取当前线程 |
有了 ThreadLocalMap,下一步就是从中获取变量值。前面说过,它的 key 是 ThreadLocal 对象,因为这些是 ThreadLocal 中的操作,所以可以看到 key 传入的是 this,代表当前 ThreadLocal 对象。
1 | ThreadLocalMap.Entry e = map.getEntry(this); |
ThreadLocalMap.Entry 的定义如下,它继承了 WeakReference 类,这样保存的 key 不是强引用,可以在必要的时刻被垃圾回收器回收,注意对 value 的引用是强引用。
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
再来看下 map.getEntry(this) 这段代码。基本思路是:计算Entry数组下标,判断对应位置是否为要找到key,是的话则返回Entry,否则继续到下一个位置查找
1 | private Entry getEntry(ThreadLocal<?> key) { |
getEntryAfterMiss 用于继续查询目标 key。它使用的是再hash法,到下一个位置查找key
1 | private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { |
- 2019-02-26
在多线程环境下,如果要访问共享变量,通常要使用同步机制来保证访问的正确性。如果变量不需要共享,可以将其移到单个线程内,这样就不需要同步操作。ThreadLocal 把线程和要使用的对象关联起来,线程自己保存一份独立的副本,从而实现了数据访问的安全性。