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

简介:

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

ThreadLocal概述
ThreadLocal是线程变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

它具有3个特性:

线程并发:在多线程并发场景下使用。
传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。
线程隔离:每个线程变量都是独立的,不会相互影响。
在不使用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();
    }
}

}

输出结果:

 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();
    }
}

}

输出结果:

 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的优势:

可以方便地传递数据:保存每个线程绑定的数据,需要的时候可以直接获取,避免了传参带来的耦合。
可以保持线程间隔离:数据的隔离在并发的情况下也能保持一致性,避免了同步的性能损失。
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的位置。直到可以插入为止,可以将这个数组看成一个环形数组。

原文地址https://www.cnblogs.com/xdcat/p/13051561.html

相关文章
|
1天前
|
消息中间件 缓存 NoSQL
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
|
1天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
1天前
|
存储 安全 Java
Java并发编程中的高效数据结构:ConcurrentHashMap解析
【4月更文挑战第25天】在多线程环境下,高效的数据访问和管理是至关重要的。Java提供了多种并发集合来处理这种情境,其中ConcurrentHashMap是最广泛使用的一个。本文将深入分析ConcurrentHashMap的内部工作原理、性能特点以及它如何在保证线程安全的同时提供高并发性,最后将展示其在实际开发中的应用示例。
|
2天前
|
缓存 Java
【Java基础】简说多线程(上)
【Java基础】简说多线程(上)
6 0
|
2天前
|
Java
Java输入输出流详细解析
Java输入输出流详细解析
Java输入输出流详细解析
|
3天前
|
并行计算 算法 安全
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
|
3天前
|
安全 Java 编译器
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。
16 0
|
3天前
|
监控 安全 Java
在Java中如何优雅的停止一个线程?可别再用Thread.stop()了!
在Java中如何优雅的停止一个线程?可别再用Thread.stop()了!
11 2
|
3天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
29 1
|
3天前
|
安全 Java
Java基础教程(15)-多线程基础
【4月更文挑战第15天】Java内置多线程支持,通过Thread类或Runnable接口实现。线程状态包括New、Runnable、Blocked、Waiting、Timed Waiting和Terminated。启动线程调用start(),中断线程用interrupt(),同步用synchronized关键字。线程安全包如java.util.concurrent提供并发集合和原子操作。线程池如ExecutorService简化任务管理,Callable接口允许返回值,Future配合获取异步结果。Java 8引入CompletableFuture支持回调。

推荐镜像

更多