菜鸟笔记
提升您的技术认知

threadlocal详解-ag真人游戏

在 java 线程中,每个线程都有一个 threadlocalmap 实例变量(如果不使用 threadlocal,不会创建这个 map,一个线程第一次访问某个 threadlocal 变量时,才会创建)。

threadlocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,threadlocal并不是一个thread,而是thread的局部变量,也许把它命名为threadlocalvariable更容易让人理解一些。

它主要由四个方法组成initialvalue(),get(),set(t),remove(),其中值得注意的是initialvalue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()时才执行,并且仅执行1次(即:最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用get()方法访问变量的时候。如果线程先于get方法调用set(t)方法,则不会在线程中再调用initialvalue方法)。threadlocal中的缺省实现直接返回一个null:

该 map 是使用线性探测的方式解决 hash 冲突的问题,如果没有找到空闲的 slot,就不断往后尝试,直到找到一个空闲的位置,插入 entry,这种方式在经常遇到 hash 冲突时,影响效率。
下面我们就具体探讨一下threadlocal。

threadlocal的作用主要是做数据隔离,threadlocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
threadlocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

说到隔离,我们应该不难联系到事务的隔离,没错,spring实现事务隔离采用的就是threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别(@transaction),巧妙地管理多个事务配置之间的切换,挂起和恢复。

spring框架里面就是用的threadlocal来实现这种隔离,主要是在transactionsynchronizationmanager这个类里面:

值得我们注意的是:spring的事务主要是threadlocal和aop去做实现的

除此之外,我们在使用simpledataformat时也会用到,可能你在使用simpledataformat时只是简单的new了一个simpledataformat对象,但是在我们使用simpledataformat的parse()方法时,其方法内部有一个calendar对象,调用simpledataformat的parse()方法会先调用calendar.clear(),然后调用calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

解决这个问题最简单的办法就是让每个线程都new 一个自己的 simpledataformat就好了,但是有个很大的问题就是如果我们有1000个线程难道new1000个simpledataformat?

所以我们这个时候就可以利用线程池加上threadlocal包装simpledataformat,再调用initialvalue让每个线程有一个simpledataformat的副本,从而解决了线程安全的问题,也提高了性能。

你以为只有这么多地方可以用到threadlocal???别着急,如果项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。

如果我们使用类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我们使用threadlocal稍微去做了一下改造,这样只需要在调用前在threadlocal中设置参数,其他地方get一下就好了,就像下面那样:

同时,像我们经常使用的cookie,session等数据隔离都是通过threadlocal去做实现的。

上面我也提到了threadlocal主要是用来做数据隔离使用的,那么它和synchronized有什么区别呢?

threadlocal其实是与线程绑定的一个变量。threadlocal和synchonized都用于解决多线程并发访问。

但是threadlocal与synchronized有本质的区别:
1、synchronized用于线程间的数据共享,而threadlocal则用于线程间的数据隔离。

2、synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
而threadlocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

简单来说threadlocal,threadlocl是作为当前线程中属性threadlocalmap集合中的某一个entry的key值entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的threadlocalmap是独一无二的,也就是不同的线程间同一个threadlocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

threadlocal localname = new threadlocal();
localname.set("张三");
string name = localname.get();
localname.remove();

上面的代码很简单:在threadlocal存放一个元素,然后再去获取它最后再把这个元素给移除,整体来说threadlocal也就这个三个基本操作:
set、get、remove

我们依次进源码来看一下:

4.1 set

set做的事很简单:主要就是threadlocalmap我们需要重点关注一下,而threadlocalmap呢是当前线程thread一个叫threadlocals的变量中获取的。

看到这儿,我们其实就已经发掘出threadlocal数据隔离的真相了。

每个线程thread都维护了自己的threadlocals变量,所以在每个线程创建threadlocal的时候,实际上数据是存在自己线程thread的threadlocals变量里面的,别人没办法拿到,从而实现了隔离。

上面我提到了一个threadlocalmap,threadlocalmap底层结构是怎么样子的呢?

4.2 threadlocalmap

我们先看看上图所示的源码,
既然有个map那他的数据结构其实是很像hashmap的,但是看源码可以发现,它并未实现map接口,而且他的entry是继承weakreference(弱引用)的,也没有看到hashmap中的next,所以不存在链表了。

我简单说明一下弱引用:弱引用主要应用在不阻止它的key或者value 被回收的mapping,什么意思呢?弱引用的出现就是为了垃圾回收服务的。它引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收。

此时就会产生一个问题,没有了链表怎么解决hash冲突呢?

