java基础 - ThreadLocal

ThreadLocal是一个泛型类,作用是实现线程隔离,ThreadLocal类型的变量,在每个线程中都会对应一个具体对象,对象类型需要在声明ThreadLocal变量时指定。

ThreadLocal的使用示例

package org.example.thread;

import org.example.domain.Book;

public class ThreadLocalTest {

    ThreadLocal<Book> localBook = new ThreadLocal<>();

    Book book = new Book();

    public void test() {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                localBook.set(book);
                System.out.println(localBook.get());
                localBook.remove();
            }
        });
        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(localBook.get());

        localBook.remove();
    }

    public static void main(String[] arg) {
        ThreadLocalTest localBookTest = new ThreadLocalTest();
        localBookTest.test();
    }
}

运行结果如下:
在这里插入图片描述
示例代码中,只声明了一个 ThreadLocal 变量localBook,在子线程中设置了localBook的值,但是在最后主线程中进行打印时,发现为null,和子线程中的结果不一样。具体原因还要看看ThreadLocal类,以及内部 set、get等方法的实现。

原理

1、ThreadLocal中有一个静态内部类 ThreadLocalMap,ThreadLocalMap中维护了一个 Entry数组,Entry又是ThreadLocalMap的内部类,用来表示一个KV键值对。部分源码如下:

    static class ThreadLocalMap {

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

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

        private static final int INITIAL_CAPACITY = 16;

        private Entry[] table;

Entry的 key是一个指向ThreadLocal对象的弱引用,value则指向与key对应的Object对象。

2、Thread类中,有一个ThreadLocalMap类型的成员变量,初始为null:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

也就是说,每个线程对象中,都有一个ThreadLocalMap,它里面可以有很多个Entry,每个Entry中的key都指向一个ThreadLocal对象,value则指向与key相对应的Object对象。在线程中调用ThreadLocal对象的set、get方法,实际操作的是当前线程自己的ThreadLocalMap。

3、set方法

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

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

果然,这里先拿到当前线程中的ThreadLocalMap,如果不为空则直接添加一组键值对,key是当前的ThreadLocal变量;
如果map为空,则新建一个map,同样添加一组键值对,key是当前的ThreadLocal变量,然后将map赋值给当前线程的ThreadLocalMap。

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

同理,get方法拿到的也是线程本地的ThreadLocalMap中,与当前ThreadLocal变量对应的value对象。

5、remove方法

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             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) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove方法,移除当前线程的ThreadLocalMap中,与当前ThreadLocal对象对应的键值对(key、value及Entry置空)。

内存泄漏相关

1、为什么ThreadLocalMap中的key要使用弱引用?

弱引用特性:在对象没有被强引用指向,而仅被弱引用指向的情况下,发生垃圾回收时,会直接清理掉该对象。
套用网上出现较多的一张图片来表示ThreadLocal变量的内存结构:
在这里插入图片描述
这里实线表示强引用,虚线表示弱引用。
假设key也是强引用,如果我们把 ThreadLocal Ref这个引用置为null,表示我们不再需要这个ThreadLocal对象,那么发生垃圾回收时,这个变量理应被回收掉;但实际并非如此,因为当前线程中还持有一个强引用,也就是ThreadLocalMap中的key还在指向它,那么只要线程不结束,这个没有实际作用的ThreadLocal对象就一直不会被回收,从而出现内存泄漏。而将key设计为弱引用,就能保证这种情况下ThreadLocal对象被回收,一定程度避免内存泄漏问题。

2、为什么使用完ThreadLocal对象,要在线程中调用remove方法?
虽然弱引用可以使不再使用的ThreadLocal对象被回收掉,但还有一个问题:
Entry中的key被置为了null,对应的value已经无法通过key访问到,然而Current thread -> ThreadLocalMap -> Entry(value) -> my value 这条强引用链仍然存在,也就是说还存在key为null,但value不为null的entry,如果这类entry不被清理掉,还是会导致内存泄露。

key为null的entry如何清理?
在ThreadLocalMap的Entry数组中,key为null的entry也会占用一个数组下标,而Threadlocal的set、get等方法,正是根据key的hashCode计算得到数组下标,然后根据下标找到对应的entry进行操作。
为了维护ThreadLocalMap的可用性,不让这些key为null的无用entry占用过多空间,set、get方法在某些情况下也会对key为null的entry进行清理。但是用户有可能不再需要调用其他ThreadLocal变量的set或get方法,所以这种方式是被动且没有针对性的。

建议在不使用ThreadLocal对象之后,直接调用remove方法,其作用就是将对应entry的key、value置空,并将entry从数组中移除,切断下图中的这三处引用关系,防止内存泄漏。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值