快速了解Java中ThreadLocal类
作者:mengwei 发布时间:2021-06-28 03:32:20
最近看Android FrameWork层代码,看到了ThreadLocal这个类,有点儿陌生,就翻了各种相关博客一一拜读;自己随后又研究了一遍源码,发现自己的理解较之前阅读的博文有不同之处,所以决定自己写篇文章说说自己的理解,希望可以起到以下作用:
- 可以疏通研究结果,加深自己的理解;
- 可以起到抛砖引玉的作用,帮助感兴趣的同学疏通思路;
- 分享学习经历,同大家一起交流和学习。
一、 ThreadLocal 是什么
ThreadLocal 是Java类库的基础类,在包java.lang下面;
官方的解释是这样的:
Implements a thread-local storage, that is, a variable for which each thread has its own value. All threads share the same ThreadLocal object, but each sees a different value when accessing it, and changes made by one thread do not affect the other threads. The implementation supports null values.
大致意思是:
可以实现线程的本地存储机制,ThreadLocal变量是一个不同线程可以拥有不同值的变量。所有的线程可以共享同一个ThreadLocal对象,但是不同线程访问的时候可以取得不同的值,而且任意一个线程对它的改变不会影响其他线程。类实现是支持null值的(可以在set和get方法传递和访问null值)。
概括来讲有三个特性:
- 不同线程访问时取得不同的值
- 任意线程对它的改变不影响其他线程
- 支持null
下面分别对这些特性进行实例验证,首先定义一个Test类,在此类中我们鉴证上边所提到的三个特性。类定义如下:
Test.java
public class Test{
//定义ThreadLocal
private static ThreadLocal name;
public static void main(String[] args) throws Exception{
name = new ThreadLocal();
//Define Thread A
Thread a = new Thread(){
public void run(){
System.out.println("Before invoke set,value is:"+name.get());
name.set(“Thread A”);
System.out.println("After invoke set, value is:"+name.get());
}
}
;
//Define Thread B
Thread b = new Thread(){
public void run(){
System.out.println("Before invoke set,value is :"+name.get());
name.set(“Thread B”);
System.out.println("After invoke set,value is :"+name.get());
}
}
;
// Not invoke set, print the value is null
System.out.println(name.get());
// Invoke set to fill a value
name.set(“Thread Main”);
// Start thread A
a.start();
a.join();
// Print the value after changed the value by thread A
System.out.println(name.get());
// Start thread B
b.start();
b.join();
// Print the value after changed the value by thread B
System.out.println(name.get())
}
}
代码分析:
从定义中我们可以看到只声明了一个ThreadLocal对象,其他三个线程(主线程、Thread A和Thread B)共享同一个对象;然后,在不同的线程中修改对象的值和在不同的线程中访问对象的值,并在控制台输出查看结果。
看结果:
从控制台输出结果可以看到里边有三个null的输出,这个是因为在输出前没有对对象进行赋值,验证了支持null的特点;再者,还可以发现在每个线程我都对对象的值做了修改,但是在其他线程访问对象时并不是修改后的值,而是访问线程本地的值;这样也验证了其他两个特点。
二、 ThreadLocal的作用
大家都知道它的使用场景大都是多线程编程,至于具体的作用,这个怎么说那?我觉得这个只能用一个泛的说法来定义,因为一个东西的功能属性定义了以后会限制大家的思路,就好比说菜刀是用来切菜的,好多人就不会用它切西瓜了。
这里,说下我对它的作用的认识,仅供参考,希望能有所帮助。这样来描述吧,当一个多线程的程序需要对多数线程的部分任务(就是run方法里的部分代码)进行封装时,在封装体里就可以用ThreadLocal来包装与线程相关的成员变量,从而保证线程访问的独占性,而且所有线程可以共享一个封装体对象;可以参考下Android里的Looper。不会用代码描述问题的程序员不是好程序员;
看代码:统计线程某段代码耗时的工具(为说明问题自造)
StatisticCostTime.java
// Class that statistic the cost time
public class StatisticCostTime{
// record the startTime
// private ThreadLocal startTime = new ThreadLocal();
private long startTime;
// private ThreadLocal costTime = new ThreadLocal();
private long costTime;
private StatisticCostTime(){
}
//Singleton
public static final StatisticCostTime shareInstance(){
return InstanceFactory.instance;
}
private static class InstanceFactory{
private static final StatisticCostTime instance = new StatisticCostTime();
}
// start
public void start(){
// startTime.set(System. nanoTime ());
startTime = System.nanoTime();
}
// end
public void end(){
// costTime.set(System. nanoTime () - startTime.get());
costTime = System.nanoTime() - startTime;
}
public long getStartTime(){
return startTime;
// return startTime.get();
}
public long getCostTime(){
// return costTime.get();
return costTime;
}
好了,工具设计完工了,现在我们用它来统计一下线程耗时试试呗:
Main.java
public class Main{
public static void main(String[] args) throws Exception{
// Define the thread a
Thread a = new Thread(){
public void run(){
try{
// start record time
StatisticCostTime.shareInstance().start();
sleep(200);
// print the start time of A
System.out.println("A-startTime:"+StatisticCostTime.shareInstance().getStartTime());
// end the record
StatisticCostTime.shareInstance().end();
// print the costTime of A
System.out.println("A:"+StatisticCostTime.shareInstance().getCostTime());
}
catch(Exception e){
}
}
}
;
// start a
a.start();
// Define thread b
Thread b = new Thread(){
public void run(){
try{
// record the start time of B1
StatisticCostTime.shareInstance().start();
sleep(100);
// print the start time to console
System.out.println("B1-startTime:"+StatisticCostTime.shareInstance().getStartTime());
// end record start time of B1
StatisticCostTime.shareInstance().end();
// print the cost time of B1
System.out.println("B1:"+StatisticCostTime.shareInstance().getCostTime());
// start record time of B2
StatisticCostTime.shareInstance().start();
sleep(100);
// print start time of B2
System.out.println("B2-startTime:"+StatisticCostTime.shareInstance().getStartTime());
// end record time of B2
StatisticCostTime.shareInstance().end();
// print cost time of B2
System.out.println("B2:"+StatisticCostTime.shareInstance().getCostTime());
}
catch(Exception e){
}
}
}
;
b.start();
}
}
运行代码后输出结果是这样的
注意:输出结果精确度为纳秒级
看结果是不是和我们预想的不一样,发现A的结果应该约等于B1+B2才对呀,怎么变成和B2一样了那?答案就是我们在定义startTime和costTime变量时,本意是不应共享的,应是线程独占的才对。而这里变量随单例共享了,所以当计算A的值时,其实startTime已经被B2修改了,所以就输出了和B2一样的结果。
现在我们把StatisticCostTime中注释掉的部分打开,换成ThreadLocal的声明方式试下。
看结果:
呀!这下达到预期效果了,这时候有同学会说这不是可以线程并发访问了吗,是不是只要我用了ThreadLocal就可以保证线程安全了?答案是no!首先先弄明白为什么会有线程安全问题,无非两种情况:
1、不该共享的资源,你在线程间共享了;
2、线程间共享的资源,你没有保证有序访问;
前者可以用“空间换时间”的方式解决,用ThreadLocal(也可以直接声明线程局部变量),后者用“时间换空间”的方式解决,显然这个就不是ThreadLocal力所能及的了。
三、 ThreadLocal 原理
实现原理其实很简单,每次对ThreadLocal 对象的读写操作其实是对线程的Values对象的读写操作;这里澄清一下,没有什么变量副本的创建,因为就没有用变量分配的内存空间来存T对象的,而是用它所在线程的Values来存T对象的;我们在线程中每次调用ThreadLocal的set方法时,实际上是将object写入线程对应Values对象的过程;调用ThreadLocal的get方法时,实际上是从线程对应Values对象取object的过程。
看源码:
ThreadLocal 的成员变量set
/**
* Sets the value of this variable for the current thread. If set to
* {@code null}, the value will be set to null and the underlying entry will
* still be present.
*
* @param value the new value of the variable for the caller thread.
*/
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
TreadLocal 的成员方法get
/**
* Returns the value of this variable for the current thread. If an entry
* doesn't yet exist for this variable on this thread, this method will
* create an entry, populating the value with the result of
* {@link #initialValue()}.
*
* @return the current value of the variable for the calling thread.
*/
@SuppressWarnings("unchecked")
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this);
}
ThreadLocal的成员方法initializeValues
/**
* Creates Values instance for this thread and variable type.
*/
Values initializeValues(Thread current) {
return current.localValues = new Values();
}
ThreadLocal 的成员方法values
/**
* Gets Values instance for this thread and variable type.
*/
Values values(Thread current) {
return current.localValues;
}
那这个Values又是怎样读写Object那?
Values是作为ThreadLocal的内部类存在的;这个Values里包括了一个重要数组Object[],这个数据就是解答问题的关键部分,它是用来存储线程本地各种类型TreadLocal变量用的;那么问题来了,具体取某个类型的变量时是怎么保证不取到其他类型的值那?按一般的做法会用一个Map根据key-value映射一下的;对的,思路就是这个思路,但是这里并没有用Map来实现,是用一个Object[]实现的Map机制;但是,若要用Map理解的话,也是不可以的,因为机制是相同的;key其实上对应ThreadLocal的弱引用,value就对应我们传进去的Object。
解释下是怎么用Object[]实现Map机制的(参考图1);它是用数组下标的奇偶来区分key和value的,就是下表是偶数的位置存储key,奇数存储value,就是这样搞得;感兴趣的同学如果想知道算法实现的话,可以深入研究一下,这里我不在详述了。
结合前面第一个实例分析下存储情况:
当程序执行时存在A,B和main三个线程,分别在线程中调用name.set()时同时针对三个线程实例在堆区分配了三块相同的内存空间来存储Values对象,以name引用作为key,具体的object作为值存进三个不同的Object[](参看下图):
四、 总结
ThreadLocal 不能完全解决多线程编程时的并发问题,这种问题还要根据不同的情况选择不同的解决方案,“空间换时间”还是“时间换空间”。
ThreadLocal最大的作用就是把线程共享变量转换成线程本地变量,实现线程之间的隔离。
来源:https://www.2cto.com/kf/201608/541771.html


