zoukankan      html  css  js  c++  java
  • 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();
            }
        }
    }

    输出结果:

    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的位置。直到可以插入为止,可以将这个数组看成一个环形数组

  • 相关阅读:
    转自 陈皓 博客 《提高效率》
    codevs 1098 均分纸牌 2002年NOIP全国联赛提高组 x
    codevs 1160 蛇形矩阵x
    【説明する】进制转换
    欧几里得?x
    codevs 1020 孪生蜘蛛 x
    [HDOJ5883]The Best Path(欧拉回路,异或)
    [HDOJ5889]Barricade(spfa,最大流)
    [PAT L2-001] 紧急救援(spfa,最短路计数, dp)
    [CF717E]Paint it really, really dark gray(dfs,构造)
  • 原文地址:https://www.cnblogs.com/xdcat/p/13051561.html
Copyright © 2011-2022 走看看