深入解析ThreadLocal和ThreadLocalMap

  ThreadLocal概述
  ThreadLocal是线程变量,ThreadLocal中填充的变量属于当时线程,该变量对其他线程而言是阻隔的。ThreadLocal为变量在每个线程中都创立了一个副本,那么每个线程能够拜访自己内部的副本变量。
  它具有3个特性:
  线程并发:在多线程并发场景下运用。
  传递数据:能够经过ThreadLocal在同一线程,不同组件中传递公共变量。
  线程阻隔:每个线程变量都是独立的,不会相互影响。
  在不运用ThreadLocal的情况下,变量不阻隔,得到的成果具有随机性。
  publicclassDemo{
  privateStringvariable;publicStringgetVariable(){returnvariable;
  }publicvoidsetVariable(Stringvariable){this.variable=variable;
  }publicstaticvoidmain(String[]args){
  Demodemo=newDemo();for(inti=0;i<5;i++){newThread(()->{
  demo.setVariable(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName()+""+demo.getVariable());
  }).start();
  }
  }
  }
  输出成果:
  ViewCode
  在不运用ThreadLocal的情况下,变量阻隔,每个线程有自己专属的本地变量variable,线程绑定了自己的variable,只对自己绑定的变量进行读写操作。
  publicclassDemo{
  privateThreadLocalvariable=newThreadLocal<>();publicStringgetVariable(){returnvariable.get();
  }publicvoidsetVariable(Stringvariable){this.variable.set(variable);
  }publicstaticvoidmain(String[]args){
  Demodemo=newDemo();for(inti=0;i<5;i++){newThread(()->{
  demo.setVariable(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName()+""+demo.getVariable());
  }).start();
  }
  }
  }
  输出成果:
  ViewCode
  synchronized和ThreadLocal的比较
  上述需求,经过synchronized加锁同样也能完成。可是加锁对功用和并发性有一定的影响,线程拜访变量只能排队等候顺次操作。TreadLocal不加锁,多个线程能够并发对变量进行操作。
  publicclassDemo{
  privateStringvariable;publicStringgetVariable(){returnvariable;
  }publicvoidsetVariable(Stringvariable){this.variable=variable;
  }publicstaticvoidmain(String[]args){
  Demodemo=newDemo1();for(inti=0;i<5;i++){newThread(()->{
  synchronized(Demo.class){
  demo.setVariable(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName()+""+demo.getVariable());
  }
  }).start();
  }
  }
  }
  ThreadLocal和synchronized都是用于处理多线程并发拜访资源的问题。ThreadLocal是以空间换时间的思路,每个线程都拥有一份变量的复制,然后完成变量阻隔,互相不干扰。重视的重点是线程之间数据的相互阻隔关系。synchronized是以时间换空间的思路,只供给一个变量,线程只能经过排队拜访。重视的是线程之间拜访资源的同步性。ThreadLocal能够带来更好的并发性,在多线程、高并发的环境中更为合适一些。
  ThreadLocal运用场景
  转账业务的例子
  JDBC关于业务原子性的控制能够经过setAutoCommit(false)设置为业务手动提交,成功后commit,失败后rollback。在多线程的场景下,在service层敞开业务时用的connection和在dao层拜访数据库的connection应该要坚持一致,所以并发时,线程只能阻隔操作自已的connection。
  处理计划1:service层的connection目标作为参数传递给dao层运用,业务操作放在同步代码块中。
  存在问题:传参提高了代码的耦合程度,加锁降低了程序的功用。
  处理计划2:当需求获取connection目标的时分,经过ThreadLocal目标的get办法直接获取当时线程绑定的衔接目标运用,假如衔接目标是空的,则去衔接池获取衔接,并经过ThreadLocal目标的set办法绑定到当时线程。运用完之后调用ThreadLocal目标的remove办法解绑衔接目标。
  ThreadLocal的优势:
  能够方便地传递数据:保存每个线程绑定的数据,需求的时分能够直接获取,防止了传参带来的耦合。
  能够坚持线程间阻隔:数据的阻隔在并发的情况下也能坚持一致性,防止了同步的功用损失。
  ThreadLocal的原理
  每个ThreadLocal保护一个ThreadLocalMap,Map的Key是ThreadLocal实例自身,value是要存储的值。
  每个线程内部都有一个ThreadLocalMap,Map里边寄存的是ThreadLocal目标和线程的变量副本。Thread内部的Map经过ThreadLocal目标来保护,向map获取和设置变量副本的值。不同的线程,每次获取变量值时,只能获取自己目标的副本的值。完成了线程之间的数据阻隔。
  JDK1.8的规划比较于之前的规划(经过ThreadMap保护了多个线程和线程变量的对应关系,key是Thread目标,value是线程变量)的优点在于,每个Map存储的Entry数量变少了,线程越多键值对越多。现在的键值对的数量是由ThreadLocal的数量决议的,一般情况下ThreadLocal的数量少于线程的数量,并且并不是每个线程都需求创立ThreadLocal变量。当Thread毁掉时,ThreadLocal也会随之毁掉,削减了内存的运用,之前的计划中线程毁掉后,ThreadLocalMap依然存在。
  ThreadLocal源码解析
  set办法
  首要获取线程,然后获取线程的Map。假如Map不为空则将当时ThreadLocal的引证作为key设置到Map中。假如Map为空,则创立一个Map并设置初始值。
  get办法
  首要获取当时线程,然后获取Map。假如Map不为空,则Map依据ThreadLocal的引证来获取Entry,假如Entry不为空,则获取到value值,回来。假如Map为空或者Entry为空,则初始化并获取初始值value,然后用ThreadLocal引证和value作为key和value创立一个新的Map。
  remove办法
  删除当时线程中保存的ThreadLocal对应的实体entry。
  initialValue办法
  该办法的第一次调用发作在当线程经过get办法拜访线程的ThreadLocal值时。除非线程先调用了set办法,在这种情况下,initialValue才不会被这个线程调用。每个线程最多调用顺次这个办法。
  该办法只回来一个null,假如想要线程变量有初始值需求经过子类承继ThreadLocal的办法去重写此办法,一般能够经过匿名内部类的办法完成。这个办法是protected润饰的,是为了让子类覆盖而规划的。
  ThreadLocalMap源码剖析
  ThreadLocalMap是ThreadLocal的静态内部类,没有完成Map接口,独立完成了Map的功用,内部的Entry也是独立完成的。
  与HashMap类似,初始容量默认是16,初始容量有必要是2的整数幂。经过Entry类的数据table寄存数据。size是寄存的数量,threshold是扩容阈值。
  Entry承继自WeakReference,key是弱引证,其意图是将ThreadLocal目标的生命周期和线程生命周期解绑。
  弱引证和内存走漏
  内存溢出:没有满足的内存供申请者供给
  内存走漏:程序中已动态分配的堆内存由于某种原因程序未开释或无法开释,形成体系内存的糟蹋,导致程序运转速度减慢甚至体系溃散等验证后沟。内存走漏的堆积会导致内存溢出。
  弱引证:废物收回器一旦发现了弱引证的目标,不论内存是否满足,都会收回它的内存。
  内存走漏的根源是ThreadLocalMap和Thread的生命周期是一样长的。
  假如在ThreadLocalMap的key运用强引证还是无法完全防止内存走漏,ThreadLocal运用完后,ThreadLocalReference被收回,可是Map的Entry强引证了ThreadLocal,ThreadLocal就无法被收回,由于强引证链的存在,Entry无法被收回,最后会内存走漏。
  在实际情况中,ThreadLocalMap中运用的key为ThreadLocal的弱引证,value是强引证。假如ThreadLocal没有被外部强引证的话,在废物收回的时分,key会被整理,value不会。这样ThreadLocalMap就出现了为null的Entry。假如不做任何措施,value永久不会被GC收回,就会产生内存走漏。
  ThreadLocalMap中考虑到这个情况,在set、get、remove操作后,会整理掉key为null的记录(将value也置为null)。运用完ThreadLocal后最后手动调用remove办法(删除Entry)。
  也就是说,运用完ThreadLocal后,线程依然运转,假如忘记调用remove办法,弱引证比强引证能够多一层确保,弱引证的ThreadLocal会被收回,对应的value会在下一次ThreadLocalMap调用get、set、remove办法的时分被铲除,然后防止了内存走漏。
  Hash抵触的处理
  ThreadLocalMap的构造办法
  构造函数创立一个长队为16的Entry数组,然后核算firstKey的索引,存储到table中,设置size和threshold。
  firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1)用来核算索引,nextHashCode是Atomicinteger类型的,Atomicinteger类是供给原子操作的Integer类,经过线程安全的办法来加减,适合高并发运用。
  每次在当时值上加上一个HASH_INCREMENT值,这个值和斐波拉契数列有关,主要意图是为了让哈希码能够均匀的分布在2的n次方的数组里,然后尽量的防止抵触。
  当size为2的幂次的时分,hashCode&(size-1)相当于取模运算hashCode%size,位运算比取模更高效一些。为了运用这种取模运算,一切size有必要是2的幂次。这样一来,在确保索引不越界的情况下,削减抵触的次数。
  ThreadLocalMap的set办法
  ThreadLocalMao运用了线性勘探法来处理抵触。线性勘探法勘探下一个地址,找到空的地址则刺进,若整个空间都没有空余地址,则产生溢出。例如:长度为8的数组中,当时key的hash值是6,6的方位已经被占用了,则hash值加一,寻觅7的方位,7的方位也被占用了,回到0的方位。直到能够刺进为止,能够将这个数组当作一个环形数组。