猜你喜欢
- 本文实例讲述了C#命令模式。分享给大家供大家参考。具体实现方法如下:using System;using System.Collection
- 问题现象前段时间升级 Android Studio 3.1.3+ 版本后,决定尝试使用 Kotlin 做 APP 开发看看。结果却发现,修改
- HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决
- import java.util.LinkedList;public class OJ { public OJ() {
- 大家好,我是梦辛工作室的灵,最近在帮客户修改安卓程序时,有要求到一个按钮要浮动在键盘的上方,下面大概讲一下实现方法:其实很简单,分三步走第一
- 这篇文章主要介绍了Spring Bean初始化及销毁多种实现方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值
- 字符串的操作是C#程序设计中十分重要的一个组成部分,本文就以实例形式展现了C#实现移除字符串末尾指定字符的方法。相信对大家学习C#程序设计有
- sms4j 2.0 全新来袭即sms-aggregation成功加入dromara之后,很多人向我们反应了项目名称太长不好记,也太绕口, 在
- 本文实例讲述了Java上传文件进度条的实现方法。分享给大家供大家参考,具体如下:东西很简单,主要用到commons-fileupload,其
- 目录第一种第二种第三种随机数的产生在一些代码中很常用,也是我们必须要掌握的。而java中产生随机数的方法主要有三种:第一种:new Rand
- 0.背景简介微软在 .NET 框架中提供了多种实用的线程同步手段,其中包括 monitor 类及 reader-writer锁。
- SharedPreferences是Android中最容易理解的数据存储技术,实际上SharedPreferences处理的就是一个key-
- HashTable和HashMap区别第一,继承的父类不同。Hashtable继承自Dictionary类,而HashMap继承自Abstr
- 前言最近项目中需要用到字符串加解密,遂研究了一波,发现密码学真的是博大精深,好多算法的设计都相当巧妙,学到了不少东西,在这里做个小小的总结,
- mybatis-plus的代码生成器会在实体类中生成数据库所有字段,我们去用mapper接口查询时,会返回数据库所有的字段。但有些字段不是我
- 前 言🍉 作者简介:半旧518,长跑型选手,立志坚持写10年博客,专注于java后端☕专栏简介:深入、全面、系统的介绍消息中间件🌰 文章简介
- 当我保持对连续将对象拖有时在移动后 5 6 拖/滴,看到有时不获取对象还原不回来,我不能用于以后。基本上我有对两个对象组的 canvas 在
- ServletWebServerApplicationContext实现了父类AbstractApplicationContext的onRe
- 前言随着敏捷开发的流行,编写单元测试已经成为业界共识。但如何来衡量单元测试的质量呢?有些管理者片面追求单元测试的数量,导致底下的开发人员投机
- 1.非静态成员变量当成员变量为非静态成员变量且对当前类进行实例化时,将会产生死循环例子:public class ConstructorCl