ThreadLocal源码分析 二

三、散列算法

  1. ThreadLocal的Hash值计算

    1. ThreadLocal里的hash code偏移量是固定值

      private static final int HASH_INCREMENT = 0x61c88647;
      
    2. 0x61c88647 转为 十进制就是0.618,0.618就是hash值的黄金分割点。计算公式也就是 √5 -1 / 2.

    3. ThreadLocal使用的就是 斐波那契散列法 + 拉链法存储数据到数组结构中。

四、源码部分

  1. 初始化,创建对象时很简单,只需要设置泛型,就会自动得到一个对应的hash值下标。

    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
  2. 设置元素

    1. 四种情况:

      • 待插入下标。是空位置直接插入
      • 待插入下标,不为空,key相同,直接更新
      • 待插入下标,不为空,key不相同,拉链法寻址。
      • 不为空,key不相同,碰到过期key。
    2. 源码:

      private void set(ThreadLocal<?> key, Object value) {
                  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) { // 查看key是否相等 相等直接更新值
                          e.value = value;
                          return;
                      }
      
                      if (k == null) { // key为空,直接赋值
                          replaceStaleEntry(key, value, i);
                          return;
                      }
                  }
      
                  tab[i] = new Entry(key, value);
                  int sz = ++size;
                  if (!cleanSomeSlots(i, sz) && sz >= threshold)
                      rehash();
              }
      
      
    3. 扩容机制

                  if (!cleanSomeSlots(i, sz) && sz >= threshold)
                      rehash();
      
      1. 首先,先进行清理操作,把过期元素清理掉,看空间是否充足

      2. 之后,判断大小,如果数组中的元素大于 len * 2 / 3 就需要扩容了,threshold = len * 2 / 3

      3. rehash()

        private void rehash() {
            expungeStaleEntries();
        
        	// 探测过期元素后判断是否满足扩容条件
            if (size >= threshold - threshold / 4)
                resize();
        }
        
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
         s           expungeStaleEntry(j);
            }
        }
        
      4. resize()

        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2; // 增加扩容两倍
            Entry[] newTab = new Entry[newLen];
            int count = 0;
        
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // 不存在了则进行置空,指控后被垃圾处理器回收释放内存
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1); // 重新进行散列计算下标
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen); // 拉链法顺延
                        newTab[h] = e;
                        count++;
                    }
                }
            }
        
            setThreshold(newLen); // 设置新长度
            size = count;
            table = newTab;
        }
        

五、获取元素

  1. 和存储元素类似,也是分为不同的情况

    • 直接定位到了,没有hash冲突,直接返回元素即可
    • 没有直接定位到,key不同,需要拉链式寻找
    • 没有直接定位到,key不同,拉链式寻找,遇到GC清理元素,需要探测式清理,再寻找元素。
  2. 源码

    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();
    }
    // 被调用的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
            return getEntryAfterMiss(key, i, e);
    }
    // 被调用的 gentEntyrtAgterMiss
    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;
    }
    
    
  3. 探测式清理,执行较耗时,在使用ThreadLocal.remove()操作,避免弱引用发生GC后,导致的内存泄露的问题。

    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;
    }
    

六、总结

  1. ThreadLocal的本质通过类名我们就可以得知,他是多线程本地的变量共享,但实际上它的作用并不能完全满足线程安全。假如存储的是引用类型的变量,线程安全的结论就不成立了,因为它内部存储的还是引用类型,最终的hash地址的指向依旧是同一个,假如多个线程使用了当前ThreadLocal变量里的值,并修改了内部的属性值,所有线程内的数据都会同步修改,最终的处理方法其实只有以下示例方式。

    public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
    
    

    此处依旧是每次线程调用时都会执行一个新的对象,但优点就是方便,节省内存空间。

  2. ThreadLocal的数据结构

    1. 底层数据存储结构依旧是数组,通过斐波那契散列法+拉链法进行了数据的存储。
  3. ThreadLocal是可以满足线程安全的吗?

    1. 如果是类似String类型的不可变数据,实际上是可以满足线程安全的说法的
    2. 假如是引用类型,就无法满足线程安全的说法,因为ThreadLocal中实际的存储依旧是指向的同一个内存地址。
  4. 在使用ThreadLocal时要注意什么

    1. 一定要在finaly中手动的remove掉ThreadLocal中存储的线程副本,避免执行自扫描GC的操作,避免引起内存泄漏
  5. ThreadLocal的应用场景

    1. 链路追踪
    2. 对线程不安全的类进行包裹使用。
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值