ThreadLocal作用与实现

ThreadLocal用于提供线程内部的局部变量,这些变量与它们的正常对象不同,每个线程访问一个单独属于自己的,独立的变量的初始副本。对于ThreadLocal中那些希望将状态与线程相关联的静态字段,通常是private的,例如 用户Id或者事务ID。例如,下面的类为每个线程生成一个本地唯一标识符,线程的id是在第一次调用ThreadId.get()时分配的,并在后续调用中保持不变。

public class ThreadId {
        //包含要分配的下一个线程ID的原子整数
         private static final AtomicInteger nextId = new AtomicInteger(0);

         // 线程局部变量包含每个线程的ID
          private static final ThreadLocal <Integer> threadId =
            new ThreadLocal<Integer>() {
                  @Override protected Integer initialValue() {
                     return nextId.getAndIncrement();
             }
         };

         // 返回当前线程的唯一ID,必要时分配它
         public static int get() {
              return threadId.get();
          }
}

只要线程在期生命周期内并且ThreadLocal是可访问的,那么每个线程持有对其线程局部副本的隐式引用变量。在一个线程消亡后,它的所有副本ThreadLocal实例会被垃圾收集器回收(除非存在其他对这些副本的引用)

总而言之,ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

接下来通过代码来演示ThreadLocal对线程内部的数据隔离效果:

public class TestThread{

     private static final ThreadLocal <Integer> value =
                new ThreadLocal<Integer>() {
                      @Override protected Integer initialValue() {
                         return 0;
                 }
             };

    static class MyThread implements Runnable{
        private int index;
        public  MyThread(int index) {
            this.index=index;
        }
        @Override
        public void run() {
            System.out.println("线程"+index+"的初始化值为:"+value.get());
            for (int i = 0; i <10;i++) {
                value.set(value.get()+i);
            }
            System.out.println("线程"+index+"最后累加的值位:"+value.get());
        }

    }
    public static void main(String[] args) throws InterruptedException{
        for (int i = 0; i <3; i++) {
            Thread thread = new Thread(new MyThread(i),"Thread"+String.valueOf(i));
            thread.start();
            TimeUnit.SECONDS.sleep(3);
        }
    }

}

执行结果如下:

线程0的初始化值为:0
线程0最后累加的值位:45
线程1的初始化值为:0
线程1最后累加的值位:45
线程2的初始化值为:0
线程2最后累加的值位:45

可以看到,各个线程的value值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

实现

set函数:

/**
     *设置此线程局部变量的当前线程的副本
      *到指定值。 大多数子类将不需要
      *覆盖此方法,只依靠initialValue
      *方法设置线程局部变量的值。
**/ 
 public void set(T value) {
        Thread t = Thread.currentThread();

        ThreadLocalMap map = getMap(t);

        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

通过set函数可以发现,先获取当前线程,然后调用一个getMap函数,得到一个ThreadLocalMap对象,判断该对象是否为null,如果不为null,则直接赋值,如果为null,则调用createMap。

getMap()函数是获取当前线程的相关连的ThreadLocalMap对象

    //获取与ThreadLocal相关联的ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

Thread类里的threadLocals 变量:

    //与当前线程有关的ThreadLocal值,由ThreadLocal类维护。
    ThreadLocal.ThreadLocalMap threadLocals = null;

creatMap函数:

    //创建与ThreadLoca关联的ThreadLocalMap
  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

通过上面的源码可以知道,每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object

再来看看ThreadLocalMap的实现:

static class ThreadLocalMap {

        / **
          *ThreadLocalMap的Entry继承了WeakReference,使用
          * ThreadLocal对象作为key。 注意,null键(即entry.get()
          * == null)意味着该键不再被引用,所以
          *entry可以从表中删除。 这些entries在随后的代码中被称为“陈旧entry”。

      **/
        static class Entry extends WeakReference<ThreadLocal<?>> {
         Object value;

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

也就是说,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的

下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

这里写图片描述

既然ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
我们来看看到底会不会出现这种情况。
其实,在JDK的ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施.
下面是ThreadLocalMap的getEntry方法的源码:


         /**
          *获取与key相关联的entry。 这个方法
          *本身只处理快速路径:直接命中现有的
          *键。 否则它将继承getEntryAfterMiss。 这是
          *旨在最大程度地提高直接命中的性能
          *通过使此方法容易内联。
          **/
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

当getEntry在其直接哈希槽中找不到键时,则要调用getEntryAfterMiss函数。遍历entry,获取entry的key,判断当前key是否等于参数ThreadLocal,如果相等则直接返回,如果key为null,则直接调用expungeStaleEntry,清除掉这个entry其实现如下:

        / **
          * 
          * @param key是ThreadLocal
          * @param i是key在hash表中的索引,table[i]
          * @param e 哈希表中的entry,通过table[i]
          * @返回与键相关联的条目,如果没有则返回null
          * /
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

expungeStaleEntry函数在staleSlot和下一个空槽之间,通过rehash任何可能的冲突条目来清除失效的条目。这也避免了在尾随null之前遇到的任何其他旧的entry。
expungeStaleEntry函数的源码:

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 在staleSlot删除条目
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // rehash直到返回null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // 与Knuth 6.4算法R不同,我们必须扫描到null,因为多个条目可能已失效。
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

整理一下ThreadLocalMap的getEntry函数的流程:
>

首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;

如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询

在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值