ThreadLocal应用

ThreadLocal作用

ThreadLocal可以保证当前拿到的变量是属于当前访问的线程。也就是每个线程自己的独立小空间。实现了线程之间的数据隔离。

ThreadLocal API

image

ThreadLocal隔离性

public class ThreadTime {

    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) {
        threadLocal.set("哈哈");

        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
            }

        }).start();

        System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
    }
}

结果:

image

ThreadLocal统计方法花费的时间

public class ThreadTime {

    private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static final void beginThread() {
        threadLocal.set(System.currentTimeMillis());
    }

    public static final long endThread() {
        return System.currentTimeMillis() - threadLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadTime.beginThread();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("cost: " + ThreadTime.endThread() + " mills");
    }
}

结果:

image

InheritableThreadLocal实现父子线程数据传递

public static void main(String[] args) {
	InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();

	for (int i = 0 ; i < 5 ; i++) {
		//每个线程的序列号,希望在子线程中能够拿到
		threadLocal.set(i);

		//这里来了一个子线程,我们希望可以访问上面的threadLocal
		new Thread(() -> {
			System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
		}).start();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

结果:

image

ThreadLocal源码

  • set方法
public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程对应的本地Map映射
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将值设置到本地map中
            map.set(this, value);
        else//如果map为空
            //创建线程本地map,将value值进行初始化 
            createMap(t, value);
}

void createMap(Thread t, T firstValue) {
        //初始化线程本地Map
        t.threadLocals = new ThreadLocalMap(this, 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);
}

解释:
获取当前线程对应的map,如果map不为空,就将值设置到map中。如果map为空就将创建Map结构,然后将值放入到链表中。

注意:
可以发现ThreadLocalMap的构造器采用HashMap的套路,初始化节点数组,然后使用键key进行按位与运算计算数组下标,设置最大临界值为初始化容量。

  • get方法
public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的本地map映射
        ThreadLocalMap map = getMap(t);
        // 如果map不为空
        if (map != null) {
            // 获取映射map的链表
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 如果节点不为空,返回节点的值
            if (e != null)
                return (T)e.value;
        }
        //返回初始化的值
        return setInitialValue();
}

private T setInitialValue() {
        // 获取初始化时传入的值
        T value = initialValue();
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的本地map映射
        ThreadLocalMap map = getMap(t);
        // 如果映射map不为空,设置当前值
        if (map != null)
            map.set(this, value);
        else // 如果映射map为空,那么创建map映射,然后初始化值到链表中
            createMap(t, value);
        return value;
}

解释:
① 首先获取当前线程的本地map映射。
② 如果map不为空,那么就走Hashmap的套路,通过键按位与运算计算下标,然后通过下标获取值。
③ 否则会再判断下map是否为空,如果空就创建下ThreadLocalMap,但是最后的效果就是拿到初始化传入的值,初始化传入的值就是null,除非自己重写initialValue方法。

ThreadLocal如何保证线程安全

看源码set方法:

image

ThreadLocal内存泄漏

  • 以getEntry()为例:
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果找到key,直接返回
        return e;
    else
        //如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}
  • getEntryAfterMiss()实现:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // 整个e是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        //如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            //如果key为null,说明弱引用已经被回收了
            //那么就要在这里回收里面的value了
            expungeStaleEntry(i);
        else
            //如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:

ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。

建议:当你不需要这个ThreadLocal变量时,主动调用remove()。

参考

https://zhuanlan.zhihu.com/p/369953316

posted @ 2023-06-02 22:32  sunpeiyu  阅读(4)  评论(0编辑  收藏  举报