如果对ThreadLocal有一定了解,可直接看
### 那些年我们仍未知道答案的问题
我的博文笔记较为连贯,故看完以后建议参考
liangzzz 的 JAVA并发-自问自答学ThreadLocal 进行重点背诵【参考博文】
①Jdk 1.8 官方解释
该类提供线程局部变量。
这些变量与它们的普通局部变量的不同之处在于,访问一个线程局部变量的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。
ThreadLocal实例(线程局部变量)通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户ID或事务ID)。
②大佬的解释
ThreadLocal 适用于如下两种场景
1.每个线程需要有自己单独的实例
2.实例要在线程类的多个方法中共享,但不希望被多线程共享
我觉得2准确的来说应该是:线程类实例的成员变量,要在线程类的多个方法中共享
③自己的理解
ThreadLocal变量是与线程类变量做对比的存在。
线程类的静态变量及实例变量,线程类实例化作为Thread变量的构造参数,创建多个子线程,而这多个子线程访问的是同一个线程类实例对象,也就是同一个线程类实例对象的静态/实例变量(可同时访问或并发访问),都会被多线程共同作用影响(静态、实例)变量值。【也就是所谓非线程安全】
但是对于线程类的ThreadLocal变量(无论静态实例),多线程访问的是自己的TLM对象,TLM对象中会复制一份线程类实例的(静态、实例)变量对象,也就是一个独立于线程类实例的变量副本,即便改变也只是改变当前子线程下的变量副本,而不会影响线程类实例的变量对象,也就不会影响其他子线程获取线程类实例的变量对象
【也就是所谓只有线程类实例的ThreadLocal变量与run方法内部的局部变量是线程安全的,不会被其他线程访问及改变】
一句话——ThreadLocal是对线程类变量的包装,实质是向每个子线程分发不回收的变量副本。(但这个分发行为是在子线程中TL变量主动调用的,如果不调用就不会分发,换句话说,线程类实例只是携带变量,并不主动分发。线程类实例:你(子线程)上来,自己动?
)
注:最后提一点,关于ThreadLocal的内存泄露问题,好像是由ThreadLocal内部的存储类Entry对象在作为key的reeference指向null后,仍有Value的Entry对象并没有被GC回收导致
1.一个线程类实例的静态变量及实例普通变量被多个子线程访问
//两子线程并发执行
Thread[子线程1,5,main]访问statictest1变量100 : 99 || 16.50.57
Thread[子线程1,5,main]访问 test2变量200 : 199 || 16.50.57
Thread[子线程2,5,main]访问statictest1变量100 : 98 || 16.51.02
Thread[子线程2,5,main]访问 test2变量200 : 198 || 16.51.02
//两子线程并行执行
Thread[子线程1,5,main]访问statictest1变量100 : 98 || 16.57.54
Thread[子线程1,5,main]访问 test2变量200 : 198 || 16.57.54
Thread[子线程2,5,main]访问statictest1变量100 : 98 || 16.57.54
Thread[子线程2,5,main]访问 test2变量200 : 198 || 16.57.54
2.一个线程类实例的静态/实例ThreadLocal变量被多个子线程访问
//两子线程并发执行
Thread[线程1,5,main]访问staticTL1变量10 : 9 || 17.14.58
Thread[线程1,5,main]访问 TL2变量20 : 19 || 17.14.58
Thread[线程2,5,main]访问staticTL1变量10 : 9 || 17.15.03
Thread[线程2,5,main]访问 TL2变量20 : 19 || 17.15.03
//两子线程并行执行
Thread[线程2,5,main]访问staticTL1变量10 : 9 || 16.57.54
Thread[线程1,5,main]访问staticTL1变量10 : 9 || 16.57.54
Thread[线程2,5,main]访问 TL2变量20 : 19 || 16.57.54
Thread[线程1,5,main]访问 TL2变量20 : 19 || 16.57.54
最后,提一下ThreadLocal与Synchronized,这俩根本不是一个层次的概念。无从比较
Http服务中的已登录用户的请求携带Session登录信息
在http容器中会保存已登录用户的登录信息。
因为http容器通常是一个线程负责执行一个请求的业务逻辑,一个用户多个请求会生成多个线程,希望为用户的每个线程/请求进行同一标识也就是每个线程分别存储这个用户的Session对象,需要注意的是,这个session并不是从始至终在所有线程的内容都是一样的,根据各个请求的处理,各个线程的Session会遇到不同的修改。
ThreadLocal可以实现当前子线程的数据库操作都是使用同一个Connection。
通过子线程实例的ThreadLocalMap成员变量 存储 线程类ThreadLocal成员变量携带的Connection对象,保证每个子线程实例都存储了一个Connection(Value)对象在TLM中。
每次事务操作都通过TLM存储的Connection对象进行,这样就不会每回获取新的连接了。
对上面展示的 线程类静态变量及实例变量 与 线程类的ThreadLocal变量 进行测试分析
下面会对ThreadLocal简称为TL
,如果是实例化的(特指)则简称为tl+n (tl1,tl2...)
线程类MyRunnable1简称为MR1
,线程类MR1的实例化对象简称为run+n(run1,run2)
通过run
对象实例化子线程对象简称为t+n(t1,t2)
TL静态内部类ThreadLocalMap简称为TLM
,实例化后被称为Entry[]
/Entry[]数组
TL.TLM的静态内部类Entry无简称,Entry实例化后称呼为Entry对象
//psvm类:ThreadLocalTest
public class ThreadLocalTest {
//psvm主函数
public static void main(String[] args) throws InterruptedException {
System.out.println("start1");
//线程类MyRunnable1 实例化runnable【这个变量包含测试4种变量】
//四种变量,(普通的)静态/实例变量,(特殊的)实例TL及静态TL变量
MyRunnable1 runnable = new MyRunnable1();
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
//开启则是模拟并发情况,注释则是并行【因为我的CPU是4核心4线程】
Thread.sleep(5000);
thread2.start();
}
//线程类模板MyRunnable1
public static class MyRunnable1 implements Runnable {
//额外:ThreadLocal的Lambda构造方式,详情查看TL的public withInitial()函数
//private ThreadLocal balance = ThreadLocal.withInitial(() -> 1000);
//问题a:静态ThreadLocal,会对其他实例线程产生影响?【static对TL的影响】
//问题b:创建并赋值tl变量的方式共有几种?
//第一种:创建静态tl1对象。使用内部类形式复写protected权限的initialValue函数
private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>() {
//初始化 静态 tl1 变量 value
@Override
protected Integer initialValue() {
//这个随机变量值,可用于测试,不同线程可获取到不同初始值后续也会做这个实验
//【证明官方解释:每个线程都是获取的独立初始化的TL变量副本】
//new Random().nextInt(10)
return 10;
}
};
//第二种:创建静态变量 test1
private static int test1 = 100;
//同问题a:实例TL变量不对其他实例线程产生影响?【static与实例TL的区别】
//第三种:实例tl2变量对象
private ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 20;
}
};
//问题c:多线程访问实例变量test2与静态变量test1的区别?
//第四种:实例变量test2
private int test2 = 200;
//【证明大佬解释:TL实例在线程类的多个方法中共享,但不希望被多线程共享(普通实例)】
//线程类方法们,分别对特殊线程类属性(静/实TL属性)及普通线程类属性(静/实属性)
//进行减法运算
public void staticTL1Change() {
MyRunnable1.threadLocal1.set(threadLocal1.get() - 1);
}
public void TL2Change() {
threadLocal2.set(threadLocal2.get() - 1);
}
public void staticTest1Change() {
MyRunnable1.test1 -= 1;
}
public void test2Change() {
test2 -= 1;
}
//复写线程类的执行体
@Override
public void run() {
//这个地方很奇怪,不论我停多少ms,两线程都是同时执行
//只有在主线程sleep才可以分时,有大佬懂的教教我
/*System.out.println();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
//执行线程类方法(TL实例在线程类的多(2)个方法中共享,但不在多线程间共享)
staticTL1Change();
TL2Change();
//(普通属性在线程类的多(2)个方法中共享,且在多个线程间共享)
staticTest1Change();
test2Change();
//下方进行了小改动,为了更直观的观察 static 与 非static 的TL变量区别
System.out.println(Thread.currentThread() + "访问staticTL1变量10 : " + threadLocal1.get() + " || " + threadLocal1.hashCode() +" || " +
new SimpleDateFormat("HH.mm.ss").format(new Date()));
System.out.println(Thread.currentThread() + "访问TL2变量20 : " + threadLocal2.get() + " || " + threadLocal2.hashCode() +" || " +
new SimpleDateFormat("HH.mm.ss").format(new Date()));
System.out.println(Thread.currentThread() + "访问statictest1变量100 : " + MyRunnable1.test1 + " || " +
new SimpleDateFormat("HH.mm.ss").format(new Date()));
System.out.println(Thread.currentThread() + "访问test2变量200 : " + test2 + " || " +
new SimpleDateFormat("HH.mm.ss").format(new Date()));
}
}
}
//当然,以下显示是我人工整理了以下,上面的sout肯定执行不出来下面的效果。
//很明显线程类MR1的 staticTL 1 在main父线程的子线程1,2中是同一个对象,线程间不共享
Thread[线程1,5,main]访问staticTL1变量10 : 9 || 316372558 || 19.46.12
Thread[线程2,5,main]访问staticTL1变量10 : 9 || 316372558 || 19.46.17
//很明显线程类MR1的 TL 2 在main父线程的子线程1,2中也是同一个对象,线程间不共享
Thread[线程1,5,main]访问 TL2变量20 : 19 || 306311720 || 19.46.12
Thread[线程2,5,main]访问 TL2变量20 : 19 || 306311720 || 19.46.17
//很明显,statictest1变量 在...子线程1,2也是同一个对象,线程间共享
Thread[线程1,5,main]访问statictest1变量100 : 99 || 19.46.12
Thread[线程2,5,main]访问statictest1变量100 : 98 || 19.46.17
//很明显,test1变量 在...子线程1,2也是同一个对象,线程间共享
Thread[线程1,5,main]访问 test2变量200 : 199 || 19.46.12
Thread[线程2,5,main]访问 test2变量200 : 198 || 19.46.17
Q c:多线程访问实例变量test2与静态成员变量test1的区别?
A c:
基础概念(基础清楚的可略过)
static静态成员变量,也称为类变量,属于类对象所有,位于方法区。
被static修饰的成员变量和成员方法独立于该类的任何对象。只要MR1类被加载,Java虚拟机就能根据MR1类名在运行时,去方法区内找到静态成员变量test1。
因此,静态变量test1可以在MR1的任何对象创建之前访问,无需引用任何对象。也就是我上面直接调用的MyRunnable1.test1。
静态成员变量test1不依赖MR1类的实例化,但被MR1类的所有实例共享。
static静态成员属性与单例run对象的多(子)线程
结论:静态成员变量test1为通过run对象生成的所有子线程共享,共享一份内存,一旦test1值被修改,则其他线程均对修改可见,故线程非安全。
实例成员属性与单例run对象的多(子)线程
单例run对象实例化多个子线程访问run单例对象的一个成员变量时 每个子线程都会得到一个该实例变量的副本 在子线程自己的栈中保存计算,以提高速度。
结论:实例变量是实例对象私有的,系统只存在一个实例对象情况下,在多线程访问下,实例变量值改变后,则其它子线程均可见,故线程非安全;
最终结论
单例run对象构建的多个子线程实例,访问run对象的实例变量/静态变量的结果都是差不多的,都是线程非安全,只是具体操作不一样。静态变量是各个子线程都去堆中修改,实例变量是各个子线程先拿一份副本过来进行修改,然后再覆盖回到run对象内。
关于单例run对象实例化多个子线程的问题:
当一个子线程修改了自己栈内run对象的实例/静态变量副本的值 ,还没有立即将同步到主存中,其他子线程再来获取主存中的该run对象的实例变量时就会得到过期数据。也就联系到了我们的Synchronized关键字,由锁机制完成控制,但相较于TL来说,确实麻烦了很多。
可能上面的文字太多了,理解起来有点麻烦,那么就先回答一个叫简单的问题b
Q b:创建并赋值tl变量的方式共有几种?
A b:一共三种,分别是:
1.ThreadLocal的Lambda构造并赋值方式:withInitial 参考zebe
2.ThreadLocal的内部类复写保护访问函数initialValue()构造并赋值方式
3.ThreadLocal构造未赋值,调用tln.set()函数的方式
以下给出实例
public static class MyRunnable1 implements Runnable {
//方法1 lambda
private ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 1000);
//方法2 内部类复写函数
private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
//new Random().nextInt(10)
return 10;
}
};
//方法3 任意方法内部调用TL.set()【当然如果你没调用过这个方法,就取不到TL的值】
public MyRunnable1() {
threadLocal1.set(12);
threadLocal2.set(11);
}
Q a:静态成员变量TL与实例成员变量TL的区别
A a:
从测试程序psvm类的运行结果来看,是没有任何区别的,不论是静态还是实例成员TL对象在不同线程中都是相同的。
单例run对象构建的多个子线程访问的都是同一个静态TL/实例TL对象。
那么二者的区别是什么呢?
当然是多例run对象构建的子线程访问的时候会有区别。
多例run对象构建的各自子线程访问的是同一个静态TL,但不是同一个实例TL对象。
至于证据,凡事都讲究证据,那么在我肝了2天的情况下。我来摆出源码讲解,因为我的Java基础有些欠缺,所以也请各位大佬温柔斧正(轻点,疼♂)
【干掉TL,就赶紧滚回去把static补了,太难受了,被static敲了很多思路上的闷棍】
Version:JDK 1.8
虚拟机的体系结构:堆,方法区,本地方法栈,CPU寄存器。
①堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的类的信息。(存储class目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身,包含Class对象(静态对象)
②栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中。
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈又分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
③方法区:
1.方法区跟堆一样,被所有的线程共享。方法区包含所有的class模板,在整个程序中永远唯一的元素。
②我的图解有点复杂,需要与下面的文字解析一起使用。
简叙
创造这个图的时候,没有将类模板与类实例区分开来,而是直接混合在一起,所以看起来会有些混乱,请不要多怪罪。
一共分为2个部分
第一部分:是左边的MR1线程类。
MR1线程类是声明TL变量的类,以MR1线程类实例run的TL变量所携带的Value对象作为分发子线程Value副本的母体。Thread类构造子线程(以run实例作为构造参数)实例时,每个子线程都会有一个threadLocals属性(TLM),tls(TLM)是定义在Thread类的包私有属性。且子线程初始化后它的threadLocals对象指向null。
等待Thread.run()中MR1实例的TL变量通过get()/set()方法进行分发TL携带的Value副本。
public class Thread implements Runnable{
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
第二部分:是右边的ThreadLocal对象。
被定义在MR1类中,初始化为MR1静态/实例变量,通常是静态,因为实例不影响使用结果
TL类内部包含静态类TLM,以及TLM内部包含静态类Entry。
TL类的Field只有一个重要属性——int类型的threadLocalHashCode变量,它用来存储TL变量携带的Value对象的唯一标识哈希码。
这个哈希码,①决定TL静态/实例变量在TLM Entry[i]数组中的存储位置索引i
②以及匹配Entry的reference,来获取当前TL存储的变量值
tLHC共使用2次,第一次是计算TL.tLHC&(16-1)得到TL在TLM实例Entry[i]的存储位置索引i值,第二次是在当前子线程的TLM对象中,与Entry对象的
//MR1类的静态TL变量tl1
MR1.tl1 488
threadLocalHashCode(tLHC) = 1253254570
//MR1类run实例的TL变量tl2
run 490
run.tl2 502
this$0 = 490[run]
tLHC = -1401181199
//Thread类实例t1(以run对象为构造参数)
t1.threadLocals[TLM] 516
//t1.table(TLM)
table[Entry[16]] 518
//t1.TLM.Entry
Entry 1[1]: 519
value 20
reference = 502 [run.tl2]
this$0 = 490 [run]
tLHC = -1401181199
Entry 2[10]:520
value 9
reference = 488 [MR1.tl1]
tLHC = 1253254570
某战斗机大队——Runnable实现类的某实例A
某大队内的战斗机s = 子线程s(由Runnable实现类某实例A构造的)
空中加油机 = Runnable实现类某实例A
输油管对接信标GPS = TL
油箱 = TL.TLM
航空燃油 = TL.TLM.Entry
讲类源码,一定是讲一个完整工作的流程,这样才能更清晰的明白各函数存在意义。
先来明确一下测试代码的完整工作流程:
第一步:加载Runnable线程类MR1,一并创建并赋值静态变量MR1.tl1,MR1.test1
第二步:实例化Runnable线程类MR1——》run对象,一并创建并赋值run.tl2,run.test2
注意,此时runnable的4种变量已经全部初始化
第三步:以runnable对象为参数构造子线程——》thread1、2对象
注意,如果基础好的大佬,第二步都清楚,Thread调用run法时是传入Runnable target对象,也就是上面的runnable对象,再去调用runnable对象的run方法。
那么问题就显而易见了,虽然以runnable对象构建的Thread类实例t1,t2,但是此时t1,t2的属性ThreadLocal.ThreadLocalMap threadLocals = null;
(图解部分提到)是构造为null的。
那么问题就来了,t1.threadLocals是何时赋值的?是直接由runnable实例对象的tl1,tl2直接传递对象赋值的吗?
第四步:t1/t2.start(),运行子线程。
流程结束。hhh抓耳挠腮吗?
Runnable线程类实例化 runnable对象,创建run对象的实例变量run.tl2
,run.test2
//psvm{}
MyRunnable1 runnable = new MyRunnable1();
MR1类中一共创建了4个变量:
静态成员变量test1,实例成员变量test2,静态成员变量tl1,实例成员变量tl2
简记:1为静态【MR1类只有唯一一个】,2为实例【MR1类每一个实例都有一个tl实例】
1与2的区别也很清晰了,即便不实例化也存在1静态对象,2实例对象依赖于父实例run对象
后面会专门解释关于静态TL与非静态TL的区别
//class MR1
//静态对象 tl1,test1
private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
//new Random().nextInt(10)
return 10;
}
};
private static int test1 = 100;
//实例对象 tl2,test2
private ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 20;
}
};
private int test2 = 200;
重点:在第二步之前,MR1的tl1/tl2被创建【仅创建TL对象,无TL关联对象】
注:创建tl1/2仅是复写了initialValue方法。只要run实例没有调用这个函数,Value对象就不会被TL创建的TLM存储。
TL本身不存储Value对象,TL存储的是实例化后的TL实例对象的唯一特殊标识值
下面给出TL
对象的构造过程
//ThreadLocal.java
//1.进入Object父类构造器,再进入TL构造无参函数(只有无参)
public ThreadLocal() {
}
//2.执行TL成员变量tLHC初始化(分配内存,及赋值)切记此时的tLHC仅仅是存储在TL中
//TL对象初始化时,不创建任何TLM/TLME对象,TL只为存储一个属性threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
//重点,下方的静态三人组!决定了一个重要的事情,在一个子线程内的TL对象s的唯一标识tLHC
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
//线程类(MR1)一个实例对象(run)的多个TL对象s在子线程内,会被唯一标识,以某种规律。
//以唯一标识 tLHC 会作为TLM Entry[i]数组的索引i值。
以线程类MR1实例run对象构造子线程t1,t2
//psvm{}
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
由于线程实例t1,t2的属性太多了,我就直接watch了,方便大家观测,创建完子线程t1、t2后,子线程自己的tls(TLM对象)指向是null的。
注意:以MR1
线程类的实例run
创建子线程t1
、t2
时,MR1
的静态tl1
与实例run.tl2
都没有与子线程t1 / t2.tls(TLM)
产生联系(创建t1/2.tls)。
启动线程t1、t2,执行MR1类实例run对象的run()方法。
public static class MyRunnable1 implements Runnable {
//重点:TL.get()/set()
public void staticTL1Change() {
MyRunnable1.threadLocal1.set(threadLocal1.get() - 1);
}
public void TL2Change() {
threadLocal2.set(threadLocal2.get() - 1);
}
@Override
public void run() {
//run实例方法
staticTL1Change();
TL2Change();
}
//此地只列出了TL变量相关方法,普通变量的请参考上面完整的测试代码
核心重点终于终于出现了!!!ThreadLocal的set和get方法
1.先列出执行threadLocal1.get() 函数的流程函数链
staticTL1Change(){ MyRunnable1.threadLocal1.set(threadLocal1.get() - 1); }
上一个函数调用的函数会在下方放置,内部类会缩进class,相较于源码只是顺序发生了改变,其余都是相同的格式
public class ThreadLocal<T> {
//一共有5个public函数:ThreadLocal(),withInitial(),get(),set(),remove()
//先列出get()
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//通过当前线程获得t对象的tls(TLM)对象
ThreadLocalMap map = getMap(t);//?
//如果t.tls(TLM)存在
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是TL变量第一次使用set/get()函数,当前线程的tls(TLM)肯定是null
//因为当前线程的tls(TLM)还未初始化,下面进行初始化顺便把 初始化值 赋进去
//如果TL变量不是第一次使用set/get()函数,可能会出现
//创建TL变量时复写的initialValue()内Value还没有存入TLM数组中。
//所以get()的return共有2个结果,获取initialValue()内的Value初始化值
//或者获取,在get()函数调用前,set()函数设置的覆盖值(覆盖了初始化值)。
return setInitialValue();//?
}
//?
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//?
private T setInitialValue() {
//将创建TL对象内部类中复写的Value获取,并传入value
T value = initialValue();//?
//???这里的判断,确实没有看懂,可能只是为了安全吧。???继承了也用不了,不懂
//获取当前线程对象t
Thread t = Thread.currentThread();
//获取线程t.tls(TLM)对象
ThreadLocalMap map = getMap(t);
//如果t.tls(TLM)存在
if (map != null)
//则直接将value存入TLM以Entry对象封装
//this是TL变量的reference。
map.set(this, value);
else
//一般是执行创建新TLM对象,上面的if我没想到哪种情况可以进去。
createMap(t, value);
return value;
}
//?此函数就是在MR1类中创建TL对象时,内部类中,复写的initialValue函数
protected T initialValue() {
return null;
}
void createMap(Thread t, T firstValue) {
//传入的是当前线程t的引用,也就是这个tls(TLM)对象是指向线程t的
//此时,将线程t中的t.threadLocals=null;————》t.tls = TLM对象;
//注意,传入TLM的this是调用createMap()的对象引用——MR1.tl1/run.tl2对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//顺便贴出TLM对象的构造函数
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建Entry[]数组,初始化容量为16
table = new Entry[INITIAL_CAPACITY];
//TL变量作为TLM数组的索引,Value变量存储在Entry中
//数组索引i的值=MR1.tl1/run.tl2.tLHC & (16-1=15)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//Entry[i] = Entry(MR1.tl1/run.tl2对象,Value对象)
//本测试案例的Value对象是initialValue()return的 10/20
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置调整大小阈值以保持最差的2/3负载系数。超过了负载系数会自动扩容
setThreshold(INITIAL_CAPACITY);
}
//Entry构造函数
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
//这个执行到最后就是:this.referent = referent;
super(k);
value = v;
}
}
}
}
2.再执行MyRunnable1.threadLocal1.set(threadLocal1.get() - 1);
set方法的流程函数链
public class ThreadLocal<T> {
public void set(T value) {
//获取当前线程t
Thread t = Thread.currentThread();
//获取当前线程t的tls(TLM)对象
ThreadLocalMap map = getMap(t);
//如果存在,则直接置入【当然前面的get方法已经初始化过了,这里就直接置入】
if (map != null)
//this是调用这个set方法的TL对象【置入到TML数组的位置由TL决定】
//注意:如果是set的TL属性已有值(例如初始值,在get调用的函数链中赋值的)
//此处的set方法是覆盖,因为引用为TL的Entry对象已经有initialValue初始值了
map.set(this, value);
else
createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
//注:TLM的长度会变的,所以使用.length
int len = tab.length;
//计算传入的TL变量在TLM的位置=索引i
int i = key.threadLocalHashCode & (len-1);
//set Value对象进入TLM,一共会遇到3种情况
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//先获取Entry对象的引用对象
ThreadLocal<?> k = e.get();
//?保存此TL变量的位置上已有与自己引用相同Entry且保存了一个Value
//判断,TL 与获取到的Entry对象的 引用对象 是否一致
if (k == key) {
//一致,则覆盖(是自己的孩子)Entry原有Value对象
e.value = value;
return;
}
//?保存此TL变量的位置上已有一个Entry,但是引用不一样,且==null
if (k == null) {
//则替换整个Entry对象,而不再是只替换Value对象
replaceStaleEntry(key, value, i);
return;
}
}
//?保存此TL变量的位置上不存在Entry对象,则新建一个Entry对象
tab[i] = new Entry(key, value);
int sz = ++size;
//cleanSomeSlots();
//试探性扫描一些格子们(Entry[]数组内对象)来寻找陈旧的Entry们对象并删除
//如果没删除任何条目,且超过负载系数
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//可能会增大Entry[](TLM)长度。
//(首先扫描整个表,删除陈旧Entrys。如果这还不足以缩小表,则将表的长度加倍)
rehash();
}
static class Entry extends WeakReference<ThreadLocal<?>>{
//继承abstract class Reference的方法(未复写)
public T get() {
return this.referent;
}
}
结束了吧????还有问题吗???我写到这里,反正我是吐了……你萌呢
hhh,还没有结束,来让我们来看看MR1实例run对象的2个子线程t1/t2执行体
对四个变量(静态tl1,test1,实例tl2,test2)函数调用的最终执行结果【并发执行】
//线程类MR1.staticTL1 在main父线程的子线程1,2中是同一个对象,线程间不共享
Thread[线程1,5,main]访问staticTL1变量10 : 9 || 316372558 || 19.46.12
Thread[线程2,5,main]访问staticTL1变量10 : 9 || 316372558 || 19.46.17
//线程类MR1实例run.TL2 在main父线程的子线程1,2中也是同一个对象,线程间不共享
Thread[线程1,5,main]访问 TL2变量20 : 19 || 306311720 || 19.46.12
Thread[线程2,5,main]访问 TL2变量20 : 19 || 306311720 || 19.46.17
//线程类MR1.statictest1 在main父线程的子线程1,2是同一个对象,线程间共享
Thread[线程1,5,main]访问statictest1变量100 : 99 || 19.46.12
Thread[线程2,5,main]访问statictest1变量100 : 98 || 19.46.17
//线程类MR1实例run.test1 在main父线程的子线程1,2也是同一个对象,线程间共享
Thread[线程1,5,main]访问 test2变量200 : 199 || 19.46.12
Thread[线程2,5,main]访问 test2变量200 : 198 || 19.46.17
分析
为什么普通的静态/实例变量会在由一个MR1类实例run对象构建的多个子线程间t1/t2共享?
非TL变量分2种,静态和实例,静态的非TL变量会在MR1类加载的时候就创建静态(类)变量并赋值,而实例的非TL变量会在MR1类实例化的时候进行创建并赋值。
注意!!!重点是:赋值了赋值了赋值了
也就是,无论多线程t1及t2访问的是静态还是实例变量,都是存在于别的地方(MR1类的变量对象/MR1类实例的变量对象)的东西**【在大家都能访问到的对象中存储】**
而TL对象本身不存储Value对象,只是构造时可携带一个initialValue对象,TL.TLM对象存储Value对象,而这个TLM对象是每个线程的私有属性,其他线程访问不到,Thread类实例化时TLM指向null,只有MR1类的TL变量(不分静态实例)调用了TL.get/set()方法才会为当前线程实例 t 创建 TLM 对象,存储MR1类的TL变量的initialValue对象,当然也可以在MR1类实例方法中调用set方法对initialValue覆盖,再get新值。【同时如果线程实例t的另一个TL变量再次调用get/set()会进行判断,如果当前存在TLM就不会再创建新的TLM】
大佬的这句话很经典:
【在MR1类实例run1对象的方法中共享,而不在MR1类实例run1/run2/run3…线程间共享】
一、线程类Runnable(简称R),TL,Value,TLM(Entry[]),Entry,子线程T的数量关系
1个R可以拥有n个TL变量 ==
n个R.TL
1个R可以构建n个子线程T 意味着 可在每1个(n个)子线程获取到n个R.TL
1个TLM
(Entry[])可以存储n个Entry对象
1个Entry
=(1个TL.reference,1个Value),Entry.reference存储1个TL的引用
1个TL可以用内部类复写initialValue方式,携带1个Value对象在方法区。但不是真正存储
1个子线程拥有1个TLM(Entry[])对象
1个子线程(拥有1个TLM(Entry[])对象)存储n个R.TL
变量
当在子线程T中调用R.TL
的set/get方法就会将R.TL
携带的Value对象赋值给T.TLM存储
具体的存储形式为new Entry对象存储。这样R.TL携带的Value就归子线程T所有。
二、MR1
类的 静态成员变量 tl1
与 实例成员变量 tl2
区别(代码实测)
造成区别的根本原因,是由static关键字导致的。
static修饰 tl1
变量,导致tl1
成为MR1.tl1
,而tl2
是MR1类实例的run.tl2
也就是,tl2
变量会在多个MR1类实例下是不同对象。
而tl1
变量在多个MR1类实例下是一个对象。
但是呢,起到的作用都是一样的……为每个线程创建独立Value变量副本
所以呢,就其实,你只写static TL 就可以了,不必要有多个TL实例的创建销毁开销。
测试实例
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("start");
//MR1类的两个实例run,run2
MyRunnable1 runnable = new MyRunnable1();
MyRunnable1 runnable2 = new MyRunnable1();
//run,run2对象构成两个线程。1实例1线程,不构成1实例多线程。
Thread thread1 = new Thread(runnable, "R1线程1");
Thread thread2 = new Thread(runnable2, "R2线程1");
}
//MR1类,静态成员变量tl1,实例成员变量tl2
public static class MyRunnable1 implements Runnable {
private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 10;
}
};
private ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 20;
}
};
public void say(){
System.out.println(Thread.currentThread()+" : tl1 : "+threadLocal1.hashCode());
System.out.println(Thread.currentThread()+" : tl2 : "+threadLocal2.hashCode());
}
@Override
public void run() {
say();
}
}
}
测试结果
Thread[R1线程1,5,main] : tl1 : 1272488711
Thread[R1线程1,5,main] : tl2 : 1159681036
Thread[R2线程1,5,main] : tl1 : 1272488711
Thread[R2线程1,5,main] : tl2 : 401793225
三、为什么TL,TLM,Entry是静态内部类的结构?
TL,TL.TLM,TL.TLM.Entry
理由的话,就是:不需要单独写一个TLM.java及Entry.java类文件,TLM和Entry只在TL类中使用,没必要拎出来做一个独立类文件(主要是没别的类用TLM。且Entry特别小)
降低包的深度,方便类的使用。静态内部类适用于包含类当中,但又不依赖与外在的类,不用使用外在类的非静态属性和方法,只是为了方便管理类结构而定义。
Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.——Oracle
declared static are called static nested classes.准确的翻译叫做静态嵌套类
,非内部类
四、为什么使用Entry对象封装Value,而不是Map
因为,Entry相较于Map
Entry在获取key的同时获取value,只是减少了Map在遍历全部
代码如下
while(iterator.hasNext()) {
Object key = iterator.next();
Object value = map.get(key);
}
while(iterator.hasNext()) {
Map.Entry entry = iterator.next();
Object key = entry.getKey();
Object value = entry.getValue();
}
Entry提供给开发人员一个同时保持了key和其对应的value的对象的类。
五、TL.threadLocalHashCode到底是起什么作用?
TL.tLHC的作用:作为Entry(
TLM根据TL.tLHC的值确定该TL携带的Value存储在TLM(Entry[i])table数组中的位置,如果发现这个位置上已经有其他TL.tLHC值的Value占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
TLM解决TL.Hash冲突的方式就是简单的步长=1,寻找下一个相邻的位置。
static class ThreadLocalMap {
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}
显然当有大量不同的TL对象放入TLM中时发生冲突,这种线性探测的执行效率很低。
故建议:TL变量尽量少设置。别太多。
六、关于TL中内存泄漏问题
大佬们讨论都是当创建ThreadLocalMap的线程实例t一直持续运行
Entry(
注意:TL内存泄漏的根源是由于TLM的生命周期跟Thread实例一样长,如果没有手动删除Entry(
而与Entry(
那么重点来了,怎么避免Entry(
就是在MR1类中调用完TL.get()/set()方法后再调用remove(),将Entry和TLM的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达,下次GC的时候就会被回收
当然,Java TL开发团队也想到了这个问题,在get和set方法内部就做了很多与null相关的判断并联动remove(),不过最好还是自己手动remove更保险
关于TLM的内存泄漏再具体一点,什么时候TLM一直存在呢??并且被反复使用呢???
就是当 thread 配合线程池使用的情况下,thread在运行完毕之后会被再次放回线程池。
如果thread线程实例不被线程池销毁,且闲置下来,
TLM(Entry[])及其内部存储的Entry(
此时再TL.get()就可能会出现内存泄漏,或者称之为脏读。
五、六问题答案大量参考大佬:Misout
最后一个问题 七、我怎么眼花在Thread类搜到了两个TML属性?
没错,你没眼花,确实还有一个,叫做:inheritableThreadLocals(itls)
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
这个itls和tls是一样的东西,只不过是用于copy父线程tls对象的子线程itls对象
没错。就是new了一个线程a,在线程a里再new一个线程b,让b继承a的tls设置的tls对象
使用上与tls无区别,这里就不详述了。【参考lnktoking的子线程共享父线程 博文】
大佬们s
郭俊 Jason:Java进阶(七)正确理解Thread Local的原理与适用场景
liuzhengyang:ThreadLocal使用和源码分析
书呆子Rico:深入理解Java对象的创建过程:类的初始化与实例化
Misout:ThreadLocal-面试必问深度解析
liangzzz:JAVA并发-自问自答学ThreadLocal
Java3y:ThreadLocal就是这么简单
占小狼:深入浅出ThreadLocal
chicm:ThreadLocal类型变量为何声明为静态?
Zebe:Java8-ThreadLocal的Lambda构造方式:withInitial
iteye_1900:Map.Entry 类使用简介
lnktoking:多线程之子线程共享父线程的ThreadLocal:InheritableThreadLocal
hadoop_dev:Java多线程中static变量的使用
知乎:为什么Java内部类要设计成静态和非静态两种?
格色情调1984:静态变量与非静态变量的区别