手记

九浅一深ThreadLocal

ThreadLocal的作用、使用示例

ThreadLocal是线程的本地存储,存储在其内的值只能被当前线程访问到,其他线程获取不到,可以存储任意对象。
经常用来存储当前线程的一些上下文信息,这样不用通过参数一层层的向下传递。比如在计算分库分表时,上层根据业务规则计算出这次要操作的数据库和表编号,存储到ThreadLocal中,下层可以通过ThreadLocal直接获取到。
如果当前线程创建了子线程,某些数据想让子线程也获取到,可以使用InheritableThreadLocal,用法和ThreadLocal类似。
具体使用代码如下:

public class Demo {    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();    private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();    public static void main(String[] args) {
        threadLocal.set("threadlocal value");
        System.out.println(Thread.currentThread().getName() + " thread get threadlocal value : " + threadLocal.get());        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " thread get threadlocal value : " + threadLocal.get());
        },"child").start();

        inheritableThreadLocal.set("inheritable threadlocal value");
        System.out.println(Thread.currentThread().getName() + " thread get inheritable threadlocal value:" + inheritableThreadLocal.get());        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " thread get inheritable threadlocal value : " + inheritableThreadLocal.get());
        },"child").start();
    }
}

执行结果:

main thread get threadlocal value : threadlocal value
main thread get inheritable threadlocal value:inheritable threadlocal value
child thread get threadlocal value : nullchild thread get inheritable threadlocal value : inheritable threadlocal value

可以看到存储到ThreadLocal中的对象只能被当前线程访问到,存储到InheritableThreadLocal中的对象可以被当前线程以及子线程访问到。

实现原理及源码分析

先不看TheadLocal的源码,思考下如果让我们实现类似功能该怎么做。
首先,数据只能被当前线程访问到,最合适存储的地方肯定是Thread的一个字段,每个线程都是Thread的一个实例,都可以通过Thread.currentThread()获取到,进而可以获取到当前线程的本地存储。其他线程通过Thread.currentThread()获取到的是各自的Thread实例,所以不同线程存储的数据做到了相互隔离。
然后看下应该用什么样的数据结构。要存储数据有可能不只一条,而且有存有取是一个map结构,于是有了一个初步实现,伪代码如下:

Thread{
    Map threadLocalMap = nulll;
}
ThreadLocal{    public set(val){        if(Thread.currentThread().threadLocalMap == null){
            Thread.currentThread().threadLocalMap = new HashMap();
        }
        Thread.currentThread().threadLocalMap.put(this,val);
    }    public Object get(){        return Thread.currentThread().threadLocalMap.get(this);
    }
}

使用this中作为key,这样一个threadLocal代表一条数据,多个线程可能持有同一个this,但通过this拿到的值是存储在各种threadLocalMap中的值。
下面看下ThreadLocal的源码,发现思路类似。Thread中有一个map结构的ThreadLocalMap,调用ThreadLocal.set(val)方法时,实际是以this为key,val为value存储到该map中。
具体实现上有很多不同之处,Thread.threadLocalMap访问权限是当前包,也就是对于使用方只能通过ThreadLocal暴露的方法进行操作,可以防止其他线程拿到Thread对象后对threadLcoalMap访问,引起安全性问题。
然后注意到java没有使用HashMap而是自己实现了一个ThreadLocalMap,这是出于什么原因呢?
根据ThreadLocalMap的代码,它依然是一个Hash表,但是在解决hash冲突时,不同于HashMap采用的链地址法,而是采用线行探查法。两种解决Hash冲突方法的示意图如下:

    
                     链地址法                                                                                        线行探查法
