对ThreadLocal的理解
一、ThreadLocal解决什么问题
ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。即同一个ThreadLocal的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的Thread中有不同的副本
note:
(1). 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
(2). 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题
(3). 既无共享变量,就更没有同步问题
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
举个栗子:
1 import java.util.concurrent.CountDownLatch; 2 3 public class ThreadLocalTest { 4 public static void main(String[] args) throws InterruptedException { 5 int threads = 3; 6 CountDownLatch countDownLatch = new CountDownLatch(threads); 7 InnerClass innerClass = new InnerClass(); 8 for (int i = 1; i <= threads; i++) { 9 new Thread(() -> { 10 for (int j = 0; j < 4; j++) { 11 innerClass.add(String.valueOf(j)); 12 innerClass.print(); 13 } 14 innerClass.set("hello world"); 15 countDownLatch.countDown(); 16 }, "thread - " + i).start(); 17 } 18 countDownLatch.await(); 19 } 20 21 private static class InnerClass { 22 public void add(String newStr) { 23 StringBuilder str = Counter.threadlocal.get(); 24 Counter.threadlocal.set(str.append(newStr)); 25 } 26 27 public void print() { 28 System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n", 29 Thread.currentThread().getName(), Counter.threadlocal.hashCode(), Counter.threadlocal.get().hashCode(), Counter.threadlocal.get().toString()); 30 } 31 32 public void set(String words) { 33 Counter.threadlocal.set(new StringBuilder(words)); 34 System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n", 35 Thread.currentThread().getName(), Counter.threadlocal.hashCode(), Counter.threadlocal.get().hashCode(), Counter.threadlocal.get().toString()); 36 } 37 } 38 39 private static class Counter { 40 private static ThreadLocal<StringBuilder> threadlocal = new ThreadLocal<StringBuilder>() { 41 @Override 42 protected StringBuilder initialValue() { 43 return new StringBuilder(); 44 } 45 }; 46 } 47 48 }
ThreadLocal支持范型,如ThreadLocal<StringBuilder>、ThreadLocal<Long>。ThreadLocal 变量通常被private static修饰。
该例使用了StringBuilder类型的ThreadLocal变量。可通过ThreadLocal的get()方法读取StringBuidler实例,也可通过set(T t)方法设置 StringBuilder。
运行结果:
1 Thread name:thread - 3 , ThreadLocal hashcode:825948542, Instance hashcode:643474225, Value:0 2 Thread name:thread - 3 , ThreadLocal hashcode:825948542, Instance hashcode:643474225, Value:01 3 Thread name:thread - 3 , ThreadLocal hashcode:825948542, Instance hashcode:643474225, Value:012 4 Thread name:thread - 3 , ThreadLocal hashcode:825948542, Instance hashcode:643474225, Value:0123 5 Thread name:thread - 1 , ThreadLocal hashcode:825948542, Instance hashcode:56688464, Value:0 6 Thread name:thread - 1 , ThreadLocal hashcode:825948542, Instance hashcode:56688464, Value:01 7 Thread name:thread - 1 , ThreadLocal hashcode:825948542, Instance hashcode:56688464, Value:012 8 Thread name:thread - 2 , ThreadLocal hashcode:825948542, Instance hashcode:1073545513, Value:0 9 Thread name:thread - 2 , ThreadLocal hashcode:825948542, Instance hashcode:1073545513, Value:01 10 Thread name:thread - 1 , ThreadLocal hashcode:825948542, Instance hashcode:56688464, Value:0123 11 Set, Thread name:thread - 3 , ThreadLocal hashcode:825948542, Instance hashcode:2129647492, Value:hello world 12 Set, Thread name:thread - 1 , ThreadLocal hashcode:825948542, Instance hashcode:229052748, Value:hello world 13 Thread name:thread - 2 , ThreadLocal hashcode:825948542, Instance hashcode:1073545513, Value:012 14 Thread name:thread - 2 , ThreadLocal hashcode:825948542, Instance hashcode:1073545513, Value:0123 15 Set, Thread name:thread - 2 , ThreadLocal hashcode:825948542, Instance hashcode:1002607118, Value:hello world
ThreadLocal支持范型,如ThreadLocal<StringBuilder>、ThreadLocal<Long>。ThreadLocal 变量通常被private static修饰。
该例使用了StringBuilder类型的ThreadLocal变量。可通过ThreadLocal的get()方法读取StringBuidler实例,也可通过set(T t)方法设置 StringBuilder。
(1). 从1、5、8行输出可见,每个线程通过 ThreadLocal 的 get() 方法拿到的是不同的 StringBuilder 实例 (Instance hashcode不同)
(2). 第1、5、8行输出表明,每个线程所访问到的是同一个 ThreadLocal 变量(ThreadLocal hashcode相同)
(3). 从4、10、14行输出以及第28行代码可见,虽然从代码上都是对 Counter 类的静态 counter 字段进行 get() 得到 StringBuilder 实例并追加字符串,但是这并不会将所有线程追加的字符串都放进同一个 StringBuilder 中,而是每个线程将字符串追加进各自的 StringBuidler 实例内
(4). 对比第14行与第15行输出并结合第33行代码可知,使用 set(T t) 方法后,ThreadLocal 变量所指向的 StringBuilder 实例被替换
二、ThreadLocal原理
Thread维护ThreadLocal与实例的映射
ThreadLocalMap由Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案如下图所示。
该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。
ThreadLocal底层实现:
ThreadLocal类提供的几个方法:
public T get() { } public void set(T value) { } public void remove() { } protected T initialValue() { }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。
1. get方法的实现:
第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。
如果获取成功,则返回value值。
如果map为空,则调用setInitialValue方法返回value。
下面来仔细分析每一句:
2. 首先看一下getMap方法中做了什么:
在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。
3. 继续去Thread类中取看一下成员变量threadLocals是什么:
实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类
4. 继续去看ThreadLocalMap的实现:
可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。
5. 继续看setInitialValue方法的具体实现:
如果map不为空,就设置键值对,为空,再创建Map
6. createMap的实现:
总结:ThreadLocal是如何为每个线程创建变量的副本的
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
例2:
public class TheadLocalTest2 { ThreadLocal<Long> longLocal = new ThreadLocal<Long>(); ThreadLocal<String> stringLocal = new ThreadLocal<String>(); public void set() { longLocal.set(Thread.currentThread().getId()); stringLocal.set(Thread.currentThread().getName()); } public long getLong() { return longLocal.get(); } public String getString() { return stringLocal.get(); } public static void main(String[] args) throws InterruptedException { final TheadLocalTest2 test = new TheadLocalTest2(); test.set(); System.out.println(test.getLong()); System.out.println(test.getString()); Thread thread1 = new Thread(){ public void run() { test.set(); System.out.println(test.getLong()); System.out.println(test.getString()); }; }; thread1.start(); thread1.join(); System.out.println(test.getLong()); System.out.println(test.getString()); } }
从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的。
总结一下:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
3)在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
存在的两个问题:
1. 为何要使用弱引用
ThreadLocalMap中的key使用弱引用。当一条线程中的ThreadLocal对象使用完毕,没有强引用指向它的时候,垃圾收集器就会自动回收这个Key,从而达到节约内存的目的。
2. ThreadLocal的内存泄漏问题
在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。当某一条线程中的ThreadLocal使用完毕,没有强引用指向它的时候,这个key指向的对象就会被垃圾收集器回收,从而这个key就变成了null;然而,此时value和value指向的对象之间仍然是强引用关系,只要这种关系不解除,value指向的对象永远不会被垃圾收集器回收,从而导致内存泄漏!
不过不用担心,ThreadLocal提供了这个问题的解决方案。
每次操作set、get、remove操作时,ThreadLocal都会将key为null的Entry删除,从而避免内存泄漏。
那么问题又来了,如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法,此时该仍然可能会导致内存泄漏。
这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。
三、ThreadLocal的应用场景
ThreadLocal 适用于如下两种场景
(1). 每个线程需要有自己单独的实例
(2). 实例需要在多个方法中共享,但不希望被多线程共享
最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
对于 Java Web 应用而言,Session 保存了很多信息。很多时候需要通过 Session 获取信息,有些时候又需要修改 Session 的信息。一方面,需要保证每个线程有自己单独的 Session 实例。另一方面,由于很多地方都需要操作 Session,存在多方法共享 Session 的需求。如果不使用 ThreadLocal,可以在每个线程内构建一个 Session实例,并将该实例在多个方法间传递,如下所示。
public class SessionHandler { @Data public static class Session { private String id; private String user; private String status; } public Session createSession() { return new Session(); } public String getUser(Session session) { return session.getUser(); } public String getStatus(Session session) { return session.getStatus(); } public void setStatus(Session session, String status) { session.setStatus(status); } public static void main(String[] args) { new Thread(() -> { SessionHandler handler = new SessionHandler(); Session session = handler.createSession(); handler.getStatus(session); handler.getUser(session); handler.setStatus(session, "close"); handler.getStatus(session); }).start(); } }
该方法是可以实现需求的。但是每个需要使用 Session 的地方,都需要显式传递 Session 对象,方法间耦合度较高。
这里使用 ThreadLocal 重新实现该功能如下所示。
public class SessionHandler { public static ThreadLocal<Session> session = new ThreadLocal<Session>(); @Data public static class Session { private String id; private String user; private String status; } public void createSession() { session.set(new Session()); } public String getUser() { return session.get().getUser(); } public String getStatus() { return session.get().getStatus(); } public void setStatus(String status) { session.get().setStatus(status); } public static void main(String[] args) { new Thread(() -> { SessionHandler handler = new SessionHandler(); handler.getStatus(); handler.getUser(); handler.setStatus("close"); handler.getStatus(); }).start(); } }
使用 ThreadLocal 改造后的代码,不再需要在各个方法间传递 Session 对象,并且也非常轻松的保证了每个线程拥有自己独立的实例。
如果单看其中某一点,替代方法很多。比如可通过在线程内创建局部变量可实现每个线程有自己的实例,使用静态变量可实现变量在方法间的共享。但如果要同时满足变量在线程间的隔离与方法间的共享,ThreadLocal再合适不过。
总结
(1). ThreadLocal 并不解决线程间共享数据的问题
(2). ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
(3). 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
(4). ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
(5). ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
(6). ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
参考:
https://www.cnblogs.com/dolphin0520/p/3920407.html
http://www.jasongj.com/java/threadlocal/
https://blog.csdn.net/u010425776/article/details/79538184