接下来我们继续聊get,remove方法。以及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();
}

内容比较简单,不再追诉.

map.getEntry(this)

private Entry getEntry(ThreadLocal<?> key) {
	//threadLocal的下一个nextHashCode与哈希表的长度计算出下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)//如果找到了直接返回
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss(key, i, e)

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {//向后遍历直到遇到空插槽
        ThreadLocal<?> k = e.get();
        if (k == key) //找到了直接返回
            return e;
        if (k == null)//旧条目需要清除
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];//下一个条目
    }
    return null;
}

根据开放寻址法,如果遍历到了空插槽还没找到条目,则表示查找失败,直接返回空。

查找还是比较简单的.

remove

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

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) {//找到了的话进行clear,并清除该插槽
            e.clear();//将引用置为null.以便expungeStaleEntry方法可以清除它
            expungeStaleEntry(i);
            return;
        }
    }
}

public void clear() {
    this.referent = null;
}

从上面可以看出get,set,remove方法都会清除旧条目.但是由于Thread有个threadLocals引用指向了ThreadLocal的table,而table又存放着entry.

内存溢出

很多文章都说thread使用不当的话会导致内存溢出,因为虽然ThreadLocalMap的key虽然为软引用,但是value是强引用.如果value值在被使用的话会导致gc无法回收条目。

但是实际上真的如此吗?

我写了个例子:

 public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);
    Thread thread = new Thread(() -> {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("a");
        threadLocal = null;
        System.gc();
        countDownLatch.countDown();
    });
    thread.start();
    countDownLatch.await();
}

debug如下图: 可以看到虽然key为null了,但是value并没有为null.是内存泄漏了.但是回顾源码,threadLocal在set,get,remove时会去清除旧条目.但是System.gc()之后并没有调用相关方法.没有触发清除动作.

我稍微改了下代码:

 public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);
    Thread thread = new Thread(() -> {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("a");
        threadLocal.remove();
        System.gc();
        countDownLatch.countDown();
    });
    thread.start();
    countDownLatch.await();
}

debug如图: 可以看到内存哈希表里面并没有数据了. 也就说上面的话只对了一半.如果不合理使用threadLocal的话会导致内存溢出.所以在使用threadLocal时一定在用完之后调用remove方法.

调用remove方法除了可以防止内存溢出之外,还可以保证线程数据的干净.在线程池中的线程如果使用threadLocal,但是没有及时remove的话,由于线程池中的线程的重用,会将上一个请求的数据带入到线程这一次请求.典型的就是Tomcate的线程池,自定义用threadLocal存放session的话,会发现A用户拿到了B用户的数据.就是由于没有及时remove导致的。