ThreadLocal 学习

一、ThreadLocal的介绍

 ThreadLocal 是线程thread的维护的一个变量,在jdk8中声明如下:

 ThreadLocal.ThreadLocalMap threadLocals = null;

  类型:   ThreadLocalMap  类似于HashMap结构 ;key保存着ThreadLocal对象,value保存着对应的值。

  作用:  每一个线程都可以独立地改变自己的副本,修改变量时候,不会影响其它线程。


二、ThreadLocal核心方法代码的介绍

2.1  set 方法

 /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    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 getMap(Thread t) {
        return t.threadLocals;
    }

set方法很简单,就是获取当前线程t的threadLocals对象,并向map进行set操作

2.2  get 方法

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

get方法也是从threadLocals获取相应的值。

通过上面代码可知,get和set方法都是调用threadLocalMap中getEntry和set方法。 下面我们将详细介绍threadLocalMap,并分析getEntry和set方法的具体实现

三、ThreadLocalMap核心代码分析

3.1 ThreadLocalMap 定义

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

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

private Entry[] table;

private static final int INITIAL_CAPACITY = 16;
private int threshold; // Default to 0

       
        private void setThreshold(int len) {
            threshold = len * 2 / 3;

1、关于ThreadLocalMap,类似Hashmap一样,有一个Entry类被用来存放Map的数据,也有一个Entry[]数组。

2、table 初始化大小是16

3、table 增长的阈值是 table长度的2/3

3.2 ThreadLocalMap set函数

 private void set(ThreadLocal key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            //首先获取新的entry位置 i
            int i = key.threadLocalHashCode & (len-1);
            //然后从i的位置开始遍历table
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();
                //如果存在key相同,说明该线程设置过该ThreadLocal,那么就直接赋予新的值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果key是null,这是因为Entry继承的是WeakReference,在垃圾回收时候已经回收
                if (k == null) {
                    //出现null需要进行置换过期entry,具体见下面代码
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //执行到这里,tab[i]位置没有被占用,此时需要将entry放入到table中
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //由于弱引用,存在一些key为空的情况,所以先要清除无用数据,才能判断现在的size有没有达到阀值threshhold
            //如果没有要清除的数据,并且达到阀值,那就要执行扩容:rehash()
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

3.3 ThreadLocalMap replaceslateentry函数

      如果Entry中的key为空,则说明ThreadLocal已经被垃圾回收,则调用replaceStaleEntry方法,具体看看replaceStaleEntry做了哪些操作

/***
        ** key :set进来的key值
        ** value: set进来的value值
        ** staleSlot : set函数hash出来的位置
        ***/
        private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //staleSlot向前找到table中第一个脏了的数据的下标
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // 找到参数key,后面的第一个过期Entry
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // 如果我们找到了当前table存在此key,那么需要就需要把它跟staleSlot位置过期的数据进行交换,保证hash表的顺序
                // 那么剩下的过期Entry呢,就可以交给expungeStaleEntry方法来擦除掉或者执行rehash方法。
                if (k == key) {
                    e.value = value;

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

                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 清除过期数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // 如果没有在后向查找中找到过期数据,那么slotToExpunge设置成i,并在后面进行清除
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 如果以上的查找都没有找见key的话,首先将value设置null,然后new Entry<key, value>进tab[staleSlot]
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 如果有其他的脏数据,依然要擦除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

  清理table方法的代码如下

/***
        ** 清理过期数据
        ** 传进来的staleSlot就是null键值的table数组的下标。
        ***/
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 擦除掉这个脏数据
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 然后从staleSlot的nextIndex开始循环,停止条件是Entry e = 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 {
                    // 重新计算哈希值,并且移动Entry
                    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;
        }

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

1、启发式扫描table,其中i:是扫描的位置;扫描log2n单元;

2、它试图在i和log2n步内找到一个key被gc清理过,如果找到调用expingeslateentry方法进行清理,然后在当前i位置继续寻找log2n步内被gc清理过的内容,以此类推。

3.4 ThreadLocalMap rehash函数

在set函数中最后一行, 如果没有要清除的数据,并且达到阀值,那就要执行扩容:rehash()

  private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

1、首先调用清除函数。

2、然后继续判断size是否大于等于 theshold*3/4,如果大于需要resize()来调整table数组长度

下面介绍一下resize函数

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            //resize会把容量扩大两倍
            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; // Help the GC
                    } else {
                        //重新计算哈希,并放到newTab原来没有被占用的桶中
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            //重新设置阀值,大小,table指针
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

此代码注释是参考:https://www.jianshu.com/p/0ce314da0248 








x

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`ThreadLocal` 是一个 Java 类,用于在多线程环境下为每个线程提供独立的变量副本。通常情况下,在多线程环境下共享变量可能会导致线程安全问题,而 `ThreadLocal` 可以为每个线程提供一个独立的变量副本,从而避免了这个问题。 在使用 `ThreadLocal` 时,每个线程可以通过 `get()` 方法获取到自己的变量副本,而且这个副本只能被当前线程访问和修改。每个线程都有自己独立的变量副本,不会相互干扰。 例如,下面的代码演示了如何使用 `ThreadLocal` 存储和访问一个字符串变量: ``` public class ThreadLocalDemo { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { threadLocal.set("Hello, ThreadLocal!"); String value = threadLocal.get(); System.out.println(value); // 输出结果:Hello, ThreadLocal! } } ``` 在上面的代码中,我们定义了一个名为 `threadLocal` 的静态变量,它的类型为 `ThreadLocal<String>`,表示它可以为每个线程提供一个独立的字符串变量。然后在 `main` 方法中,我们通过 `threadLocal.set()` 方法为当前线程设置了一个字符串变量,然后通过 `threadLocal.get()` 方法获取到了这个字符串变量,并输出到控制台上。 需要注意的是,每个线程都需要通过 `get()` 方法获取自己的变量副本,并且在使用完毕后需要及时调用 `remove()` 方法将变量副本从内存中清除,以免造成内存泄漏。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值