【JavaSE8 高级编程 多线程 ThreadLocal】ThreadLocal详解 2019_7_30

多线程入门补充

  • ThreadLocal
    • 概念解释
      • 概念实例化执行结果观测
    • 应用场景
      • 用户请求之Session
      • 子线程与Connection
    • 完整测试实例及分析
      • 用语约定
      • 测试源码
      • 运行结果
      • Q&A
        • 先回答问题c
        • 再回答问题b
        • 重点···问题a
      • 补充小知识
    • ThreadLocal源码分析
      • TL与TL.M与TL.M.Entry图解
      • 内存中结构及地址展示
      • 简单比喻
      • 以测试代码流程进行TL源码解析
        • 测试代码流程分析
        • debug详细分析
          • 第一步
          • 第二步
          • 第三步
          • 第四步
      • 那些年我们仍未知道答案的问题
  • 重要参考博文

ThreadLocal

如果对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,这俩根本不是一个层次的概念。无从比较

应用场景

用户请求之Session

Http服务中的已登录用户的请求携带Session登录信息
在http容器中会保存已登录用户的登录信息。
因为http容器通常是一个线程负责执行一个请求的业务逻辑,一个用户多个请求会生成多个线程,希望为用户的每个线程/请求进行同一标识也就是每个线程分别存储这个用户的Session对象,需要注意的是,这个session并不是从始至终在所有线程的内容都是一样的,根据各个请求的处理,各个线程的Session会遇到不同的修改。

子线程与Connection

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&A

先回答问题c

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

可能上面的文字太多了,理解起来有点麻烦,那么就先回答一个叫简单的问题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);
    }

重点···问题a

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模板,在整个程序中永远唯一的元素。

ThreadLocal源码分析

TL与TL.M与TL.M.Entry图解

①大佬的图【Misout】
【JavaSE8 高级编程 多线程 ThreadLocal】ThreadLocal详解 2019_7_30_第1张图片

②我的图解有点复杂,需要与下面的文字解析一起使用。
【JavaSE8 高级编程 多线程 ThreadLocal】ThreadLocal详解 2019_7_30_第2张图片
简叙
创造这个图的时候,没有将类模板与类实例区分开来,而是直接混合在一起,所以看起来会有些混乱,请不要多怪罪。
一共分为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对象的的key(reference)判断是否与TL.tLHC相同,如果key!=null且key=TL.tLHC,就返回Entry.getValue(返回Value对象),或执行TLM.getEntryAfterMiss()——在直接散列数组TLM Entry[]对象中找不到key时的getEntry()版本。如果最终还是找不到,就会返回null。

内存中结构及地址展示

//MR1类的静态TL变量tl1
MR1.tl1 488threadLocalHashCode(tLHC) = 1253254570

//MR1类run实例的TL变量tl2
run 490
run.tl2 502this$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

以测试代码流程进行TL源码解析

测试代码流程分析

讲类源码,一定是讲一个完整工作的流程,这样才能更清晰的明白各函数存在意义。
先来明确一下测试代码的完整工作流程:
第一步:加载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抓耳挠腮吗?

debug详细分析

第一步

加载类,创建静态变量tl1,test1
【JavaSE8 高级编程 多线程 ThreadLocal】ThreadLocal详解 2019_7_30_第3张图片

第二步

Runnable线程类实例化 runnable对象,创建run对象的实例变量run.tl2,run.test2

	//psvm{}
	MyRunnable1 runnable = new MyRunnable1();

【JavaSE8 高级编程 多线程 ThreadLocal】ThreadLocal详解 2019_7_30_第4张图片
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");

【JavaSE8 高级编程 多线程 ThreadLocal】ThreadLocal详解 2019_7_30_第5张图片
由于线程实例t1,t2的属性太多了,我就直接watch了,方便大家观测,创建完子线程t1、t2后,子线程自己的tls(TLM对象)指向是null的。

注意:以MR1线程类的实例run创建子线程t1t2时,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的形式,不需要通过get(key)的形式遍历所有key来获取value,只需要直接获取Entry对象,然后通过getValue()即可得的value。逻辑上更简洁
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(Entry[i])table数组对象中的位置 i

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()作为key的MR1.TL引用被GC回收后,而Entry本身存储在TLM Entry[]中而不被回收,导致的Entry()仍然存在——内存泄漏。

注意:TL内存泄漏的根源是由于TLM的生命周期跟Thread实例一样长,如果没有手动删除Entry()就会因为Entry()的存在导致内存泄漏。
而与Entry()的key是弱引用无关。

那么重点来了,怎么避免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( 当这个线程被再次启用,那么threadLocalMap也就不会再重新初始化了。
此时再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:静态变量与非静态变量的区别

你可能感兴趣的:(Java高级,多线程,Java高级,ThreadLocal,Java,8)