ThreadLocal学习

概述

ThreadLocal 译为线程本地变量,适用场景有很多总结起来就是一个:只希望在本线程使用的变量。

用户从浏览器发起请求,服务器有相应的线程处理请求,但是http无状态,session 可以用来保存一些状态只与用户此次会话相关,此时我们就可以使用线程本地变量来保存session。注意一个线程不是为一个用户服务的,当使用完成之后进行remove 本地变量,防止内存泄漏。

同一个线程处理非常复杂业务的时候,可以将显式传递的参数当作线程本地变量,从而可以在多个方法中使用。

在数据库连接的时候可以使用ThreadLocal
在这里插入图片描述
Entry 设计为继承WeakReference ,其Key 则变为了弱引用,这样当线程执行完任务退出时,相应的栈帧退出后,threadlocal无强引用,此时的弱引用只存在于堆中的Map 的key,正因为key是弱引用之后在gc时会被回收。我们常说的内存泄漏其实是关于value 的使用,value 指向的对象可为强引用,如果不去保证释放,则会造成map中存在key为null,value 不为null 的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();
								//如果从当前i 向后遍历table,找到entry 的k 正好为 需要set 进去的key,此时只需修改value 就好
                if (k == key) {
                    e.value = value;
                    return;
                }
								//如果k == null 说明此entry 为stale ,此时我们应该明白不该直接将key value设置到该位置上,因为在该位置到后续entry为null 位置的前一个位置上可能有entry的key为我们设置的key,如果是这种情况我们应该找到该
              //entry 并设置value,同时把该entry 设置到stalelot 上。如果未找到,那么当然就在该位置上直接创建。实际情况伴随着stale entry 的清除,具体逻辑见下面replaceStaleEntry。
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

				//替换过期entry 的方法
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            int slotToExpunge = staleSlot;
          //从后向前找过期的entry的索引,直到遇到一个null entry 停止,此时记录的slotToExpunge即为staleslot 之前的最早的过期entry,之后我们清理的时候可以从该entry 开始。
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
          
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
              //从staleslot 开始,从当前向后记录过期entry ,如果遍历的过程中遇到和当前要set 的key相等的k,说明我们可以将新entry 设置到staleslot 上,然后把staleslot 上的entry 拿到该索引位置上。
                if (k == key) {
                    e.value = value;

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

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
              //如果staleslot 之前没有过期entry,此时更新slotToExpunge 为当前索引
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
          

            // If key not found, put new entry in stale slot
          //执行到此,说明staleslot 之后没有与其哈希冲突的entry,所以应将staleslot 位置设置为我们应该插入的位置,同时进行清除过期entry。
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
          //如果slotToExpunge == staleslot 说明staleslot 之前 之后 都没有过期slot 自然不需要清理否则都需要清理。清除逻辑见expungeStaleEntry和cleanSomeSlots
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
	//清除过期entry 返回值为null entry 的索引。
  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();
              //如果为null 代表找到过期entry
                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;
        }
//采用启发算法 删除过期entry 做到logn的时间复杂度 达到 和O(n)差不多的效果
// 返回值表示是否删除成功。
        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;
        }

getEntry 方法

  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
              //当索引位置上k 与 key不相同时线性探测 
                return getEntryAfterMiss(key, i, e);
        }
//需要注意的是该方法在发现过期entry 时会及时进行清除。
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)
                  //清除之后后面的hash冲突结点会移到正确的位置,所以,此时不会进行i = nextIndex(i, len);
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

remove 方法

		private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                  //主动将key对应的引用队列出队列,即删除。
                    e.clear();
                  //删除对应的entry
                    expungeStaleEntry(i);
                    return;
                }
            }
     }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值