ThreadLocal

Posted joe-go

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal相关的知识,希望对你有一定的参考价值。

  除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。比如,让100个人填写个人信息表,如果只有一支笔,大家就得挨个填,对于管理人员来说,必须保证大家不会去哄抢仅存的一支笔,否则,谁也填不完。当然我们还可以从另外一个角度出发,就是准备100支笔,人手一支,那么所有人都可以各自为营,很快就能完成表格的填写工作。

  如果说锁是一种思路,那么ThreadLocal就是第二种思路。

ThreadLocal的简单使用

  ThreadLocal是线程的局部变量,也就是说,只有当前线程才能访问。既然是只有当前线程才能访问的数据,自然就是线程安全的,下面是一个简单的示例,了解ThreadLocal的基本用法:

 1 public class ThreadLocalDemo {
 2     static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
 3 
 4     public static class ParseDate implements Runnable{
 5         int i = 0;
 6         public ParseDate(int i){
 7             this.i = i;
 8         }
 9 
10         @Override
11         public void run() {
12             try {
13                 if (threadLocal.get() == null){
14                     threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
15                 }
16                 Date t = threadLocal.get().parse("2018-10-16 09:13:"+ i%60);
17                 System.out.println(i + ":" + t);
18             }catch (ParseException e) {
19                 e.printStackTrace();
20             }
21         }
22     }
23     //测试
24     public static void main(String[] args){
25         ExecutorService es = Executors.newFixedThreadPool(10);
26         for (int i = 0;i < 1000;i++){
27             es.execute(new ParseDate(i));
28         }
29     }
30 }

上述代码第13,14行,如果当前线程不持有SimpleDateformat对象实例。那么就新建一个并把它设置到当前线程中,如果持有,则直接使用。

  从上面的例子可以看出,为每一个线程人手分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为不同的线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。ThreadLocal只是起到了容器的作用。

ThreadLocal的实现原理

ThreadLocal又是如何保证这些对象只被当前线程所访问呢?下面来看看ThreadLocal的内部实现。

set():

1 public void set(T value) {
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t);
4         if (map != null)
5             map.set(this, value);
6         else
7             createMap(t, value);
8     }

在set时,首先获得了当前线程对象,然后通过getMap()拿到当前线程的ThreadLocalMap,并将值设置到ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是可以把它简单的理解成HashMap),但是它是定义在Thread内部的成员。

ThreadLocal.ThreadLocalMap threadLocals = null;

上述代码是在Thread类中定义的。可以看出设置到ThreadLocal中的数据,就是写入到了threadLocals这个Map中。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程所有的“局部变量”,也就是一个ThreadL变量的集合。

get():

 1 public T get() {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);
 4         if (map != null) {
 5             ThreadLocalMap.Entry e = map.getEntry(this);
 6             if (e != null) {
 7                 @SuppressWarnings("unchecked")
 8                 T result = (T)e.value;
 9                 return result;
10             }
11         }
12         return setInitialValue();
13     }

首先,get()方法也是先取得当前线程的ThreadLocalMap对象,然后,通过将自己作为key取得内部的实际数据。否则通过setInitialValue()方法,返回null。

在了解了ThreadLocal的set()和get()方法后,就会想到一个问题。那就是这些变量是维护在Thread类内部的,这也就意味着只要线程不退出,对象的引用将一直存在。

线程退出

当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap,下面方法就是线程退出的源码:

 1 private void exit() {
 2         if (group != null) {
 3             group.threadTerminated(this);
 4             group = null;
 5         }
 6         /* Aggressively null out all reference fields: see bug 4006245 */
 7         target = null;
 8         /* Speed the release of some of these resources */
 9         threadLocals = null;
10         inheritableThreadLocals = null;
11         inheritedAccessControlContext = null;
12         blocker = null;
13         uncaughtExceptionHandler = null;
14     }

可以看出在线程退出时,会将threadLocals设置为null,这样会让GC在下一次回收时,回收这个引用。

注意:

  如果我们使用线程池,那就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在),如果这样,将一些大的对象设置到了ThreadLocal中(实际上是保存在ThreadLocalMap中),可能会导致内存泄露的可能(意思就是:你设置了大的对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象就不再有用了,但是它却无法被回收),此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。还有一种方法是将ThreadLocal的变量手动设置为null(比如:threadLocal = null),那么这个ThreadLocal对应的局部变量会更加容易被垃圾回收器发现,从而加速回收。

ThreadLocal的回收机制

  要了解ThreadLocal的回收机制,需要更进一步了解ThreadLocal.ThreadLocalMap的实现。之前说过,ThreadLocalMap类似于HashMap,其实,更准确的说是,ThreadLocalMap更加类似于WeakHashMap。

  ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,若发现弱引用,就会立即回收。ThreadLocalMap是由一系列的Entry构成的,下面看一下Entry源码:

