ThreadLocal

ThreadLocal类是java.lang包中提供的类,下面来从应用场景、同步和原理等方面讨论一下它。

一、ThreadLocal介绍

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

大致的含义是:

ThreadLocal是保存线程的本地变量,访问的get/set方法都是相对独立的,private static ThreadLocal 实例化出来的私有静态字段是希望将某个状态与线程做关联。

大白话就是,ThreadLocal是和线程相关的,在一个线程的生命周期内,任意的set/get的值都只和当前线程相关。

二、原理

在了解ThreadLocal之前,我们先了解一下Thread、ThreadLocalMap和ThreadLocal这三者的关系。如下图所示:
title

1. 每个Thread中都维护了一个ThreadLocalMap

1
2
3
4
5
// 查看Thread.class

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

2. 每个ThreadLocalMap中都维护了多个ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 查看ThreadLocalMap.class

/**
* 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;
}
}

三、同步问题

1. 到底能不能解决同步?

有一些说法是ThreadLocal可以解决多线程问题,这里举一个使用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
28
29
30
31
32
33
/**
* @author zyh
* @Description:
*/
public class ThreadId {

// 线程Id 共享变量
private static Integer id = new Integer(0);

// 获取线程ID
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return id ++;
}
};

public static int get(){
return threadId.get();
}

public static void main(String[] args) {

for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(ThreadId.get());
}).start();

}
}

}

结果:

title

2. 为什么会出现这样的问题?

多个线程内的ThreadLocal确实是各自一份,但是ThreadLocal内部操作的静态变量(id)却是相同的引用,这个变量在内存只实例化了一次。换句话说,多个线程虽然在修改各自内部的ThreadLocal,但是ThreadLocal最终操作的id其实只是一个。

解决这个问题有两个办法:

  • 让这个静态变量id保持原子性
  • 让变量id实例化多次

3. 保持原子性解决同步

下面演示一下通过AtomicInteger让变量保持原子性,进而实现同步操作:

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
28
29
30
31
32
33
34
35
36
import java.util.concurrent.atomic.AtomicInteger;

/**
* @author zyh
* @Description:
*/
public class ThreadId {

// 线程Id 共享变量
private static AtomicInteger id = new AtomicInteger(0);

// 获取线程ID
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return id.getAndIncrement();
}
};

public static int get(){
return threadId.get();
}

public static void main(String[] args) {


for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(ThreadId.get());
}).start();

}

}

}

这里AtomicIntegerJUC包中的类,能够保证id的原子性加一操作。

4. 实例化多次解决同步

网上讨论的有关ThreadLocal<DateFormat>的同步问题,原理其实就是保证每个线程自己的ThreadLocal指向的DateFormat引用不相同。像下面每次都去实例化一个:

1
2
3
4
5
6
public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};

四、拓展InheritableThreadLocal

在开发中不免遇到子线程获取父线程的ThreadLocal中的值的场景。只使用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
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.concurrent.atomic.AtomicInteger;

/**
* @author zyh
* @Description:
*/
public class ThreadId {

// 线程Id 共享变量
private static AtomicInteger id = new AtomicInteger(0);

// 获取线程ID
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(()-> id.getAndIncrement());

public static int get(){
return threadId.get();
}

public static void set(Integer value){
threadId.set(value);
}

public static void main(String[] args) throws Exception{
// 调用处父线程设置值为8
set(8);
// 实例化子线程异步获取设置的值
Thread son = new Thread(()->{
System.out.println("子线程获取的值 : " + get());
});
son.start();
// 阻塞, 父线程等待子线程执行完成
son.join();

// 父线程获取设置的值
System.out.println("父线程获取的值 : " + get());

}

}

结果:

1
2
3
4
子线程获取的值 : 0
父线程获取的值 : 8

Process finished with exit code 0

下面演示如果使用InheritableThreadLocal让子类共享父类的值:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package threadlocal;

import java.util.concurrent.atomic.AtomicInteger;

/**
* @author zyh
* @Description:
*/
public class ThreadId {

// 线程Id 共享变量
private static AtomicInteger id = new AtomicInteger(0);

// 获取线程ID
private static final ThreadLocal<Integer> threadId = new InheritableThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return id.getAndIncrement();
}
};

public static int get(){
return threadId.get();
}

public static void set(Integer value){
threadId.set(value);
}

public static void main(String[] args) throws Exception{
set(8);
Thread son = new Thread(()->{
System.out.println("子线程获取的值 : " + get());
});
son.start();
son.join();

System.out.println("父线程获取的值 : " + get());

}

}

运行结果:

1
2
3
4
子线程获取的值 : 8
父线程获取的值 : 8

Process finished with exit code 0

ThreadLocal
http://example.com/2020/12/07/2020年12月07日21:00:20_ThreadLocal/
Author
Hoey
Posted on
December 7, 2020
Licensed under