前言

本文适用于了解 ThreadLocal 的 API 但对于其应用场景难以理解或一知半解的读者,所以本文不会赘述 ThreadLocal 的 API,因为他的 API 很简单且已经有了许多解释其的文章。

正文

JDK 1.8 里 ThreadLocal 类源码的第一句注释就是这么一句话:

This class provides thread-local variables.

这句话的翻译是: ThreadLocal 提供线程本地的变量。ThreadLocal 的作用正是围摇着这句话而发挥的。

思考一个问题:除了 ThreadLocal 还有什么能提供线程本地的变量?

一个类的非静态字段属于该类的实例的本地变量,一个线程可以理解为一个 Thread 类或其子类的实例,那么 Thread 类或其子类的非静态字段就属于由其构造的线程(实例)的本地的变量。

根据以上理解不难发现,ThreadLocal 可以提供 Thread 子类的非静态字段作为本地变量的作用,亦即 ThreadLocal 能够替代一部分线程类的非静态字段。代码清单-1 和代码清单-2 可以体现这一点。

代码清单-1 使用了非静态字段存储线程的名称,代码清单-2 则使用了一个 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
/* 代码清单-1 */
private static class MyThread extends Thread {
private String name;

public MyThread(int i) {
this.name = "Thread-" + i;
}

@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name);
}
}

public static void main(String[] args) {
int threads = 3;
for (int i = 0; i < threads; i++) {
new MyThread(i).start();
}
}

/*
运行结果:
Thread-0
Thread-1
Thread-2
*/
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
   /* 代码清单-2 */
public static void main(String[] args) {
ThreadLocal<String> name = new ThreadLocal<>();
int threads = 3;
ExecutorService executor = Executors.newFixedThreadPool(threads);
try {
for (int i = 0; i < threads; i++) {
int finalI = i;
executor.execute(() -> {
name.set("Thread-" + finalI);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name.get());
});
}
} finally {
executor.shutdown();
}
}

/*
运行结果:
Thread-0
Thread-1
Thread-2
*/

那么为什么以及什么情况下我们要用 ThreadLocal 替代非静态字段?

对比代码清单-1 和代码清单-2 可以知道一些答案。众所周知,为了在不能修改一个类本身的情况下的给一个类添加字段,我们要么使用继承创造语法上的子类,要么使用组合创造行为上的子类,代码清单-1 是继承的完美体现,而代码清单-2 是组合的另类体现。组合没有继承那么强的侵入性,代码清单-1 只能手动创建线程,而这是不被提倡的,代码清单-2 则能利用线程池的优势。

代码清单-1 和 代码清单-2 的场景是比较简单的,现实场景相对复杂很多。

下面再举一个案例。

日志经常被使用来记录程序的运行情况,假如有一个日志类用于将各种信息记录于文件中,现在将其运用于记录线程的运行信息,要求每个线程的信息被记录于属于他的独立的文件中。关于该类的实现,有两种想法:1)该类是线程安全的,即使用者不需要担心他会引入线程安全问题,2)该类不是线程安全的,即使用者需要解决由他引入的线程安全问题。

代码清单-3 是第2类实现,Log 类不保证线程安全性,为了弥补这一点,让每个使用他的线程都有一个他的实例。注意到,Log 类侵入了本来用于实现其他业务逻辑的线程类,增加了耦合度。代码清单-4 利用 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
40
/* 代码清单-3 */
public class Log {
private PrintWriter writer;

public ThreadSpecificLog(String fileName) throws IOException {
writer = new PrintWriter(new FileWriter(fileName));
}

public void println(String s) {
writer.println(s);
}

public void close() {
writer.close();
}
}

public class MyThread extends Thread {

private Log log;

public MyThread(String name) throws IOException {
super(name);
log = new Log(name + "-log.txt");
}

@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
log.println(getName() + " " + i);
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.close();
}
}
}
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
43
44
45
46
47
48
49
50
51
52
53
/* 代码清单-4 */
public class ConcurrentLog {
private static final ThreadLocal<Log> log = new ThreadLocal<>();

public static void println(String s) throws IOException {
getLocalLog().println(s);
}

public static void close() throws IOException {
println("End of Log");
getLocalLog().close();
}

private static Log getLocalLog() throws IOException {
Log localLog = log.get();
if(localLog == null) {
localLog = new Log(Thread.currentThread().getName() + "-log.txt");
log.set(localLog);
}
return localLog;
}
}

public class Test {
public static void main(String[] args) {
int threads = 3;
ExecutorService executor = Executors.newFixedThreadPool(threads);
try {
for (int i = 0; i < threads; i++) {
int finalI = i;
executor.execute(() -> {
Thread.currentThread().setName("Thread-"+ finalI);
try {
for (int j = 0; j < 10; j++) {
ConcurrentLog.println(Thread.currentThread().getName() + ": " + j);
Thread.sleep(200);
}
} catch (InterruptedException | IOException e) {
e.printStackTrace();
} finally {
try {
ConcurrentLog.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
} finally {
executor.shutdown();
}
}
}

代码清单-4 中的 ConcurrentLog 类是第1类实现,为 Log 提供了线程安全性,增加了一层抽象,为线程提供了更好的接口,减少了与线程的耦合,使线程更能专注于实现自己的业务逻辑。从 测试类可以体现出来,ConcurrentLog 与线程的耦合度很低,他不会导致线程需要手动创建的情况发生,线程池内的线程的运用信息也能被记录。

总结

当需要使用非静态字段来提供线程安全性时,可以考虑使用 ThreadLocal 类来代替。 ThreadLocal 相当于不在 Thread 子类内声明的非静态字段,与后者相比他更加灵活,他可以在外部被声明然后在更多的地方使用。最后提一点:他不是天生线程安全的,就像不同线程的非静态字段可以引用同一实例来在多线程中共享数据一样,一个 ThreadLocal 实例在不同线程里也可以存储同一实例,毕竟 ThreadLocal 提供的是类似变量的功能,他可以在多个线程内引用同一个实例,这通常意味着 bug, 因为ThreadLocal 的语义被破坏了,何必在这种场景下使用 ThreadLocal 呢。