ThreadLocal的深入理解

0. 前言

·  最近对多线程,高并发产生了兴趣(其实是面试大厂的时候对性能提升这一块有较高的要求 ),以前只会用 synchronized 关键字来实现线程安全,其实这是比较狭隘的,解决线程安全的问题有很多种方法,比如:非阻塞同步(乐观锁机制:在一个可能有线程安全问题的方法中,去判断此操作的结果是否符合预期,如果不符合就放弃此操作的输出,并重新对开始一轮新的操作直到符合预期后再输出),使用可重入的代码(无法对共享变量进行修改的代码)以及我们今天要提及的ThreadLocal
·  我写博客主要还是为了给自己看,多了一些自己的理解,但是可能并不精炼。而讲ThreadLocal的好文已经有很多了,这也不是什么新技术,各位还是看比较权威的文章更好一点,我这里实名推荐一篇:(转载)ThreadLocal实现原理但是我仍要强烈鄙视一下这篇文章的博主,明明是转载的,却不标明出处。

1. 使用场景

·  我在前言里说了辣么多解决线程安全的方法,这时候人本能就会想到两个问题:1. 哪个最好?2. 如果有最好的,还要用其他的干什么。
·  在这里我仅说一下我的理解:

  1. synchronized方法是最能解决线程安全问题的方法,因为多线程能够安全地操作同一块内存地址的区域,这是其他方法做不到的。 但是这个方法弊端也很明显,多线程串行执行一个任务,效率极低(当然现在的锁进行了很多的优化措施,至少不会让某个线程在一个任务上长时间阻塞了,这个不是重点)。
  2. 非阻塞同步方法解决了线程串行的问题,但是存在ABA隐患(这也不是重点,所谓ABA请自行搜索啦)。
  3. ThreadLocal方法,它并不能解决一部分的线程安全问题,因为就算你使用了ThreadLocal,你也无法阻止多线程操作同一块内存空间的隐患,而ThreadLocal却解决了在web领域的一个重大的问题:各线程的独立操作共享数据

1.1 ThreadLocal使用场景解释

