java ThreadLocal

本文详细介绍了ThreadLocal的概念及其在Java中的实现原理。包括ThreadLocal如何通过避免共享数据来消除同步问题,其内部实现机制,以及如何预防内存泄露等问题。

什么是thread local?

线程局部(thread local)是在学习并发时,会经常遇到的一个专业名词,它是一种通用的概念,而非java专有的抽象。即表示线程私有的变量,从而直接避免了使用共享数据所需要面临的同步问题。

ThreadLocal类

ThreadLocal类是java对线程局部提供的java api。它的对外提供的方法有点类似java 8中的Optional,通过一个容器来封装具体的值。

public T get() {
    //...
}

public void set(T value) {
    //...
}

而在内部实现中,它是通过一个ThreadLocalMap实现,每个Thread对象都有一个唯一的ThreadLocalMap类型的变量。

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap类

ThreadLocalMap类似WeakHashMap,特点是采用比较特殊的HashMap实现:

  • 采用弱键,通过一个继承WeakReference的Entry来引用键。
  • 使用线性探测法来解决哈希冲突问题。

在ThreadLocal中,使用ThreadLocal本身作为键,插入数据到ThreadLocalMap中。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

被继承WeakReference的Entry所引用:

static class Entry extends WeakReference<ThreadLocal<?>> {
}

ThreadLocalMap与内存泄露

当ThreadLocal实例本身没有强引用,导致Entry中的弱键被GC回收时,value就成了无效数据,但因为是强键而存在于内存中,这就产生了内存泄露。
726809-20180802213306442-1807081879.png
(图片来自http://www.cnblogs.com/onlywujun/p/3524675.html)

为了最小化内存泄露的可能性,ThreadLoal在调用get(),set(),remove()方法时,将会触发ThreadLocalMap中对无效value的清理方法。但如果没有调用相关方法,内存泄露就会一直存在。[^1]

因此最佳实践是,使用完ThreadLocal后,调用remove()方法对value进行清理。

ThreadLocal会被回收吗?

ThreadLocalMap使用WeakReference来关联ThreadLocal对象,如果ThreadLcoal对象没有了强引用,那么ThreadLocalMap中的WeakReference将可能被清理,导致value无效。

那么在Web开发中,可能出现一个请求的处理过程中,保存在ThreadLocal中的User等数据因为GC丢失吗?

答案是不会。

一般情况下,ThreadLocal实例是作为类的静态变量或静态常量来使用的,如:

private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

该实例被类的Class对象所强引用,而类的回收条件是很苛刻的[^2]:

  • 类的所有实例被回收
  • 该类的ClassLoader被回收
  • 该类的Class对象没有被任何地方引用

因为一般情况下,我们不会使用特殊的ClassLoader来加载ThreadLocal实例所在类,因此不用担心ThreadLocal被回收。

如果你对特殊情况感兴趣,也可以参考stackoverflow上面的示例[^3]写一个使用自定义ClassLoader而被回收的类属性的ThreadLocal,如下:

自定义ClassLoader:

public class CustomClassLoader extends URLClassLoader {

    public CustomClassLoader(URL... urls) {
        super(urls, null);
    }

    @Override
    protected void finalize() {
        System.out.println("*** CustomClassLoader finalized!");
    }
}

测试类:

public class ThreadLocalGCTest {
    public static void main(String[] args)throws Exception{
        while (true) {
            loadFoo();
            System.gc();////此时Foo.class和CumstomClassLoader.class将被回收
            Thread.sleep(1000);
        }
    }

    private static void loadFoo() throws Exception {
        CustomClassLoader cl = new CustomClassLoader(new URL("file:C:\\Users\\anonym\\Projects\\java-learning\\out\\production\\java-learning\\"));//根据自己的情况改变路径
        Class<?> clazz = cl.loadClass("thread.threadlocal.ThreadLocalGCTest$Foo");
        clazz.newInstance();
        cl = null;
    }

    public static class Foo {
        private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>();

        public Foo() {
            tl.set(50);
            System.out.println("ClassLoader: " + this.getClass().getClassLoader());
        }

        @Override
        protected void finalize() {
            System.out.println("*** Foo finalized!");
        }
    }
}

测试结果:

ClassLoader: thread.threadlocal.CustomClassLoader@372f7a8d
*** Foo finalized!
*** CustomClassLoader finalized!

析构函数执行,说明类已经被卸载。此时的ThreadLocal实例也已经被回收。

[^1]http://www.cnblogs.com/onlywujun/p/3524675.html
[^2]《深入理解Java虚拟机 第二版》
[^3] https://stackoverflow.com/questions/17968803/threadlocal-memory-leak

转载于:https://www.cnblogs.com/redreampt/p/9410508.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值