欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

线程局部变量ThreadLocal实现原理

程序员文章站 2024-01-26 15:17:40
ThreadLocal,即线程局部变量,用来为每一个使用它的线程维护一个独立的变量副本。这种变量只在线程的生命周期内有效。并且与锁机制那种以时间换取空间的做法不同,ThreadLocal没有任何锁机制,它以空间换取时间的方式保证变量的线程安全。 本篇从源码方面分析ThreadLocal的实现原理。 ......

  threadlocal,即线程局部变量,用来为每一个使用它的线程维护一个独立的变量副本。这种变量只在线程的生命周期内有效。并且与锁机制那种以时间换取空间的做法不同,threadlocal没有任何锁机制,它以空间换取时间的方式保证变量的线程安全。

  本篇从源码方面分析threadlocal的实现原理。

  

  先看一下threadlocal类图结构

  线程局部变量ThreadLocal实现原理

  suppliedthreadlocal主要是jdk1.8用来扩展对lambda表达式的支持,有兴趣的自行百度。

  threadlocalmap是threadlocal的静态内部类,也是实际保存变量的类。

  entry是threadlocalmap的静态内部类。threadlocalmap持有一个entry数组,以threadlocal为key,变量为value,封装一个entry。

  

  下面以一张图简要说明thread,threadlocal,threadlocalmap和entry的关系。

  线程局部变量ThreadLocal实现原理

  说明一下上图:

  1. 一个thread拥有一个threadlocalmap对象
  2. threadlocalmap拥有一个entry数组
  3. 每个entry都有k--v
  4. entry的key就是某个具体的threadlocal对象

  下面分析主要方法。

  1、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);
    }

  这里可以看出:一个thread只拥有一个threadlocalmap对象;具体存值调用的是threadlocalmap的set(),传入的参数key就是当前threadlocal对象。

  再看看threadlocalmap的set()方法:

private void set(threadlocal<?> key, object value) {
            entry[] tab = table;
            int len = tab.length;
            int i = key.threadlocalhashcode & (len-1); // 1

            for (entry e = tab[i];  // 2
                 e != null;
                 e = tab[i = nextindex(i, len)]) {
                threadlocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replacestaleentry(key, value, i);
                    return;
                }
            }

            tab[i] = new entry(key, value); // 3
            int sz = ++size;
            if (!cleansomeslots(i, sz) && sz >= threshold) // 4
                rehash();
        }
  1. 通过key的hashcode与数组容量 -1 取模,计算数组index
  2. 从当前index开始遍历,清除key为null的无效entry
  3. 将k-v封装为entry,并放入数组
  4. 判断是否需要进行entry数组扩容。threshold的值为数组容量的2/3。

  看看扩容的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; // help the gc
                    } 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;
        }

  这里主要就是扩容为原先的2倍。然后遍历旧数组,根据新数组容量重新计算entry在新数组中的位置。

 

  2、get()

  threadlocal的get()方法如下:

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

 

  threadlocalmap的getentry()方法如下:

private entry getentry(threadlocal<?> key) {
            int i = key.threadlocalhashcode & (table.length - 1); // 1
            entry e = table[i];
            if (e != null && e.get() == key) // 2
                return e;
            else
                return getentryaftermiss(key, i, e); //3
        }

private entry getentryaftermiss(threadlocal<?> key, int i, entry e) {
            entry[] tab = table;
            int len = tab.length;

            while (e != null) { //4
                threadlocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungestaleentry(i);
                else
                    i = nextindex(i, len);
                e = tab[i];
            }
            return null;
        }
  1. 计算index
  2. 当前index上的entry不为空且key相同,直接返回
  3. 否则去相邻index寻找
  4. 循环查找,发现无效key就清除。找到就结束循环。

3、remove()

public void remove() {
         threadlocalmap m = getmap(thread.currentthread());
         if (m != null)
             m.remove(this);
     }
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;
                }
            }
        }

  处理方式和查找保存类似,删除对应entry后都会去除key为null的无效元素。

 

注意

static class entry extends weakreference<threadlocal<?>> {}

  threadlocal可能存在oom问题。因为threadlocalmap是使用threadlocal的弱引用作为key的,发生gc时,key被回收,这样我们就无法访问key为null的value元素,如果value本身是较大的对象,那么线程一直不结束的话,value就一直无法得到回收。特别是在我们使用线程池时,线程是复用的,不会杀死线程,这样threadlocal弱引用被回收时,value不会被回收。

  在使用threadlocal时,线程逻辑代码结束时,必须显示调用threadlocal.remove()方法。