threadlocalmap 结构就是 entry 数组,我们开发过程中可以一个线程可以有多个treadlocal来存放不同类型的对象的,但是他们都将放到你当前线程的threadlocalmap里,所以肯定要数组来存。
至于具体是如何解决hash冲突的,我们先过一下源码:

从源码里面看到threadlocalmap在存储的时候会给每一个threadlocal对象一个threadlocalhashcode,在插入过程中,根据threadlocal对象的hash值,定位到table中的位置i,int i = key.threadlocalhashcode & (len-1)
很明显这是很简单的线性探测法,所以解决hash冲突的方式为线性探测法
然后会判断一下:如果当前位置是空的,就初始化一个entry对象放在位置i上。

如果位置i不为空,如果这个entry对象的key正好是即将设置的key,那么就刷新entry中的value;

如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
整体流程如下图所示:

由此,在get的时候,也会根据threadlocal对象的hash值,定位到table中的位置,然后判断该位置entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

4.3 get

上图便是get的所有过程

说到这里很多人可能在想threadlocal的实例以及其值存放在哪里呢?

在java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。

但是并不能说threadlocal的实例以及其值存放在栈上,虽然threadlocal中值为每个线程所私有,threadlocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而threadlocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

使用inheritablethreadlocal可以实现多个线程访问threadlocal的值,我们在主线程中创建一个inheritablethreadlocal的实例,然后在子线程中得到这个inheritablethreadlocal实例设置的值。

    public void test() {
  
        final threadlocal threadlocal = new inheritablethreadlocal();
        threadlocal.set("ninesun");
        thread t = new thread() {
  
            @override
            public void run() {
  
                super.run();
                system.out.println("获取存放的值:"   threadlocal.get());
            }
        };
        t.start();
    }

好了,我们现在知道了使用inheritablethreadlocal可以实现多个线程访问threadlocal的值,但是这些值是怎么在子线程之间进行传递的呢?
传递的逻辑很简单,

上图是我截取的thead里面代码片段,thread在初始化创建的时候(即构造函数里)有以下操作:

这段代码也很简单,大致意思是:如果线程的inheritthreadlocals变量不为空,而且父线程的inheritthreadlocals也存在,那么我就把父线程的inheritthreadlocals给当前线程的inheritthreadlocals。比如我们上面的例子。

threadlocal已经讲了大半了,可是你可能还没意识到问题的严重性,因为上面提到,key是弱引用,而value却是强引用,如果我们在使用threadlocal操作不当时,就会导致一个很严重的后果:内存泄漏

我们可以看到,threadlocal在保存的时候会把自己当做key存在threadlocalmap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成weakreference弱引用了。

上图便是key被gc以后的场景。
产生上面这种场景的原因来自于弱引用对象的生命周期

只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

这就导致了一个问题,threadlocal在没有外部强引用时,发生gc时会被回收,如果创建threadlocal的线程一直持续运行,那么这个entry对象中的value就有可能一直得不到回收,发生内存泄露。

这样说可能不是很直观,我举个简单的例子:

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,threadlocal设定的value值被持有,导致内存泄露。

我们怎么取解决呢?
解决办法太简单了,在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。
比如,我们之前的代码是:

    public void test() {
  
        final threadlocal threadlocal = new inheritablethreadlocal();
        threadlocal.set("ninesun");
    }

那么我们就可以通过:

    public void test() {
  
        final threadlocal threadlocal = new inheritablethreadlocal();
        try {
  
            threadlocal.set("ninesun");
        } finally {
  
            threadlocal.remove();
        }
    }

remove的源码也很简单,如上图所示,就是找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。

那么问题来了,为啥非得把key设计为弱引用?

key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。

如果 threadlocalmap 的 key 是强引用, 那么只要线程存在, threadlocalmap 就存在, 而 threadlocalmap 结构就是 entry 数组. 即对应的 entry 数组就存在, 而 entry 数组元素的 key 是 threadlocal。

即便我们在代码中显式赋值 threadlocal 为 null, 告诉 gc 要垃圾回收该对象. 由于上面的强引用存在, threadlocal 即便赋值为 null, 只要线程存在, threadlocal 并不会被回收。

而设置为弱引用, gc扫描到时, 发现threadlocal 没有强引用, 会回收该threadlocal对象。

并且 threadlocal 的 set get remove 都会判断是否 key 为 null, 如果为 null, 那么 value 的也会移除, 之后会被 gc 回收。

threadlocal的不足,如解决冲突使用效率最低的线性探测法之类的,可以看看netty的fastthreadlocal来弥补。《谈谈fastlocal为啥这么快》

网站地图