zoukankan      html  css  js  c++  java
  • 九浅一深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 : null
    child 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是独立的,只是在创建线程的时候浅复制了一份而已。

  • 相关阅读:
    【矩阵乘】【DP】【codevs 1305】Freda的道路
    Giraph源代码分析(九)—— Aggregators 原理解析
    Dubbo框架应用之(三)--Zookeeper注冊中心、管理控制台的安装及解说
    Oracle中对数字加汉字的排序(完好)
    PKU-2104-K-th Number
    IE下推断IE版本号的语句
    自己定义View时,用到Paint Canvas的一些温故,简单的帧动画(动画一 ,&quot;掏粪男孩Gif&quot;顺便再提提onWindowFocusChanged)
    <html>
    测试一个网段主机在线情况,禁ping的情况除外
    glance image-create --name "wj_js_company_img" --file a0e1c7fa-d6d3-410f-9bb5-e699e342db91 --disk-format qcow2 --container-format bare --progress --visibility public
  • 原文地址:https://www.cnblogs.com/FuzhePan/p/9377395.html
Copyright © 2011-2022 走看看