·  很多小伙伴都懵逼了,“独立操作共享数据”是什么意思?其实很简单,我这里的解释需要涉及一点Servlet的知识才可以懂。

  • ThreadLocal是干嘛的
    ·  一句话总结:ThreadLocal实例能够把某个对象与当前thread绑定。于是在任何时刻,当前thread都能通过ThreadLocal获得该对象。
  • Servlet的线程安全问题
    ·  我们都知道原生的Servlet是单例的,会产生线程安全问题,但是这具体是怎么产生的呢?这里给出一个例子:当线程A和线程B同时去访问同一个Servlet,并同时给request域对象中设置了 相同的键 和 不同的值,这时候,这两个线程就会抢夺资源,导致最终转发给用户的request可能并不是用户想要的。
  • Servlet安全问题的解决思路
    ·  如果,线程A和线程B能够独立操作该Servlet中的request对象,操作的方式仅对自己的线程的可见,这样这条线程在整个“客户端–服务端–客户端”的流程中,都能保存数据的一致性。所以,解决方法就是:每个线程都会创建一个仅对自己可见的request代理对象,当线程死亡后,该request代理对象也随之销毁。“创建一个仅对自己可见的request代理对象”,就需要用到我们的ThreadLocal
  • ThreadLocal使用场景的思考
    ·  可见,ThreadLocal方法,并不能实现多线程安全操作共享变量(如果不新建Request代理对象,就会依然存在安全问题,因为Request对于多线程而言还是单例的),所以我认为它不能解决所有线程安全问题,(但是,它的优势在于:每个线程看似都能并发地独立"修改"某个共享变量(将thread与这个共享变量的代理对象绑定),极大地提高时间利用率
    ·  关于我的对线程安全的理解,我举个很通俗的比方,耳熟能详的 “生产者 — 产品 — 消费者” 问题上,ThreadLocal就不能帮我们解决线程安全问题,因为创建产品的代理对象没有意义。而在上述的Servlet问题中,我们并不关心线程间操作request的顺序是什么样的,我们只需要各线程能且只能看到自己的数据而已。
  • 个人的理解
    ·  根据我的理解,threadLocal被设计出来主要是用来解决并发条件下的数据隔离问题,是用空间换取时间的一种手段。其实归根结底,threadLocal用来解决Servlet的线程安全问题,理念上就是用了多例模式而已(给共享变量创建副本),只不过这多出来的‘例’仅对当前线程可见。不过也不尽然,如果我们非要把与某个线程绑定的对象再与其他线程绑定也是没有问题的,但这就失去了使用threadLocal的意义,我们一般只会把某个对象与特定的一个thread绑定,这样我们就可以通过这个thread在任何时刻访问到这个对象,而其他thread无法做到。这意味着threadLocal使用场景并不局限在解决线程安全的问题上 :我们从数据库连接池获取连接的时候,我们只希望一条线程在它的生命周期内只能获取到一个连接对象,且这个连接对象在线程死亡前不会被释放。这就需要我们用threadLocal在当前线程第一次申请连接时,将某个池里的连接与当前线程绑定,以后每次要申请连接,该线程都只会得到这个连接,在线程死亡后,连接被归还。
  • 一句话总结:两种情况下需要用threadLocal
  1. 每个线程希望操作自己独有的对象,如Request对象
  2. 在一个线程中,同一个对象需要在多个方法中共享,如进行事务管理时,某个业务操作需要用到多个DAO方法,这些方法我们希望使用同一个Connection对象。

2. ThreadLocal结构的简单说明

  • 什么是ThreadLocal
    ·  一句话总结:本地线程副本
    ·  这句话有一层重要含义:threadLocal不特定属于某个线程,它用来管理所有的本地线程。也就是说,threadLocal本身是一个全局变量,每个线程通过它来获得仅对自己可见的局部变量
    ·  用刚才Servlet的问题来举个例子(并不准确,但是便于理解):出现安全问题是因为多线程共同访问了同一个Request对象,但是我们使用了ThreadLocal之后,每个线程都只会去访问一个ThreadLocal< Request >对象,并从这个对象中获取只对当前线程可见的Request代理对象。
    ·  这意味着,一个线程其实可以从多个ThreadLocal对象中获取相对应的多个值。即:每个线程既可以通过ThreadLocal< Request >对象获得自己的Request代理对象,也可以同时通过ThreadLocal< Connection >对象获取自己的数据库连接对象。只不过这两者的使用理念不同,前者主要是为了保证多线程之间的数据隔离,后者主要是为了保证对象在单线程中对多方法共享

2.1 对象是如何与线程相绑定的

·  一句话总结:通过ThreadLocal的内部类ThreadLocalMap
·  下面是Thread源码的一部分:

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

·  由源码可知,在Thread内部有一个由ThreadLocal维护的ThreadLocalMap对象。因此我们可知,实现绑定的关键在于这个ThreadLocalMap类,下面是其一部分源码:

	static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
		...
	}

·  由源码可知,ThreadLocalMap本质上是一个Entry数组,Entry又是其内部类。这个内部类的key是一个threadLocal的弱引用(super(k)),value是我们要与当前线程绑定的对象。
·  因此,我们可以简要的画出ThreadLocal的结构图:
在这里插入图片描述
·  在这个图里有几点必要重要的信息:

  1. 全局的ThreadLocal对象在整个存储的结构中,是作为一个key而存在的,而且还是这个key的弱引用。
  2. Map是每个线程独有的,和全局的ThreadLocal不存在从属关系,也就是说,这个map里可以存放多个不同的ThreadLocal对象作为key,这些ThreadLocal对象对于线程的ThreadLocalMap而言是同级的。(虽然很基础,但是我还是提一嘴,这个内部类是静态的,所以ThreadLocalMap本质上不特定属于某一个ThreadLocal对象)

2.2 深入理解的注意事项

