Java多线程之深刻解析ThreadLocal和ThreadLocalMap

ThreadLocal概述

ThreadLocal是线程变量,ThreadLocal中填充的变量属于当前线程,该变量对其余线程而言是隔离的。ThreadLocal为变量在每一个线程中都建立了一个副本,那么每一个线程能够访问本身内部的副本变量。数据库

它具备3个特性:数组

  1. 线程并发:在多线程并发场景下使用。
  2. 传递数据:能够经过ThreadLocal在同一线程,不一样组件中传递公共变量。
  3. 线程隔离:每一个线程变量都是独立的,不会相互影响。

在不使用ThreadLocal的状况下,变量不隔离,获得的结果具备随机性。安全

public class Demo {
    private String variable;

    public String getVariable() {
        return variable;
    }

    public void setVariable(String variable) {
        this.variable = variable;
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                demo.setVariable(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
            }).start();
        }
    }
}

输出结果:多线程

Thread-2 Thread-2
Thread-4 Thread-4
Thread-1 Thread-2
Thread-0 Thread-2
Thread-3 Thread-3
View Code

在不使用ThreadLocal的状况下,变量隔离,每一个线程有本身专属的本地变量variable,线程绑定了本身的variable,只对本身绑定的变量进行读写操做。并发

public class Demo {
    private ThreadLocal<String> variable = new ThreadLocal<>();

    public String getVariable() {
        return variable.get();
    }

    public void setVariable(String variable) {
        this.variable.set(variable);
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                demo.setVariable(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
            }).start();
        }
    }
}

输出结果:ide

Thread-0 Thread-0
Thread-1 Thread-1
Thread-2 Thread-2
Thread-3 Thread-3
Thread-4 Thread-4
View Code

synchronized和ThreadLocal的比较

上述需求,经过synchronized加锁一样也能实现。可是加锁对性能和并发性有必定的影响,线程访问变量只能排队等候依次操做。TreadLocal不加锁,多个线程能够并发对变量进行操做。函数

public class Demo {
    private String variable;
    public String getVariable() {
        return variable;
    }

    public void setVariable(String variable) {
        this.variable = variable;
    }

    public static void main(String[] args) {
        Demo demo = new Demo1();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                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的优点:

  1. 能够方便地传递数据:保存每一个线程绑定的数据,须要的时候能够直接获取,避免了传参带来的耦合。
  2. 能够保持线程间隔离:数据的隔离在并发的状况下也能保持一致性,避免了同步的性能损失。

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使用完后,ThreadLocal Reference被回收,可是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的位置。直到能够插入为止,能够将这个数组当作一个环形数组

相关文章
相关标签/搜索