ThreadLocal知识点补充

ThreadLocal再总结

前言

之前我的一篇文章Android消息机制——补充完善中大致分析过ThreadLocal,所以这篇文章主要为了补充完善关于它的相关知识。

思考

:为什么每个线程中要持有一个ThreadLocal.ThreadLocalMap对象而不是ThreadLocal对象?

这个问题的答案,我觉得这篇文章讲的不错:threadlocal为什么这么设计?

文章分析了两种ThreadLocal的设计方案:

  • 方案一:每个线程可以持有多个ThreadLocal对象,这些ThreadLocal对象将Thread的id(或者线程的名称等线程标识字段)作为key,想要存储的具体实例的引用作为value存储在其中的ThreadLocalMap中,如下图所示:

    img

    这种方式主要的问题是需要进行同步,效率较低。

    因为ThreadLocal可能会被多个线程引用,所以在多线程环境下对其中的ThreadLocalMap进行操作就需要进行同步了。

  • 方案二:每个线程持有ThreadLocal内部的ThreadLocalMap对象,这个ThreadLocalMap将外部类ThreadLocal对象的引用作为key,具体实例的引用作为value存储在其中,如下图所示。ThreadLocal就是使用的这种设计方案。

img

​ 在这种方式下,每个线程直接持有一个ThreadLocalMap对象,各自线程的变量被各自线程中ThreadLocalMap对象的value所引用,每个线程对这个ThreadLocalMap对象的set、get等操作都不会影响其他线程,所以直接避免了线程同步的问题,效率较第一种方案高。

ThreadLocalMap的哈希冲突

threadlocalmap的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);
}

//ThreadLocalMap.set方法
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);	//下标通过threadlocal的hashcode&(entry数组长度-1)获得

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        /*
        这里的nextIndex方法就是为了处理哈希冲突
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
        可以看到,它处理哈希冲突的办法为开放定址法中的线性探测再散列法,冲突时每次寻找后一个元素
        */
        ThreadLocal<?> k = e.get();

        if (k == key) {		//如果当前位置有值,就将value覆盖
            e.value = value;
            return;
        }

        if (k == null) {	//当前索引的entry还未使用,直接将key-value放入
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

一个小问题

ThreadLocal中使用的不是Entry数组存储吗,为什么能存储键值对呢?

我们来看看Entry数组的定义:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);	//调用了父类WeakReference的构造方法
        value = v;
    }
}

public WeakReference(T referent) {
    super(referent);	//调用了父类Reference的构造方法
}

Reference(T referent) {
    this(referent, null);	//调用了下面的构造方法
}

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

可以看到,Entry间接继承于Reference,也就是说它从Reference类继承了referent这个成员变量,Entry的构造方法最终调用了Reference的构造方法,将ThreadLocal存储在了成员变量referent中作为 “键” 。

内存泄漏问题

原因

  • ThreadLocalMap的生命周期跟Thread一样长
  • 发生GC时,弱引用Key指向的ThreadLocal对象会被回收,而Value强引用指向的对象可能不会被回收。

解决办法

在使用完ThreadLocal对象后,调用它的remove方法来断开value强引用。

下面看看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) {
            e.clear();	//清除弱引用
            expungeStaleEntry(i);
            return;
        }
    }
}

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;		//移除value强引用
            tab[i] = null;		//移除当前索引指向的的Entry对象
            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;
}

参考资料

threadlocal为什么这么设计?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值