简单的使用

是什么

ThreadLocal是一个线程的局部变量,只有当前线程可以访问,因为只有当前线程可以访问,所以是线程安全的。

示例

  1. 举例
    多线程环境中使用SimpleDateFormat 解析日期

  2. 代码

    public class SimpleDateFormatTest {

    // SimpleDateFormat 实例
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 解析日期任务类
    public static class ParseDate implements Runnable {
    private int i = 0;

    public ParseDate(int i) {this.i = i;}

    @Override
    public void run() {
    try {
    Date date = sdf.parse("2020-10-06 19:21:" + i % 60);
    System.out.println(i + " : " + date);
    } catch (ParseException e) {
    e.printStackTrace();
    }
    }
    }

    public static void main(String[] args) {
    // 创建线程池,解析日期
    ExecutorService pool = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 1000; i++) {
    pool.execute(new ParseDate(i));
    }
    }
  3. 执行结果

Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: multiple points

可以看到执行后抛出异常,所以SimpleDateFormat.parse()方法不是线程安全的

解决方案

方案一:在parse方法前后加锁(这里不讨论)
方案二:使用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
public class x02ParseDate {
// SimpleDateFormat 实例
private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();

// 日期解析任务类
public static class ParseDate implements Runnable {
private int i = 0;

public ParseDate(int i) {
this.i = i;
}

@Override
public void run() {
try {
// 如果ThreadLocal中没有SimpleDateFormat实例,那就初始化一个
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date date = tl.get().parse("2020-10-06 19:21:" + i % 60);
System.out.println(i + " : " + date);
} catch (ParseException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
// 创建线程池,解析日期
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
pool.execute(new ParseDate(i));

}
}
}

为每一个线程分配不同的对象,需要在应用层进行保证;ThreadLocal只是起到了容器的作用

实现原理

set() 方法

我们来看看 set() 方法的内部

1
2
3
4
5
6
7
8
9
10
11
public void set(T value) {
// 1、获取当前线程对象
Thread t = Thread.currentThread();
// 2、获取当前线程对象的 ThreadlLocalMap
ThreadLocalMap map = getMap(t);
// 3、将值存入 ThreadlLcaolMap 中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

可以简单地把ThreadlLocalMap看作是一个Map(但请注意,它不是HashMap,而是存储 key-value 结构的 Entry)。

ThreadlLocalMap 是定义在Thread类中地成员,如下代码所示:

1
ThreadLocal.ThreadLocalMap threadLocals = null;	// Thread 类中

我们设置的数据写入了 threadLocals 中,其中 key 为 ThreadLocal 当前对象,value 就是我们设置的值。threadLocals 保存了当前自己线程所有的“局部变量”,也就是一个ThreadLocal变量的集合。

get() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public T get() {
// 1、获取当前线程
Thread t = Thread.currentThread();
// 2、获取当前线程的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 3、将自己作为 key 取得内部的数据
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T) e.value;
}
}
return setInitialValue();
}

ThreadLocal 中的一些变量是维护在Thread类内部的,所以:只要线程不退出,对象的引用就一直存在,无法被GC回收
当线程退出时,Thread类会进行一些清理操作(包括清理ThreadLocalMap)。通过将引用置为null,使得JVM将其当作垃圾并进行回收,详细内容见下小节.

ThreadLocal 导致内存泄漏

  1. 问题
    如上面所说,线程不退出,对象引用就一直存在,这在线程池中可能会导致内存泄漏。
    例如固定大小的线程池(FixThreadPool),其线程总是存在。如果将一些大对象设置进了 ThreadLocal 中,使用了几次后就不再使用,同时也没有清理它,这会导致大对象无法被回收,最终导致内存泄露。
  2. 如何解决
    • 使用 ThreadLocal.remove() 方法移除这个变量
    • 使用类似于 obj = null 的代码
  3. *为何使用类似于 obj = null 的代码可以防止内存泄漏**

ThreadLocalMap使用了弱引用(弱引用就是:在垃圾回收时,JVM 发现弱引用,就直接将其回收)。
ThreadLocalMap 内部由一系列 Entry 构成,每个Entry都继承了弱引用 WeakRefefence,如下代码所示:

1
2
3
4
5
6
7
8
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;

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

其中 k 是 ThreadLocal 实例,作为弱引用使用。super(k)就是调用了 WeakReference 的构造函数;
因此虽然使用 ThreadLocal 作为 Entry 的 k,但实际上它并不持有 ThreadLocal 的引用。
当 ThreadLocal 的外部强引用被回收时,Entry 中的 k 就会变成 null。
当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会进行一次清理,虽然JDK不一定会进行一次彻底的扫描),就会将这些垃圾数据回收。ThreadLocal的回收机制如图所示:

在这里插入图片描述