1 static class Entry extends WeakReference<ThreadLocal<?>> {
2             /** The value associated with this ThreadLocal. */
3             Object value;
4 
5             Entry(ThreadLocal<?> k, Object v) {
6                 super(k);
7                 value = v;
8             }
9         }

可以看出,每一个Entry都是WeakReference<ThreadLocal>。这里的参数k就是Map的key,v就是Map的value。其中k就是ThreadLocal的实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数),因此,虽然这里使用ThreadLocal作为Map的key,但是实际上它并不是真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变为null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理),就会将这些垃圾数据回收。

ThreadLocal 的性能

 为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然,这不是一定的。这取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,这样我们就可以考虑使用ThreadLocal为每个线程分配单独的对象。下面举一个典型的案例:多线程下产生随机数

 1 public class RandomDemo {
 2     public static final int GEN_COUNT = 10000000;//每个线程要产生的随机数
 3     public static final int THREAD_COUNT = 4;//线程数
 4     static ExecutorService es = Executors.newFixedThreadPool(THREAD_COUNT);//线程数量为4的固定线程池
 5     public static Random random = new Random(123);//共享的Random实例用于产生随机数
 6 
 7     //ThreadLocal 封装了Random
 8     public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>(){
 9         @Override
10         protected Random initialValue() {
11             return new Random(123);
12         }
13     };
14 
15     public static class RndTask implements Callable<Long>{
16         private int mode = 0;
17         public RndTask(int mode){
18             this.mode = mode;
19         }
20         //mode=0:共享一个Random实例;mode=1:每个线程分配一个实例
21         public Random getRandom(){
22             if (mode == 0){
23                 return random;
24             }else if (mode == 1){
25                 return tRnd.get();
26             }else {
27                 return null;
28             }
29         }
30         //每个线程产生100000个随机数,完成工作后,记录并返回所消耗的时间
31         @Override
32         public Long call() {
33             long b = System.currentTimeMillis();
34             for (long i = 0;i < GEN_COUNT;i++){
35                 getRandom().nextInt();
36             }
37             long e = System.currentTimeMillis();
38             System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms");
39             return e-b;
40         }
41     }
42     //测试
43     public static void main(String[] args) throws ExecutionException, InterruptedException {
44         Future<Long>[] futs = new Future[THREAD_COUNT];
45         for (int i = 0;i < THREAD_COUNT;i++){
46             futs[i] = es.submit(new RndTask(0));
47         }
48         long totalTime = 0;
49         for (int i = 0;i < THREAD_COUNT;i++){
50             totalTime += futs[i].get();
51         }
52         System.out.println("多线程访问同一个Random实例:"+ totalTime + "ms");
53         //ThreadLocal情况
54         for (int i = 0;i < THREAD_COUNT;i++){
55             futs[i] = es.submit(new RndTask(1));
56         }
57         totalTime = 0;
58         for (int i = 0;i < THREAD_COUNT;i++){
59             totalTime += futs[i].get();
60         }
61         System.out.println("使用ThreadLocal包装Random实例:" + totalTime + "ms");
62         es.shutdown();
63     }
64 }

 输出结果:

pool-1-thread-2 spend 978ms
pool-1-thread-1 spend 983ms
pool-1-thread-3 spend 808ms
pool-1-thread-4 spend 1216ms
多线程访问同一个Random实例:3985ms
pool-1-thread-4 spend 109ms
pool-1-thread-2 spend 111ms
pool-1-thread-3 spend 115ms
pool-1-thread-1 spend 130ms
使用ThreadLocal包装Random实例:465ms

可以看出:多线程在共享一个Random实例的情况下,总共耗时3985ms,而在ThreadLocal模式下,仅仅耗时465ms。注意:不是所有的并发情况下都适合ThreadLocal,只是在锁资源竞争激烈的情况下,ThreadLocal才有可能适合。

最后

  ThreadLocal的key就是线程本身的弱引用,一个ThreadLocalMap只能存储一种特定的数据类型。如果想让ThreadLocalMap里存储不同数据类型的数据,你如一个String,一个Integer,一个Date,那么就只能多定义几个ThreadLocal,ThreadLocal支持泛型"public class ThreadLocal<T>"。

参考:《Java高并发程序设计》 葛一鸣 郭超 编著:


以上是关于ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

源代码系列02——ThreadLocal源码分析(基础篇)

java中的ThreadLocal详解及示例代码

ThreadLocal介绍

MyBatis基础:使用java提供的ThreadLocal类优化代码

Java 单线程代码ThreadLocal串值问题

ThreadLocal