ThreadLocal之实现原理

我会带着你远行 2021-09-30 08:42 459阅读 0赞

好久木有来更新了,下面来记录并分享下ThreadLocal的实现原理:

下面让我们一起深入ThreadLocal的内部实现。
我们需要关注的,自然是ThreadLocal的set()方法和get()方法。从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内部的成员。注意下面的定义是从Thread类中摘出来的:

  1. ThreadLocal.ThreadLocalMap.threadLocals = null;

而设置到ThreadLocal中的数据,也正是写入了threadLocals这个Map。其中key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。
在进行get()操作时,自然就是将这个Map中的数据拿出来:

  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. return (T)e.value;
  8. }
  9. return setInitialValue();
  10. }

首先,get()方法也是先取得当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据。
在了解了ThreadLocal的内部实现后,我们自然会引出一个问题。那就是这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。
当线程退出时,Thead类会进行一些清理工作,其中就包括清理ThreadLocalMap,注意下述代码的加粗部分:

  1. //在线程退出前,由系统回调,进行资源清理
  2. private void exit(){
  3. if(group != null){
  4. group.threadTerminated(this);
  5. group = null;
  6. }
  7. target = null;
  8. //加速资源清理
  9. threadLocals = null;
  10. inheritableThreadLocals = null;
  11. inheritedAccessControlContext = null;
  12. blocker = null;
  13. uncaughtExceptionHandler = null;
  14. }

因此,如果我们使用线程池,那就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在的)。如果这样,将一些大大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals Map内),可能会使系统出现内存泄露的可能(设置了对象到ThreadLocal中,但是不清理它,在使用几次后,这个对象也就不再有用了,但是它却无法被回收)。

此时,如果希望及时收回对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄露。
另外一种有趣的情况是JDK也可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写出类似obj=null之类的代码。如果这么做,obj所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。
同理,如果对于ThreadLocal的变量,我们也动手将其设置为null,比如t1=null,那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。这里面的奥秘是什么呢?先来看一个简单的例子:

  1. public class ThreadLocalDemo_Gc{
  2. static volatile ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>(){
  3. protected void finalize() throws Throwable{
  4. System.out.println(this.toString() + "is gc");
  5. }
  6. };
  7. static volatile CountDownLatch cd = new CountDownLatch(10000);
  8. public static class ParseDate implements Runnable{
  9. int i=0;
  10. public ParseDate(int i){
  11. this.i = i;
  12. }
  13. public void run(){
  14. try{
  15. if(tl.get() == null){
  16. t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")){
  17. protected void finalize() throws Throwable{
  18. System.out.println(this.toString() + "is gc");
  19. }
  20. });
  21. System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
  22. }
  23. Date t = tl.get().parse("2019-04-23 10:47:"+i%60);
  24. }catch(ParseException e){
  25. e.printStackTrace();
  26. }finally{
  27. cd.countDown();
  28. }
  29. }
  30. }
  31. public static void main(String[] args) throws InterruptedException{
  32. ExecutorService es = Executors.newFixedThreadPool(10);
  33. for(int i=0; i<10000; i++) {
  34. es.execute(new ParseDate(i));
  35. }
  36. cd.await();
  37. System.out.println("mission complete!!");
  38. tl = null;
  39. System.gc();
  40. System.out.println("first GC complete");
  41. //在设置ThreadLocal的时候,会清除ThreadLocalMap中的无效对象
  42. tl = new ThreadLocal<SimpleDateFormat>();
  43. cd = new CountDownLatch(1000);
  44. for(int i=0; i<1000; i++) {
  45. es.execute(new ParseDate(i));
  46. }
  47. cd.await();
  48. Thread.sleep(1000);
  49. System.gc();
  50. System.out.println("second GC complete");
  51. }
  52. }

上述案例是为了跟踪ThreadLocal对象以及内部SimpleDateFormat对象的垃圾回收。为此,我们在第3行和第17行,重载了finalize()方法。这样,我们在对象被回收时,就可以看到它们的踪迹。
在主函数main中,先后进行了两次任务提交,每次1000个任务。在第一次任务提交后,代码第39行,我们将tl设置为null,接着再进行一次GC。接着,我们进行第2次任务提交,完成后,在第50行再进行一次GC。
注意这些输出所代表的含义。首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到hreadLocal对象被回收了。接着提交了第2次任务,这次一样也创建了10个SimpleDateFormat对象。然后,进行第二次GC。可以看到,在第2次GC后,第一次创建的10个SimpleDateFormat子类全部被回收。可以看到,虽然我们没有手工remove()这些对象,但是系统依然有可能回收它们。
要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。之前我们说过,ThreadLocalMap是一个类似HashMap的东西,更加精确地说,它更加类似于WeakHashMap。
ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference的ThreadLocal集合。

  1. static class Entry extends WeakReference<ThreadLocal>{
  2. Object value;
  3. Entry(ThreadLocal k , Object v) {
  4. super(k);
  5. value = v;
  6. }
  7. }

这里的参数k就是Map的key,v就是Map的value。其中k也就是ThreadLocal实例,作为弱引用(super(k)就是调用了WeakReference的构造函数)。因此,虽然这里使用ThreadLocal作为Map的key,但实际上,它并不真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然JDK不一定会进行一次彻底的扫描),它会自然而然将这些垃圾数据回收。
三、对性能有何帮助
每一个线程分配一个独立的对象对系统性能也是有帮助的。当然了,这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。
这里,我们简单的测试下在多线程下产生随机数的性能问题。首先,我们定义一些全局变量:

  1. public static final int GEN_COUNT = 10000000;
  2. public static fianl int THREAD_COUNT = 4;
  3. static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
  4. public static Random rnd = new Random(123);
  5. public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>(){
  6. @Override
  7. protected Random initialValue(){
  8. return new Random(123);
  9. }
  10. };

代码第1行定义了每个线程要产生的随机数数量,第2行定义了参与工作的线程数量,第3行定义了线程池,第4行定义了被多线程共享的Random实例用于产生随机数,第6-11行定义了由ThreadLocal封装的Random。
接着,定义一个工作线程的内部逻辑。它可以工作在两种模式下:
第一是多线程共享一个Random(mode =0)。(耗时长)
第二是多个线程各分配一个Random(mode=1)。(耗时短)

发表评论

表情:
评论列表 (有 0 条评论,459人围观)

还没有评论,来说两句吧...

相关阅读