跳至主要內容

ThreadLocal的用法

代码小郭...大约 3 分钟JAVA并发编程

ThreadLocal的用法

一、原理

ThreadLocal(线程本地变量),是一个以 ThreadLocal 对象为键、任意对象为值的存储结构,底层是map键值对方式。

查看Thread类源码可以看到每个线程实例都会有自己的一个成员变量threadLocalMap:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap这个结构被附带在每个线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。我们可以通过 set(T)方法来设置一个值,在当前线程下再通过 get()方法获取到原先设置的值。 通过ThreadLocalMap的set方法可以看出,ThreadLocalMap是一个哈希表结构。set方法是将value插入到哈希表中的操作。插入可能会出现哈希冲突,使用了线性探测再散列法解决。ThreadLocalMap的key是当前的ThreadLocal。

查看set方法源码:

    public void set(T value) {
        //获取当前线程引用
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量threadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 将值存储到ThreadLocalMap中
            map.set(this, value);
        } else {
            // 创建ThreadLocalMap,并存储值
            createMap(t, value);
        }
    }

    //获取当前线程的成员变量threadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    //创建ThreadLocalMap,并存储值
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }


查看get方法源码:

    public T get() {
        //获取当前线程引用
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量threadLocalMap
        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();
    }

在后端开发中,ThreadLocal有大量的应用场景,尤其是在并发访问的场景下用来保障并发数据安全,比如Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接。还有比如用于web场景保存用户信息。

二、使用示例

ThreadLocal是一个泛型类,泛型表示ThreadLocal可以存储的类型,它的使用非常简单。

举个例子,统计主线程和子线程各自的执行耗时,代码如下:

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {

        TimeRecorder timeRecorder = new TimeRecorder();
        timeRecorder.begin();
        TimeUnit.SECONDS.sleep(1);

        timeRecorder.end();

        new Thread(()->{
            TimeRecorder r = new TimeRecorder();
            r.begin();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            for (int i=0;i<100000000;i++) {
            }
            r.end();
        }).start();

    }


}
class TimeRecorder {
    private static final ThreadLocal<Long> time = new ThreadLocal<>();

    public void begin() {
        time.set(System.currentTimeMillis());
    }

    public void end() {
        long t = System.currentTimeMillis()-time.get();
        System.out.println(Thread.currentThread().getName()+" 耗时:"+t);
    }
}

上面演示了main线程和子线程分别统计各自执行耗时的逻辑。

三、内存泄漏问题

通过查看ThreadLocal源码得知,其中的map元素结构entry继承了一个弱引用WeakReference:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

为什么ThreadLocalMap中的Entry要继承WeakReference,使ThreadLocal作为一个弱引用呢?我们知道,弱引用在发生GC时这个对象一定会被回收。通常来说使用弱引用是为了避免内存泄漏。这里也不例外,ThreadLocal使用弱引用可以避免内存泄漏问题的发生。

然而,仅仅将Entry继承WeakReference,只依赖这一点来避免内存泄漏是不太现实的。因为在使用ThreadLocal时存在以下引用链路:

如果我们的Thread一直在运行,Value是强引用的类型,那么此时由于强引用的Value无法被GC自动回收,此种情况下也可能出现内存泄漏的问题。因此,一般情况下,在不需要使用这个ThreadLocal变量的使用,需要主动调用ThreadLocal.remove方法来避免内存泄漏的风险。

你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3