Java-ThreadLocal之工作原理

基础概念

概念引入

在实际开发中,并发是很重要的需求,由此而带来的语言层面的切入点就是线程了,引入多线程开发之后,自然要考虑好同步、互斥、安全等内容。
因为这些需求就出现了以下三种来实现线程安全的手段:

  • 互斥同步
    简单点理解就是通过加锁来实现对临界资源的访问限制。加锁方式有Synchorized 和Lock 。
  • 非阻塞同步
    前面提到的互斥同步属于一种悲观锁机制,非阻塞同步属于乐观锁机制。典型的实现方式就是CAS 操作。
  • 无同步方案
    要保证线程安全,并不是一定就需要同步,两者没有因果关系,同步只是保证共享数据征用时正确性的手段,如果一个方法本来就不涉及共享数据,那它就不需要任何同步措施去保证正确性。ThreadLocal 的概念就是从这里引申出来的。

概念

在我看来其实其实现原理非常简单,简单理解Thread 即线程,Local 即本地。连续起来理解就是每个本地独有的线程。


示例用法

先通过下面这个实例来理解ThreadLocal 的用法。先声明一个ThreadLocal 对象,存储布尔类型的数值。然后分别在主线程中、Thread1 、Thread2 中为ThreadLocal 对象设置不同的数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ThreadLocalDemo {
public static void main(String[] args) {

// 声明 ThreadLocal对象
ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();

// 在主线程、子线程1、子线程2中去设置访问它的值
mThreadLocal.set(true);

System.out.println("Main " + mThreadLocal.get());

new Thread("Thread#1"){
@Override
public void run() {
mThreadLocal.set(false);
System.out.println("Thread#1 " + mThreadLocal.get());
}
}.start();

new Thread("Thread#2"){
@Override
public void run() {
System.out.println("Thread#2 " + mThreadLocal.get());
}
}.start();
}
}

打印的结果输出如下所示:

1
2
3
MainThread true
Thread#1 false
Thread#2 null

可以看见,在不同线程对同一个ThreadLocal 对象设置数值,在不同的线程中取出来的值不一样。


结构概览


清晰的看到一个线程Thread 中存在一个ThreadLocalMap ,ThreadLocalMap 中的key 对应ThreadLocal ,在此处可见Map可以存储多个key 即(ThreadLocal)。另外Value 就对应着在ThreadLocal 中存储的Value 。
因此总结出:每个Thread 中都具备一个ThreadLocalMap ,而ThreadLocalMap 可以存储以ThreadLocal 为key 的键值对。
这里解释了为什么每个线程访问同一个ThreadLocal ,得到的确是不同的数值。


源码分析

ThreadLocal#set

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

利用Thread 对象作为句柄获取ThreadLocalMap 对象

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

如果一开始设置,即ThreadLocalMap 对象未创建,则新建ThreadLocalMap 对象,并设置初始值。

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal#get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 public T get() {
// 获取Thread对象t
Thread t = Thread.currentThread();
// 获取t中的map
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

如果map为null ,就返回setInitialValue() 这个方法。

1
2
3
4
5
6
7
8
9
10
11
private T setInitialValue() {
// 返回null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

最后返回的是value ,而value 来自initialValue() 。

1
2
3
protected T initialValue() {
return null;
}

原来如此,如果不设置ThreadLocal 的数值,默认就是null,来自于此。


问答

  1. ThreadLocal 的实例以及其值存放在栈上呢?
    其实不是,因为ThreadLocal 实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal 的值其实也是被线程实例持有。
    它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

  2. ThreadLocal 只能被一个线程访问吗?
    使用InheritableThreadLocal 可以实现多个线程访问ThreadLocal 的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private void testInheritableThreadLocal() {
    final ThreadLocal threadLocal = new InheritableThreadLocal();
    threadLocal.set("droidyue.com");
    Thread t = new Thread() {
    @Override
    public void run() {
    super.run();
    Log.i(LOGTAG, "testInheritableThreadLocal =" + threadLocal.get());
    }
    };
    t.start();
    }
  3. ThreadLocal 会导致内存泄露吗?
    有网上讨论说ThreadLocal 会导致内存泄露,原因如下:

  • 首先ThreadLocal 实例被线程的ThreadLocalMap 实例持有,也可以看成被线程持有。
  • 如果应用使用了线程池,那么之前的线程实例处理完之后出于复用的目的依然存活。
  • 所以,ThreadLocal 设定的值被持有,导致内存泄露。

上面的逻辑是清晰的,可是ThreadLocal 并不会产生内存泄露,因为ThreadLocalMap 在选择key 的时候,并不是直接选择ThreadLocal 实例,而是ThreadLocal 实例的弱引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

所以实际上从ThreadLocal设计角度来说是不会导致内存泄露的。


引用

https://allenwu.itscoder.com/threadlocal-source
https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/