·  在讲ThreadLocal如何进行线程的绑定等一系列操作前,我们还是先缕清一些问题。

  • 为什么使用弱引用
    ·  本人在做了一些研究之后,给大家简单地解释一下:在上图中,我们可以明显的看到ThreadLocal对象有两条对其的引用,一条来自外界的引用,一条来自内部Entry对其的引用,因为内部引用的释放我们一般是不愿意去操作的(因为太底层了),因此我们希望只要释放ThreadLocal的外部引用,这个ThreadLocal实例就能被垃圾回收。但是,如果内部引用是强引用,那么即使外部引用被释放了,也有一条强引用链:“CurrentThread Ref ---- CurrentThread ---- ThreadLocalMap ---- Entry ---- Entry.key ---- ThreadLocal” 这样我们就无法有效地回收ThreadLocal了,久而久之GC如果一直回收不掉这个实例,而我们又不使用这个ThreadLocal了,那就导致了内存泄漏,很容易造成OutOfMemory异常。但是,如果这条强引用链最后的引用变成了弱引用,那GC就能顺利释放掉ThreadLocal对象了(GC会回收掉只有弱引用引用的对象)。如果想进一步了解Java内存泄漏的情况,可以看一下我的另一篇博客:HashMap为什么会出现内存泄漏问题 以及 Java中引用类型的存储位置

  • 使用了弱应用就不存在内存泄漏的问题了吗
    ·  显然不是,从图中很容易看出,即使ThreadLocal对象被释放掉了,与线程绑定的Entry的Value并没有被释放掉,而我们也不会再使用这个Entry了,这同样也是内存泄漏的问题(这里的强引用链就不写了)。为了避免这样的问题,在释放掉ThreadLocal的外部引用前,我们一般会利用ThreadLocal对象先把这个Entry对Value的强引用释放掉,这就是在下一章要提到的threadLocal.remove()方法。

  • 为什么要使用ThreadlLocal进行对象与线程的绑定
    ·  仔细想一想,如果让我们自己设计,我们可能会想着直接用线程去绑定某个对象:在线程内部维护一个List<HashMap<T,V>>的结构:T表示要绑定的对象类型,V是这个对象实例的引用。但是仔细一想,这样做实在是不精明,因为首先这个结构过于复杂,其次并非所有线程都需要这个List来保存本地变量副本的(如用synchronized去保证线程安全的情况),而HashMap的构造必需分配内存空间,当线程量多的时候造成的内存空间浪费将会十分严重。而我们反观ThreadLocalMap的设计,首先结构简单,其实Thread类维护的这个Map采用懒加载的方式,不使用的话就不会在堆空间中分配内存,可谓是鬼斧神工。

3. ThreadLocal部分源码

·  本节内容十分简单,主要涉及到ThreadLocal自身的set(), get(), remove() 方法。假设我们的对象为ThreadLocal<?> tl = new ThreadLocal<>(); 代码的详解,请参考这篇博客:(转载)ThreadLocal实现原理,我这里仅仅只是概括而已。

  • tl.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);
    }

·  这源码再清晰不过了:获取当前线程对象,并得到其内部维护的map,并把要绑定的对象放入map,其键为this,表示threadLocal对象的弱引用。createMap(t, value)体现了懒加载的特性,源码就不贴了,就是分配内存空间,这里主要看一下map.set(this,value)

		private void set(ThreadLocal<?> key, Object value) {
            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

·  这里再详细的源码就不贴了,我说明一下map.set做了什么事情:

  1. 这里必须说明一下,threadLocalMap采用线性探测的方式向Entry数组里填入数据,至于什么线性探测这里不细说了,这是解决hash冲突的最简单的方式。
  2. 如果Entry数组中有key(代码里是k)与set的key一样,则直接用新value覆盖掉原value
  3. 如果发现有Entry数组中无效槽k==null,则替换掉这个无效槽,并把key,value填入
  4. 若Entry数组中所有槽均有效,则在连续段末尾处放入key,value,随后检测一下这个Entry数组是否需要扩容。
  5. 至于key.threadLocalHashCode,这是一个魔数,这里不细说。数学是美妙的,一般底层程序中经常会出现类似的魔数。
  • tl.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();
    }

·  这个代码就更简单了:当前线程的Map通过这个threadLocal对象获得了与其绑定的值,这与我们第二节的理解完全一致,不过再强调一遍加深印象:一个threadLocal实例只能保证一个对象与当前线程绑定,这个线程要与多个对象绑定,最好把这些对象封装起来(比如Servlet的域对象们,SpringMVC的源码似乎就是这么做的)。
·  如果这个map还没有初始化的话,会执行:setInitialValue();,该操作新建一个表,并把此null赋值给Entry.value

  • tl.remove()
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

·  这里同样不继续看m.remove(this)的源码了,这里的remove主要就干一件事:Entry.value = null。保证了这个Value对象能被垃圾回收器及时回收。

4. 总结

·  ThreadLocal的设计十分巧妙,通过自身的对象去实现线程与数据之间的绑定,而这个绑定又以自己的弱应用作为key,这极大程度上简化了本应该很复杂的数据结构,因为这巧妙地解决了与线程绑定的对象数据类型不确定这个问题,否则就要用我上面提到的List<HashMap<T,V>>来存储。我仍不是很清楚设计师是怎么想出来这种设计模式的,也不知道这种设计理念是什么,总之路漫漫其修远兮,吾将上下而求索。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值