Java并发编程系列:ThreadLocal源码分析

1.ThreadLocal示例

ThreadLoca它是一个线程的局部变量,也就是说只有当前线程可以访问,他肯定是线程安全的。
ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。在spring的作用域中用到了它。

public class TestThreadLocal implements Runnable {
	public static void main(String args[]) {
	ThreadLocal<String> tl = new ThreadLocal<>();
		tl.set("1");
		System.out.println(tl.get());
	}
}

JDK的实现主要包括如下三个类:

  • Thread
    Thread类中定义了了属性 ThreadLocal.ThreadLocalMap threadLocals = null;
  • ThreadLocal
    是对ThreadLocalMap 的代理,其实变量都是保存在ThreadLocalMap 中的
  • ThreadLocal.ThreadLocalMap
    该类是Map的实现,底层其实是数组
2. HashCode

下面分析ThreadLocal类几个成员变量。

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
    new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

ThreadLocal 通过 threadLocalHashCode 来标识每一个 ThreadLocal 的唯一性。每一个ThreadLocal对象的HashCode都是不同的,也就是说不通与线程A关联的ThreadLocal在B线程是不会get到值的。threadLocalHashCode 通过 CAS 操作进行更新,每次 hash 操作的增量为 0x61c88647。这个数字也是有很多学问的。

3.ThreadLocal set/get

set方法,首先是拿到当前线程对象,然后通过getMap拿到线程的ThreadLocalMap,并将值放入ThreadLocalMap中,而ThreadLocalMap可以理解为一个HashMap,以ThreadLocal实例对象为key,而value就是保存的值。

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		//如果为空,则创建
		createMap(t, value);
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get方法,首先获取当前线程的ThreadLocalMap对象,然而将自己作为Key值取得内部的实际数据

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

当线程退出时,Thread类会进行一些清理工作,其中就包括ThreadLocalMap,如果使用线程池比如固定大小的线程池,就不会退出,因此一些大的对象设置到ThreadLocal中,可能会出现不在使用对象了,但是对象却无法被回收。

4.ThreadLocalMap

ThreadLocalMap是ThreadLoca的内部类,类图如下,Entry又是ThreadLocalMap的内部类。
ThreadLocal
属性INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry 类型的数组,用于存储数据;size 代表表中的存储数目; threshold 代表需要扩容时对应 size 的阈值
Entry类的定义如下:

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

Entry类继承了WeakReference<ThreadLocal<?>>,即每个Entry对象都有一个ThreadLocal实例的弱引用作为key,即Map不回影响ThreadLocal对象本身的回收,假如Entry中的key使用强引用,当外部变量声命在方法内,方法执行完毕栈帧销毁变量丢失,强引用只存在Map中,因此造成key迟迟不能回收,如果是弱引用则不影响ThreadLocal对象的回收,当ThreadLocal对象被回收之后,Map中会存在null key的entry,set方法中会处理null key的value。但弱引用并不能完全解决内存泄漏。
ThreadLocalMap的构造方法ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue):

 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
	 table = new Entry[INITIAL_CAPACITY];
	 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
	 table[i] = new Entry(firstKey, firstValue);
	 size = 1;
	 setThreshold(INITIAL_CAPACITY);
}

构造函数的第一个参数就是本ThreadLocal实例(this),第二个参数就是要保存的线程本地变量。构造函数首先创建一个长度为16的Entry数组,然后根据threadLocalHashCode计算出firstKey对应的哈希值,然后存储到table中,并设置size和threshold。注意每个ThreadLoca的threadLocalHashCode都是不同的。

计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现(和HashMap中的思路相同)。正是因为这种算法,我们要求size必须是 2的指数,因为这可以使得hash发生冲突的次数减小。

ThreadLocalMap的Set方法:

 private void set(ThreadLocal<?> key, Object value) {
    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)]) {
    	//取出key
        ThreadLocal<?> k = e.get();
		//如果key相同,进行覆盖
        if (k == key) {
            e.value = value;
            return;
        }
		//如果key为空,stale(陈旧的)key,就将其替换为当前的key和value:
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

如果发生冲突了,就会通过nextIndex方法再次下表:

private static int nextIndex(int i, int len) {
	return ((i + 1 < len) ? i + 1 : 0);
}

可以看出ThreadLocalMap 解决冲突的方法是 线性探测法(不断加 1),而不是 HashMap 的 链地址法。

如果没有冲突,将Entry赋值到可以用的坐标系下,并且每一次的添加都会出发cleanSomeSlots,

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

一旦发现一个位置对应的 Entry 所持有的 ThreadLocal 弱引用为null,就会把此位置当做 staleSlot 并调用 expungeStaleEntry 方法进行整理 。

如果清除失败并且容量大于阈值,那么调用rehash方法。

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

rehash 操作会执行一次全表的扫描清理工作,并在 size 大于等于 threshold 的四分之三时进行 resize。但注意在 setThreshold 的时候又取了三分之二。
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;
        }
    }
}

什么时候无用的 Entry 会被清理:

  • Thread 结束的时候
  • 插入元素时,发现 staled entry,则会进行替换并清理 插入元素时,
  • ThreadLocalMap 的 size 达到 threshold,并且没有任何 staled entries 的时候,会调用 rehash 方法清理并扩容 调用
  • ThreadLocalMap 的 remove 方法或set(null) 时

可以看到无用的 Entry 只会在以上四种情况下才会被清理,这就可能导致一些 Entry 虽然无用但还占内存的情况。因此,我们在使用完 ThreadLocal 后一定要remove一下,保证及时回收掉无用的 Entry。

内存泄漏:
在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从 Thread 中Entry连接过来的强引用. 只有当前thread结束以后, thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值