Java Thread&Concurrency(10): 深入理解ThreadLocal及其实现原理

背景(注释):

ThreadLocal提供了线程私有变量。每一个变量对于每个线程来说都会有唯一的一个副本,通过get方法得到或者set方法设置,独立于其他线程的版本。ThreadLocal变量是典型的字段,只用于当前的一个线程(比如userID或者TransactionID)。

一下的这个用例给了每个线程一个ID:

 public class ThreadId {
      // Atomic integer containing the next thread ID to be assigned
      private static final AtomicInteger nextId = new AtomicInteger(0);
 
      // Thread local variable containing each thread's ID
      private static final ThreadLocal<Integer> threadId =
          new ThreadLocal<Integer>() {
              @Override protected Integer initialValue() {
                  return nextId.getAndIncrement();
          }
      };
 
      // Returns the current thread's unique ID, assigning it if necessary
      public static int get() {
          return threadId.get();
      }
  }

每个线程都隐含有一个变量,当线程存活着并且ThreadLocal变量被引用时存在。当线程结束,所有他的私有变量都会被垃圾回收。


我们来看get源代码:

    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,即threadLocals。
  • 假如不为空则,从中根据当前ThreadLocal作为key来获取Entry对象,不为空则返回value。
  • 假如当前map为空,则调用setInitialValue来初始化。
set方法有类似的过程:
   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

注意:是从Thread对象中得到ThreadLocalMap对象,事实上每个Thread都有个Map。
  • 如果map还未初始化,则先初始化(创建16个Entry的table),然后放入第一个Entry(key, value),这里的Entry继承WeakReference<ThreadLocal<?>>
  • 假如map已经存在,则调用set方法放入Entry,具体代码如下:
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

使用hashCode定位下标,假如发现存在“脏”(因为Entry继承WeakReference,当ThreadLocal即我们的key被垃圾回收后,会是null)的Entry则取代,否则将会创建一个新的Entry。值得注意的是,不管那种情况都会尝试删除已经“脏”的数据,增加的时候还会看是否需要扩展底层Entry组。
我们接着来看replaceStaleEntry:

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

详情如下:
  • 首先取得staleSlot前面的不为null的Entry中最前面的staleEntry(slotToExpunge,默认为staleSlot)。
  • 然后从staleSlot后面开始查询,假如发现“脏”数据,则在slotToExpunge==staleSLot的情况下更新slotToExpunge。假如找到同样的k,则更新Entry,并且置换staleSlot和当前的Entry,然后试着清理当前的脏数据。
  • 假如最后探测到null的Entry,则清理如果需要的话清理slotToExpunge上的脏数据。
以上的规则是slotToExpunge保证是连续不为null的Entry中最前面的“脏”数据,当slotToExpunge不为staleSlot时需要清理。

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

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter 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;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

尝试在下标staleSlot上清理脏数据,并且连续清理直到遇见null,过程中需要时会做rehash()。

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

cleanSomeSlots通过长度n以2为底数得到探测的次数。

重点是,以上代码中出现的e.get()==null是极为关键的,由于e是扩展WeakReference,所以当我们的key不存在引用时,只存在若引用的ThreadLocal很可能会被入队(引用队列),所以通过Entry得到的ThreadLocal可能为空。

这一套机制这里适合用于做Entry数组上的清理操作。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值