聊聊ThreadLocal

多线程带来了性能的提升,但是在读写共享变量时也带来了线程安全问题。你可以对共享变量加锁,包括 synchronized 内置锁、ReentrantLock、读写锁等等,这样可以实现多个线程的读写安全。如果每个线程可以独立访问自己的数据,那么就不存在线程安全的问题,前提是可以为每个线程创建副本,副本之间保持独立,这也就是 ThreadLocal 的实现思路。

共享变量和ThreadLocal的区别

如前面所述,ThreadLocal 并不是解决多线程共享变量带来的线程安全问题,相反,它是避免了多线程间的数据共享,也就不存在竞争的问题。下面看下这两种方式之间的区别:

共享变量

当使用共享变量时,所有线程访问的都是同一份数据,如下图所示。如果所有线程都只是读取共享变量内容,那么就不存在数据竞争。相反,如果有多个线程对共享变量进行读+写操作,那么就需要一些额外的手段保证结果的正确性,根据具体的场景,可以对共享变量加锁或者使用 CAS 原子操作。

image-20200807012337768

ThreadLocal

每个线程会单独保存一份 ThreadLocal 变量对应数据的副本,这个副本仅对单个线程可见,逻辑上它是跟线程绑定到一起的。

ThreadLocal的用法

先来回顾下它的用法。在一个线程内可以对一个 ThreadLocal 变量数据进行多次操作(通常是 get、set)这些操作可以在多个方法中,只要最终是在一个线程中执行即可。如下是一个示例,用于在一个线程内统计耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Profiler {
public static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>();

public static final void begin() {
TIME_THREADLOCAL.set(System.currentTimeMillis());
}

public static final long end() {
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}

public static void main(String[] args) throws InterruptedException {
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println("Cost: " + Profiler.end() + " mills");
}
}

这里主要关注以下几个点:

  1. ThreadLocal 变量的定义。主要它是一个静态类型的变量,它的泛型是 Long,用于保存 Long 类型的变量

    1
    public static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>();
  2. set 操作。通过 set 方法设置变量值,只需要传入目标数据即可,不需要指定线程或其他参数。

    1
    TIME_THREADLOCAL.set(System.currentTimeMillis())
  3. get 操作。返回在这之前设置的值

    1
    TIME_THREADLOCAL.get()

可以看到 ThreadLocal 的使用还是比较简单的,它不涉及太多的参数,只需要关心要保存的数据即可。

ThreadLocal的内部结构

如下是 ThreadLocal 的内部结构。可以看到,为了保存变量副本,每个线程内部都有一个 ThreadLocalMap 对象,它的 key 为 ThreadLocal 对象,value 为要保存的变量。

ThreadLocal 变量可以有多个,相应地 ThreadLocalMap 的 key 也是多个。

image-20200807012421803

get操作

当要访问一个 ThreadLocal 变量的值时,首先要找到 ThreadLocalMap 对象,因为它是跟线程绑定的,所以可以通过当前线程变量去获取

1
2
3
4
5
6
7
8
9
10
// 获取当前线程
Thread t = Thread.currentThread();

// 从线程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);

// 这个是从具体的实现
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

有了 ThreadLocalMap,下一步就是从中获取变量值。前面说过,它的 key 是 ThreadLocal 对象,因为这些是 ThreadLocal 中的操作,所以可以看到 key 传入的是 this,代表当前 ThreadLocal 对象。

1
2
3
4
5
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}

ThreadLocalMap.Entry 的定义如下,它继承了 WeakReference 类,这样保存的 key 不是强引用,可以在必要的时刻被垃圾回收器回收,注意对 value 的引用是强引用。

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

再来看下 map.getEntry(this) 这段代码。基本思路是:计算Entry数组下标,判断对应位置是否为要找到key,是的话则返回Entry,否则继续到下一个位置查找

1
2
3
4
5
6
7
8
9
10
11
12
13
private Entry getEntry(ThreadLocal<?> key) {
// 计算Entry数组下标
int i = key.threadLocalHashCode & (table.length - 1);
// 取Entry
Entry e = table[i];
// 判断是否为要找到的key
if (e != null && e.get() == key)
// 返回Entry
return e;
else
// 到下一个位置查找
return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss 用于继续查询目标 key。它使用的是再hash法,到下一个位置查找key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
// 数组长度
int len = tab.length;
// 查找Entry
while (e != null) {
// 取当前Entry保存的key
ThreadLocal<?> k = e.get();
// 是要找到,直接返回
if (k == key)
return e;
// Entry不是null,但是key是null,说明被垃圾回收了,已经失效了,需要单独处理
if (k == null)
expungeStaleEntry(i);
// 不是要找到的,也不是null,继续往后找下一个下标
else
i = nextIndex(i, len);
// 取出Entry
e = tab[i];
}
return null;
}