多线程之ThreadLocal

前言

  前几天在京东的同学给我打了个电话,聊了下家常,技术宅的我多嘴问了最近有没有学啥? 他说最近有点忙,但抽空也看了几篇博客,他说我考考你吧,我说可以啊,他问我: ThreadLocal 使用不当会导致 OOM 吗?我不假思索的回答:会。他继续追问道:为什么? 我说:因为 ThreadLocal 和操作它的线程绑定在一起,如果操作他的线程不被销毁,与之关联的 ThreadLocal 不会被 GC 。因为使用线程大多都是通过线程池来创建的,因此只要该线程活跃,就不会被线程池销毁,如果我们使用的时候忘记调用 ThreadLocal 的 remove 方法,则 ThreadLocal 保存的值无法被 GC ,如此多了就会发生 OOM 。然后他突然问了一句:为啥 Thread 里的threadLocals 属性的key是弱引用类型的? 这个之前我是不知道的。然后他给我解释了一下,这也是这篇文章的由来,好记性不如烂笔头,顺便验证一下他说的,也是对知识的巩固。

ThreadLocal

  多个线程间共享变量,可能会造成线程不安全的问题,需要加锁来实现线程安全,但是加锁会降低系统的吞吐量。
  但是有些变量就不需要线程间共享。比如数据库连接池里的连接,我们可以通过串行线程封闭技术来安全的使用连接池中的连接。一个线程A从连接池中把连接拿走,连接池保证不把该连接给别的线程,线程A同样不会把连接发布出去,用完之后返回给连接池,这样一个连接总是在一个线程中使用,不会同时被两个线程操作。线程A保存数据库连接就可以使用 ThreadLocal 来保存,可以在多个方法中获取操作数据库,用完删除即可。(生产者和消费者模式也是使用串行线程封闭技术,大家可以考虑下。)
   ThreadLocal 里的数据,其它线程无法访问,只要使用者不把数据发布出去,就可以安全操作它们。我们来看看如何一个 demo 来看下 ThreadLocal 如何使用:

public class NotThreadSafe {
    private ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> Integer.MIN_VALUE);

    public void increment() {
        Integer countValue = count.get();
        countValue++;
        count.set(countValue);
    }

    public void decrement() {
        Integer countValue = count.get();
        countValue--;
        count.set(countValue);
    }

    public int getValue() {
        return count.get();
    }

    public void remove() {
        count.remove();
    }

    public static void main(String[] args) {
        NotThreadSafe notThreadSafe = new NotThreadSafe();
        new Thread(() -> {
            try {
                notThreadSafe.increment();
                System.out.println("increment i=" + notThreadSafe.getValue());
                notThreadSafe.decrement();
                System.out.println("decrement i=" + notThreadSafe.getValue());
            } finally {
                notThreadSafe.remove();
            }
        }).start();
    }
}
ThreadLocal与Thread如何绑定

  上文我说过 ThreadLocal 会与它所属的 Thread 绑定,这个绑定是什么意思呢,下面我们来看看 Thread的一处源码:

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

  注释 的译文:与此线程相关的ThreadLocal值。这个映射由ThreadLocal类维护。
  ThreadLocal.ThreadLocalMap 类型的 threadLocals 属性是保存与当前线程相关的ThreadLocal 实例,该mapThreadLocal来维护。下面我来看看 ThreadLocalMap 到底是个什么。
   先来看下官方解释:
ThreadLocalMap 是一个定制的散列映射,只适用于维护线程本地值。没有任何操作被导出到 ThreadLocal 类之外。类是包私有的,允许在类线程中声明字段。为了帮助处理非常大且长期存在的用法,哈希表条目对键使用 WeakReference 。但是,由于没有使用引用队列,所以只有在表空间不足时才会删除陈旧的条目。
   ThreadLocalMap其实就是一个散列表和HashMap差不多,只不过是定制的,只用于维护线程本地的值。为了帮助处理非常大且长期存在的用法,哈希表条目对键使用 WeakReference,现在大家比较关心这个散列表的 对应着的是什么吧?我们来看看ThreadLocalMap 中的Entry是如何定义的:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /**与ThreadLocal关联的值。*/
            Object value;
            //key 就是ThreadLocal 对象本身,而值就是大家想要保存的数据如数据库连接
            Entry(ThreadLocal<?> k, Object v) {
              //将k置为弱引用
                super(k);
                value = v;
            }
        }

看了源码可知:ThreadLocalMap是以ThreadLocal 实例为健,用户要线程私有化的数据为值的散列表,并且 还是弱引用类型的。
   下面我们来讲下 ThreadLocal 如何与线程关联起来的。ThreadLocal 实例在调用 setget 的时候,会先获取当前线程的threadLocals 属性,判断 threadLocals 属性是否为空,若不为空则进行获取或者添加操作,否则会创建一个 ThreadLocalMap 实例赋给当前线程的属性 threadLocals;然后往里 put 一个键值对,当getset 方法时都是当前ThreadLocal实例,只不过是get时,值为ThreadLocal 中initValue方法返回的值,默认为 null ;方法为set时,则为调用者传进的实参。

ThreadLocal 的 get方法:
    public T get() {  
        Thread t = Thread.currentThread();
        //获取当前线程的 threadLocals 属性
        ThreadLocalMap map = getMap(t);
        if (map != null) {
         //若threadLocals属性不为空,以 this(当前 ThreadLocal)实例为健获取对应的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //若已经设置过值或者有初始值就直接返回
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
      //当前线程的threadLocals属性为空或者没有设置过值时设置初始值
        return setInitialValue();
    }
   /**
     * 获取与给定线程相关联的ThreadLoal散列表 
     * @param  t 当前线程
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    private T setInitialValue() {
        //调用 initialValue 获取初始值默认为 null
        T value = initialValue();
        Thread t = Thread.currentThread();
         //获取当前线程的 threadLocals 属性
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果已经创建与当前线程关联的 ThreadLoal 散列表,则直接设值
            map.set(this, value);
        else
            //创建与当前线程相关的 ThreadLocal 散列表 并设值
            createMap(t, value);
        return value;
    }
   /**
     *  创建与当前线程关联的 ThreadLocal 散列表,
     *  并将它赋值给给定线程的 threadLocals 属性
     * @param t 当前线程
     * @param ThreadLocal散列表第一个Entry的初始值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocal 中的set 方法

   /**
     * 向当前线程的线程私有变量设置指定的值
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        //获取当前线程的 threadLocals 属性
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果已经创建与当前线程关联的 ThreadLoal 散列表,则直接设值
            map.set(this, value);
        else
            //创建与当前线程相关的 ThreadLocal 散列表, 并设值
            createMap(t, value);
    }

  下面我们用一张图来概括下线程,线程私有变量以及用户定义的数据之间的关系,加深我们的理解:
线程,线程私有变量以及用户定义的数据之间的关系

后记

   我们讨论了 ThreadLocal 如何使用以及其与 Thread 之间关系,下一节我们讨论下 ThreadLocalMap 的具体实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值