关于java:ThreadLocal

58次阅读

共计 4202 个字符,预计需要花费 11 分钟才能阅读完成。

同一个 threadlocal 变量所蕴含的对象,在不同的线程中是不同的正本。

既然是不同的线程领有不同的正本且不容许其余线程拜访,所以不存在共享变量的问题。

解决的是变量在线程间隔离在办法或类之间共享的问题。

解决这个问题可能有的两种计划

第一种:一个 threadlocal 对应一个 map,map 中以 thread 为 key

这种办法多个线程针对同一个 threadlocal1 是同一个 map,新增线程或缩小线程都须要改变 map,这个 map 就变成了多个线程之间的共享变量,须要额定机制比方锁保障 map 的线程平安。

线程完结时,须要保障它所拜访的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存透露。-----为什么会导致内存透露呢?见下文

第二种:一个 thread 对应一个 map,map 中以 threadlocal 为 key 辨别

Thread 中有一个变量

ThreadLocal.ThreadLocalMap threadLocals = null;// 每个线程保护一个 map

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;

ThreadLocalMap 保护了一个 table 数组,存储 Entry 类型对象,Entry 类型对象以 ThreadLocal 为 key,任意对象为值的健值对

ThreadLocal 的 get set

 public T get() {Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);// 返回以后线程保护的 threadlocals 变量
        if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();}
    
    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;}
    

Entry key 的弱援用以及内存透露

为什么说 threadlocal 会存在内存透露:

每个 thread 保护的 threadlocalmap key 是指向 threadlocal 的弱援用,当没有任何其余强援用指向 threadlocal 的时候,gc 会把 key 回收。

但 value 是是 thread 指向的强援用,thread 不完结,value 不会被回收。

所以当 threadlocal 不可用但 thread 还在的这段时间内,会存在所说的内存透露。尤其当应用线程池的时候,线程被复用。

jdk 有没有相应的解决:

再回到 threadlocalmap 的 get set 办法

public T get() {Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); // 获取以后线程的 map
        if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this); // 找到以后的 threadlocal
            if (e != null)
                return (T)e.value;// 取值
        }
        return setInitialValue();// map 为 null 或者 map 找不到指定 key 时,初始化根本值,不开展}
    
    //getEntry 函数
      private Entry getEntry(ThreadLocal key) {int i = key.threadLocalHashCode & (table.length - 1);// 依据 key 计算索引
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);//table 中该索引地位对象 e 为 null 或者 索引地位 key 不符进入 getEntryAfterMiss
        }
        
   //getEntryAfterMiss 函数
     private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {Entry[] tab = table;
            int len = tab.length;
            // 遍历 table 始终到找到了 k=key 的地位,返回相应对象 e
            // 遍历过程中如果遇到了 k 为 null,即调用 expungeStaleEntry 清理该 entry,即后面所说的内存透露,这里是解决的一个机会
            while (e != null) {ThreadLocal k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len); // 循环遍历 table,ThreadLocal 采纳的是凋谢地址法,即有抵触后,把要插入的元素放在要插入的地位前面为 null 的中央
                e = tab[i];
            }
            return null;// 如果是 e 为 null  返回 null
        }
   //expungeStaleEntry 函数
  private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;
            int len = tab.length;
            // expunge entry at staleSlot:key 为 null 的索引地位的对象.value 置为 null,对象也置为 null
            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;  // 遍历是从 staleSlot 之后到遇到的第一个 e 为 null
                 i = nextIndex(i, len)) {ThreadLocal k = e.get();
                if (k == null) {// 遍历的过程中遇到 key 为 null 做和下面同样的解决
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else { //key 不为 null 的从新 hash
                    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;
        }
 
        // 再看 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);
    }
    
    // 重点在 map.set 函数
      private void set(ThreadLocal key, Object value) {// We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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)]) { // 如果依据索引找到的 entry 不是空的
                ThreadLocal k = e.get();
                if (k == key) {  //key 雷同,value 间接笼罩
                    e.value = value;
                    return;
                }

                if (k == null) {  // 遍历过程中 key 为 null,革除
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
                        // 下面没有解决掉,找到第一个为 null 的能用的坑位,new 一个 entry 放入
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();}

能大抵看到,下面的代码中,threadlocalmap 的 get set 都会做对 key 为 null 的革除工作,从而解决了下面说的内存透露问题,只是这种解决依赖对 set get 的调用

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

综上所述,很多中央会看到有这样的两条倡议:

1 . 使用者须要手动调用 remove 函数,删除不再应用的 ThreadLocal.

2 . 还有尽量将 ThreadLocal 设置成 private static 的,这样 ThreadLocal 会尽量和线程自身一起沦亡。

正文完
 0