本文介绍下Java多线程方面高频出现的ThreadLocal类
基本实践
在传统的多线程开发场景中,为了避免多个线程同时操作一个共享变量而引起并发的问题。通常会通过加锁的形式进行处理。特别是在这个共享变量,并不是一个所谓的共享资源而只是用于线程内部各方法传递、使用的参数时,这种加锁的并发控制显然会降低系统的吞吐量。而ThreadLocal类则给我们提供一个新的思路——线程本地私有存储数据。简单来说就是,ThreadLocal为共享变量在每个线程内部提供了一个副本,用于进行线程内部自身的访问、存储等操作。避免了该共享变量在多线程环境下的操作冲突。该类典型方法如下所示
1 2 3 4 5 6 7 8
| 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变量数据被线程隔离了
实现原理
前面我们提到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<?>> { 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");
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的非线程安全
传递上下文
鉴于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进行更新、清除等操作