链地址法:计算key的hash值并取模,存储到对应的坑位,如果发现该坑位已经被占用,则创建一个链表节点,挂在该坑位的最后,查找时,根据key的hash值计算找到对应坑位,然后遍历坑位下的链表,知道找到key相同的节点。
线行探查法:计算完key的哈希值并取模后,如果发现Hash表(数组)中对应的坑位已经存在了数据,则找该坑位的下一个坑,直到找到没有数据的坑位,进行存储。
除这两种,还有再哈希法、公共溢出区法等,在此不做详细论述。
链表法适用于经常进行插入和删除的情况,插入和删除时只需要把对应坑位的链表节点插入和删除即可,不会影响到其他坑位的数据。缺点是容易浪费空间,如果hash表有0~9十个链表,我们有十条数据但都hash冲突比较多,有可能只有第一个链表有十个数据,其他链表都是空的。而且,散列比较均匀,每个链表有一个数据,除了存储该数据,还需要存储头结点
线行探查法的有点是没有空间的浪费,只需要存储数据。缺点是不适合经常删除,如果删除一个结点,该结点之后的数据都需要重新计算要入到哪个坑位。或者可以假删除,等有下个元素的时候再进行替换。
ThreadLocal使用场景是不会频繁插入,基本不会有删除操作,所以更适合线行探查法。
使用线行探查法,要尽量保证hash值取模后能尽量均匀的三列在每个坑位上,提高插入和查找的效率。
ThreadLocalMap在计算hash值的时候采用了0x61c88647,可以保证散列的非常均匀,基本没有hash冲突。计算this,即ThreadLocal对象的hash值时,返回的是0x61c88647或0x61c88647的倍数。0x61c88647是根据斐波那契散列法得到的一个值,对应着32位有符号整数中的2654435769,用它对2的幂进行取模可以保证散列的非常均匀。
源码中ThreadLocalMap的每个Entry都是弱引用,这是为什么呢?
这个也是重新实现map,而没有使用HashMap的原因之一。ThreadLocalMap是存储在Thread对象里的,线程不结束,这个map就会永远存在不会被GC,如果一个线程之前存储了很多数据到ThrealLocal中,现在已经不用了,但没有删除,如果强引用,这些数据就不会被GC。设置成弱引用,当ThreadLocal对象已经不被引用时,不会因为被ThreadLocalMap而引用,导致无法回收内存。

可被子线程继承的本地存储 InheritableThreadLocal 实现原理解析

InheritableThreadLocal使用模板方法模式,继承自ThreadLocal。
和ThreadLocal相比,InheritableThreadLocal只有一点差别:数据存在在Thread对象的inheritableThreadLocal字段,而不是ThreadLocal字段。所以只覆盖了ThreadLcoal的getMap方法和createMap方法,分别用于获取map和给Thread.inheritableThreadLocal赋值。
在创建一个线程的时候,在线程的init方法中可以看到如下代码:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)            this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

inheritThreadLocals是init方法的参数,默认为true。只要父线程的inheritableThreadLocals字段不为空,在创建子线程的时候就会把这些数据复制给子线程。
ThreadLocal.createInheritedMap方法是一个工厂方法,这个方法用于创建一个map对象,并把参数传递进来的map复制到新map中,需要注意的是这个复制是浅复制,map中存的是原对象的引用。示例代码如下

public class Demo {    private static InheritableThreadLocal<User> inheritableThreadLocal = new InheritableThreadLocal<>();    public static void main(String[] args) {        /**
         * 更新对象属性示例         */
        User user = new User();
        user.name = "lanlingwang";
        inheritableThreadLocal.set(user);
        Thread thread = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name);
        },"child1");

        inheritableThreadLocal.get().name = "ake";

        thread.start();
        System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name);        /**
         * 更新对象示例         */
        Thread thread2 = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name);
        },"child2");

        User user2 = new User();
        user2.name = "nakelulu";
        inheritableThreadLocal.set(user2);

        thread2.start();
        System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name);
    }
}class User{    public String name ;
}

如果ThreadLocal中存储了一个User对象,其中一个线程拿到这个对象后更改了name字段,这个更改对其他线程是可见的。如果某个线程重新往ThreadLocal中set了一个新对象,只是set到当前线程而已,不会修改其他线程的数据,因为各个线程的ThreadLocalMap是独立的,只是在创建线程的时候浅复制了一份而已。

作者:FuzhePan

出处:http://www.cnblogs.com/FuzhePan/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明。


0人推荐
随时随地看视频
慕课网APP