多线程---ThreadLocal

ThreadLocal类可以理解为线程本地变量。每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。通过让每个线程有自己的独立副本从而实现线程封闭的机制。每个线程有一个自己的ThreadLocalMap。底层是一个数组实现的。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。å¨è¿éæå¥å¾çæè¿°

 存储结构解析

       static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

这个Entry的key是一个弱引用。value是真正的值。为什么需要用弱引用:弱引用在每次GC的时候都会回收,且不会影响其对象的回收。即一个对象只剩弱引用,那么他会被回收。如果使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

ThreadLocal的set方法:首先根据线程找到map,然后根据设置值。如果map不存在就创建一个。

   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

ThreadLocalMap的set方法:

    private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        // 获取到数组的下标
        int i = key.threadLocalHashCode & (len-1);
        // 如果下标有值 发生了hash冲突 使用线性探测
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            // 找到了key 直接覆盖
            if (k == key) {
                e.value = value;
                return;
            }
            // 这个hash值下有对象过期了 进行替换
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 找到空地方了 插入
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

replaceStaleEntry方法:
        前向搜索,出现key为null的Entry肯定是因为上次GC了,而之所以去前向搜索,是因为很有可能其它Entry在上次GC中也没能存活。遍历到为数组元素为null。
        向后遍历,是因为ThreadLocal用的是开地址,很可能当前的stale entry对应的并不是hascode为此槽索引的Entry,而是因为哈希冲突后移的Entry,那么很有可能hascode对应该槽的Entry会往后排。遍历到为数组元素为null。

每次发现了过期数据会调用cleanSomeSlots进行清理

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

        // 向前移动 看是否有过期的entry 将最前一个过期的下标赋值为slotToExpunge
        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();
            // 找到了 进行交换
            // 这种情况: staleSlot这个位置的数据已经过期 而i>=staleSlot 
            // 根据探测方式 这个hash值应该先放staleSlot 后放i 所以需要交换
            if (k == key) {
                e.value = value;

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

                // 前面的位置没有过期的 那么过期的就从i开始
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                // 清理数据 从slotToExpunge 到 len
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }
            // 前面没有过期的 且这个位置过期了(k == null) 清理开始位置赋值为i
            // slotToExpunge代表发现的第一个数据过期的位置
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 没有找到这个key 先赋值
        tab[staleSlot].value = null;
        tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

        // 发现有过期的数据 进行清理和调整
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

expungeStaleEntry:负责将第一个过期元素的位置到元素为null的位置中间的元素重新hash,移动到应有的位置,释放掉key为null的元素。

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

        // staleSlot位置数据已经过期 设置为null
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        // 调整
        // 因为使用的是线性探测方法 所以需要把因发生了hash冲突的entry向前移动
        ThreadLocal.ThreadLocalMap.Entry e;
        int i;
        for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            // 数据过期 设置为null
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                // 检测是不是因为hash冲突而移动的值
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) { // 是因为hash冲突而移动的值
                    tab[i] = null;
                    // h是本应该放的位置 然后使用线下探测找到第一个空位
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        // staleSlot后面第一个为null的位置
        return i;
    }

expungeStaleEntry(int staleSlot):此时staleSlot就等于1。按照逻辑,会把2 3 5这三个元素重新hash,会把1 4释放.

数组下标0123456789
key10null2023null2428
valuev0v1v2v3v4v5v8

就会变成。此时返回值为5.第一个null的坐标。此时v1和v4才能被gc回收

数组下标0123456789
key1020232428
valuev0v2v3v5v8

看一下cleanSomeSlots方法。就是调用log2(n)次expungeStaleEntry方法,是查询和回收的一个折中。

        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);// log2(n)
            return removed;
        }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值