0%

Java多线程之ThreadLocal

本文介绍下Java多线程方面高频出现的ThreadLocal类

abstract.jpeg

基本实践

在传统的多线程开发场景中,为了避免多个线程同时操作一个共享变量而引起并发的问题。通常会通过加锁的形式进行处理。特别是在这个共享变量,并不是一个所谓的共享资源而只是用于线程内部各方法传递、使用的参数时,这种加锁的并发控制显然会降低系统的吞吐量。而ThreadLocal类则给我们提供一个新的思路——线程本地私有存储数据。简单来说就是,ThreadLocal为共享变量在每个线程内部提供了一个副本,用于进行线程内部自身的访问、存储等操作。避免了该共享变量在多线程环境下的操作冲突。该类典型方法如下所示

1
2
3
4
5
6
7
8
// 设置Value
public void set(T value);

// 获取数据
public T get();

// 移除数据
public void remove();

这里通过一个Demo来了解下该如何使用

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
package com.aaron.ThreadLocalTest;

import java.util.Random;

public class Demo1 {

public static void main(String[] args) throws Exception{
Runnable task = new MyTask();

new Thread( task ).start();
new Thread( task ).start();

}

}

class MyTask implements Runnable {

private Integer num = null;

private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

@Override
public void run() {
// 每个线程随机生产一个数
Integer count = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + ", count: " + count );

// 模拟业务耗时
try{
Thread.sleep(5000);
}catch (Exception e) {
}

// 存储数据
num = count;
threadLocal.set(count);

// 获取数据
System.out.println( Thread.currentThread().getName() + ", num: " + num + ", threadLocal: " +threadLocal.get() );

// 移除当前线程所存的数据
threadLocal.remove();
}
}

通过上述示例及其测试结果,可以看出对于普通的共享变量num而言,在多线程操作过程中会发生冲突。具体表现为Thread-0线程下该变量本来为53,却又被Thread-1线程修改为74;而对于threadLocal变量而言,从测试结果直观上我们就可看出并未在多线程环境下发生冲突,各线程的threadLocal变量数据被线程隔离了

figure 1.jpeg

实现原理

前面我们提到ThreadLocal是通过线程本地私有存储数据实现线程安全的。这里结合ThreadLocal、Thread类的源码做进一步阐述。以set方法为例,其首先通过currentThread获取当前线程的Thread实例。并通过getMap方法获取该线程的threadLocals属性,即一个ThreadLocal.ThreadLocalMap实例。而在ThreadLocalMap则通过Entry实现对ThreadLocal及其值的存储。进一步地,为了支持存储多个ThreadLocal变量及其值,ThreadLocalMap类中提供一个Entry数组类型的table字段。get方法同理,不再赘述。简而言之,ThreadLocal之所以可以实现对数据的线程隔离,是因为其将数据存储到Thread实例中

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
54
55
56
57
58
59
60
61
62
63
64
65
66
public class ThreadLocal<T> {

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

public T get() {
Thread t = Thread.currentThread();
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();
}

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

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

static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

private Entry[] table;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}

}

...

public class Thread implements Runnable {

ThreadLocal.ThreadLocalMap threadLocals = null;

}

应用场景

线程内资源的复用

众所周知,时间格式化类SimpleDateFormat在多线程环境下是非线程安全的。为此传统的解决方案,要么通过加锁的方式实现,此举会产生阻塞显著降低效率;要么则定义为局部变量,每次使用需自行new该实例,如果任务的数量较多,显然会严重浪费内存、CPU等资源。而ThreadLocal则可以很好的解决该问题。将SimpleDateFormat实例与线程实例进行绑定。一方面,各线程使用不同的SimpleDateFormat实例,避免了SimpleDateFormat线程不安全问题;另一方面,在基于线程池的方式利用线程的场景下,该线程所绑定的SimpleDateFormat实例在本次任务完成后,可以在该线程下一次的任务中继续复用。避免根据任务频繁地创建SimpleDateFormat实例。示例Demo如下所示

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
public class Demo2 {

public static void main(String[] args) throws Exception{
Runnable task = new Task();

new Thread( task ).start();
new Thread( task ).start();
}

}

class Task implements Runnable {

private SimpleDateFormat dateFormat1 = new SimpleDateFormat("HH:mm:ss");

// 使用static进行修饰,保持对ThreadLocal实例的强引用。这样只要该线程不结束退出,该SimpleDateFormat即可通过dateFormat2重复访问、使用
private static ThreadLocal<SimpleDateFormat> dateFormat2 = ThreadLocal.withInitial(
() -> new SimpleDateFormat("HH:mm:ss")
);

@Override
public void run() {
Long ts = RandomUtil.randomLong(10L, 5000000L);
Date date = new Date(ts);
System.out.println(Thread.currentThread().getName() + ", date: " + date );

String str1 = dateFormat1.format(date);
String str2 = dateFormat2.get().format(date);

System.out.println( Thread.currentThread().getName() + ", str1: " + str1 + ", str2: " + str2 );

}

}

测试结果如下所示,两个线程str1输出的结果证明了SimpleDateFormat的非线程安全

figure 2.jpeg

传递上下文

鉴于ThreadLocal的线程隔离特性,可以很方便我们在一个线程内的多个方法进行参数传递。即所谓的Context上下文。典型地,包括用户身份信息、数据库连接信息等。以避免通过添加入参的形式进行传递

内存泄露

Entry的Key

前面我们提到在ThreadLocalMap内部是通过Entry实现对ThreadLocal及其值的存储。而Entry的key字段则是一个指向ThreadLocal实例的弱引用。这里对弱引用 WeakReference 作必要的补充说明:GC进行回收时,对于只具有弱引用的对象,不管当前内存空间是否充足,均会回收该对象。故当一个ThreadLocal实例没有外部强引用时,其必然可以被GC回收,显然利用static修饰的ThreadLocal变量除外。试想如果Entry的key字段是一个指向ThreadLocal实例的强引用,那么如果该线程永远不结束退出,则会导致ThreadLocal实例无法被回收

Entry的Value

需要注意的是,Entry存储value则是通过强引用进行关联的。结合前面通过对Entry的key进行分析可知,一旦ThreadLocal实例不存在外部的强引用而被GC回收后,则相应的Entry实例就会变为key为null而value依然存在强引用。除非该线程退出结束,否则该value对象将会一直被Entry实例强引用而无法进行回收。这也是大家通常所说的ThreadLocal存在内存泄露的根源所在。其实,在ThreadLocal的set、get方法内部实现中也会对Entry数组进行检查,对key为null的Entry实例进行清除。但显然这种清除的触发是有一定的条件。故推荐大家在使用完ThreadLocal后,通过remove方法显式地清除该value值。或者将ThreadLocal变量修饰为static属性,以保持对ThreadLocal实例的强引用。这样就能保证任何时候均可以通过ThreadLocal实例对value进行更新、清除等操作

请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