多线程之ThreadLocal

什么是ThreadLocal

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
从源码来看:

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法,也就是说,当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。。
使用ThreadLocal类,那么threadLocal对象不会传递到子线程,不存在父子线程的线程安全问题;只有使用InheritableThreadLocal类时,会在子线程初始化时,将父线程的inheritableThreadLocal对象的值填充到子线程,此时父线程和子线程才是共享这一对象的——共享那么当然子线程的修改对父线程是可见的,也就是会有父子线程的线程安全问题,即使在initialValue()方法中new对象也是无法解决,得到的仍然会是父线程传递过来的对象(如果使用InheritableThreadLocal类的childValue()方法new一个对象,倒是真的可以在子线程中使用新new的对象)。
下面是网上常见的一张threadlocal,threadlocalmap,thread的关系图:
threadlocal关系图
从上面的结构图,就能ThreadLocal的核心机制:

  • 每个Thread线程内部都有一个键值对的threadlocalmap,叫做threadlocals,存放线程私有数据(通常是业务对象)。
  • threadlocals的entry的键(key)是threadlocal对象,也就是我们需要new的,值(value)就是目标存放的线程私有变量/对象。
  • 但是,我们不能直接去操作threadlocalmap增减entry,它只能通过ThreadLocal对象去操作,所以我们要先new一个ThreadLocal对象,通过它的set/get方法向threadlocals增删改查它对应的entry。
public void set(T value) {
    //1. 获取当前线程实例对象
    Thread t = Thread.currentThread();
    //2. 通过当前线程实例获取到ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //3. 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入
        map.set(this, value);
    else
        //4.map为null,则新建ThreadLocalMap并存入value
        createMap(t, value);
}
//创建ThreadLocalMap,并绑定关系
void createMap(Thread t, T firstValue) {
    t.threadlocals = new ThreadLocalMap(this, firstValue);
}

set方法总结:

  1. 通过当前线程对象thread获取该thread所维护的ThreadLocalMap;
  2. 若ThreadLocalMap不为null,则以ThreadLocal实例为key,值为value的键值对存入ThreadLocalMap;
    若ThreadLocalMap为null的话,就新建ThreadLocalMap然后在以threadLocal为键,值为value的键值对存入即可。

    public T get() {
        Thread t = Thread.currentThread();
       //获取此线程中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//如果此map存在则获取对应的存储实体
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //如果该实体e不为空,则返回我们之前存储的值
                T result = (T)e.value;
                return result;
            }
        }
        //若map不存在执行当前代码
        //若map存在没有与当前ThreadLocal关联的entry实体则执行当前代码
        return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadlocals;
    }
 
    private T setInitialValue() {
    	//获取初始化的值,该方法可以被子类重写,如果不重写默认返回null
        T value = initialValue();
        
        //获取当前线程的map
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
 
    //这个方法是protected修饰的也就是说继承ThreadLocal的子类可重写该方法,实现赋值为其他的初始值。
    protected T initialValue() {
        return null;
    }

get方法总结:

  1. 通过当前线程thread实例获取到它所维护的ThreadLocalMap,然后以当前threadLocal实例为key获取该map中的键值对(Entry);
  2. 若Entry不为null则返回Entry的value;
    如果获取ThreadLocalMap为null或者Entry为null的话,就以当前threadLocal为Key,value为null存入map后,并返回null。也可以value不为null存入,须自定义initialValue方法。

ThreadLocal与ThreadLocalMap

通过上面这些内容,我们足以得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装(壳),或者说是代理,为其传递变量/对象。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。
这种类似代理的紧密关系就是它们的设计初衷,反应在代码里则是:ThreadLocalMap是ThreadLocal的静态内部类。
静态内部类

为什么是静态内部类?

一般来说,static是不能用来修饰类的,但是成员内部类可以看做外部类中的一个成员,所以可以用static修饰,这种用static修饰的内部类我们称作静态内部类,也称作嵌套内部类. 所以通常静态用来修饰类的话那一定是内部类,而内部类又分静态和非静态,他们有什么区别?或者说静态内部类有什么意义?

它们的区别

  • 访问类内部成员的权限

静态类:只能访问类内部的静态成员;
非静态类:可访问类内所有成员;

  • 声明类内部成员的权限

静态类:可以声明静态成员和非静态成员;
非静态类:只能声明非静态成员;

  • 类的初始化方式

假设,类结构为:Outer类内包含Inner类,InnerStatic类为Outer类的内部静态类,Inner为非静态类
静态类的初始化:
Outer.InnerStatic innerStatic = new Outer.InnerStatic();
非静态类初始化:
Outer.Inner inner = new Outer().new Inner();
或者是
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

  • 通俗的讲,对于它们所属的外部类来说,

静态内部类就是个独立的类,我跟你没关系,自己可以完全独立存在,但是我就想借你的壳用一下,来隐藏一下我自己(就算外部类没实例化,它也能实例化)。
非静态内部类才是真正的内部类,我了解你,是你的一部分,知道你的全部,没有你就没有我,并且编译后会默认的保存一个指向外部类的引用。(外部类实例化了它才能实例化,所以内部类对象是以外部类对象存在为前提的)

什么时候使用静态内部类

  1. 比如有A,B两个类,B有点特殊,虽然可以独立存在,但只被A使用。这时候怎么办?如果把B并入A里,复杂度提高,搞得A违反单一职责。如果B独立,又可能被其他类(比如同一个包下的C)依赖,不符合设计的本意。所以不如将其变成A.B,等于添加个B的注释(A.),告诉其他类别使用B了,它只跟A玩。
    显然ThreadLocal和ThreadLocalMap的关系就很适合。
  2. 当某个类需要接受多个参数进行初始化时,推荐使用静态类构建。
    比如:
    没有使用静态类的例子:
public class Car {
    private String name;
    private String model;
    private int height;
    private int width;

    public Car(String name){
        this.name=name;
    }
    ......
    //这里省略了 若干Car类的构造函数
    public Car(String name,String model,int height,int width){
        this.name=name;
        this.model=model;
        this.height=height;
        this.width=width;
    }
    ......
}

下面调用可以看出,未使用静态类的初始化构造函数 初始化变量很不灵活, 如果有太多变量需要初始化,并且还要有更多种选择时,那类的代码将非常庞大。

public class test {
    Car car1 = new Car("捷达王","大众捷达",16501950);
    Car car2 = new Car("皇冠");
     car2.setModel("丰田皇冠3.0");
     car2.setHeight("1920");
}

下面是使用静态类的实现:

public class Car {
    private String name;
    private String model;
    private int height;
    private int width;

    private Car(Builder build){
        this.name=build.name;
        this.model= build.model;
        this.height=build.height;
        this.width=build.width;
    }
    public static class Builder {
        private String name;
        private String model;
        private int height;
        private int width;
        public Builder(){

        }
        public Builder withName(String name){
            this.name=name;
            return this;
        }
        public Builder withModel(String model){
            this.model=model;
            return this;
        }
        public Builder withHeight(int height){
            this.height=height;
            return this;
        }
        public Builder withWidth(int width){
            this.width=width;
            return this;
        }
        public Car build(){
            return new Car(this);
        }
    }
}

这样调用代码就非常灵活,适合各种情况:

public class test {
    Car jettaCar = new Car.Builder().withName("捷达王")
                                .withModel("大众捷达")
                                .withHeight(1650)
                                .withWidth(1950)
                                .build();
    Car toyotaCar = new Car.Builder().withName("皇冠")
                                .withModel("丰田皇冠3.0")
                                .withHeight(1920)
                                .build();
}

ThreadLocalMap的Hash冲突

为了解决散列冲突,主要采用下面两种方式: 分离链表法(separate chaining)和开放定址法(open addressing)。

分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。典型实现为hashMap,concurrentHashMap的拉链法。
开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。

与concurrentHashMap,hashMap等容器一样,threadLocalMap也是采用散列表进行实现的。但是和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测(开放定址法)的方式。所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
在这里插入图片描述
源码中通过nextIndex(i, len)方法解决hash冲突的问题,该方法为((i + 1 < len) ? i + 1 : 0);,也就是不断往后线性探测,当到哈希到表末尾的时候再从0开始,成环形, 看看ThreadLocalMap的set方法:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
		
    //使用线性探测法查找元素
    //根据计算出的索引,从数组中取出一个元素e,对其进行判空。
    for (Entry e = tab[i];  e != null;e = tab[i = nextIndex(i, len)]) {
        //如果不为空则取出对应的键(ThreadLocal对象)
        ThreadLocal<?> k = e.get();
 
	//如果该键已经存在,则覆盖其值。
        if (k == key) {
            e.value = value;
            return;
        }
 
	/*
            如果该键不存在(e不为null,但k为null。
            说明存在之前的ThreadLocal对象的强引用已经被释放掉了)当前数组中的Entry是一个陈旧的元素。
            用新元素替换旧元素。
        */
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
 
    //创建一个新的Entry实例,插入在空元素位置处
    tab[i] = new Entry(key, value);
    int sz = ++size;
		
    /*
        清除那些key已经为null,但值还存在的entry(这个可以避免内存泄漏)。
        如果没有清除任何entry,且当前的使用量已经达到了阈值,则进行一次全表的扫描清理。
    */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
 =====================================================================
//当i >= len-1(即数组最后一个索引时),返回的是0;否则让数组索引每次增加1。这相当于一个循环数组
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

总结

  1. 根据key计算出索引i,然后查找i位置上的Entry
  2. 若是Entry已经存在并且k等于传入的key ,那么这时候直接给这个Entry赋新的value值
  3. 若是Entry存在,但是k为null ,则调用replaceStaleEntry来更换这个k为null的Entry
  4. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return ,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

ThreadLocal与内存泄漏

下图显示了threadLocal变量,threadlocals,线程等在内存中的布局
虚线标识弱引用
注意上图中的实线表示强引用,虚线表示弱引用
强引用(StrongReference) 是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference) 则是当垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
Memory overflow(内存溢出):没有足够的内存提供给申请者使用。
Memory leak(内存泄漏):是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
由于Entry中的key是堆中ThreadLocal实例的弱引用,当ThreadLocal实例外部强引用(ThreadLocalRef)随着业务完成被置为null后, 那么下一次系统 GC 的时候,根据可达性分析,这个堆中的threadLocal实例就只剩下map中key的弱引用,那么这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value -> 业务对象,那这个entry的value强引用所指向的内存中的业务对象就一直存在无法被回收,造成内存泄漏。
那如果Entry中的key是强引用是否还有这种情况?
强引用
可以看出,如果是强引用,不仅value无法被回收,整个entry都无法被回收。

如何避免内存泄露

有两种方法:

  1. 当业务结束使用完ThreadLocal 对象时,调用其remove方法删除threadlocals中对应的Entry
    在这里插入图片描述
  2. 线程的生命周期非常短,使用完ThreadLocal ,当前Thread也随之运行结束,线程中的threadlocals这整个map自然也会被全部回收。
    显然,第一种方式最灵活,而且在业务中通常都会用到线程池,第二种就完全不适用了。

为什么建议private static

首先,是否使用private修饰与ThreadLocal本身无关,只是为了安全,也就是说,是否使用private修饰是一个普遍的问题而不是与ThreadLocal有关的一个具体问题。
其次,ThreadLocal一般会采用static修饰。因为这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享 此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只 要是这个线程内定义的)都可以操控这个变量。
换句话说ThreadLocal只是为了将线程共享对象隔离成每个线程的私有变量,所以没必要在每个线程内部为同个类的多个实例重复创建TSO(Thread Specific Object,即ThreadLocal所关联的对象),这即便不会导致错误,也会导致浪费。
但是,static关键字也会让ThreadLocalRef(见上面threadlocal内存布局图)的生命周期延长 --> ThreadMap的key在线程生命期内始终有值 --> ThreadMap中对应的整个entry在线程生命期内不释放 ——> 造成内存泄漏。
故线程池下,用static修饰theadLocal引用,必须(1)手动remove (2)手动 ThreadLocal ref = null。

应用实例

在自己实现一个线程安全的TransactionManager(本文引用博客链接3)这个博客中,定义一个线程安全的SingleThreadConnectionHolder连接类如下:

public class SingleThreadConnectionHolder  
{  
    private static ThreadLocal<ConnectionHolder> localConnectionHolder = new ThreadLocal<ConnectionHolder>();  
             
    public static Connection getConnection(DataSource dataSource) throws SQLException  
    {  
        return getConnectionHolder().getConnection(dataSource);  
    }  
             
    public static void removeConnection(DataSource dataSource)  
    {  
        getConnectionHolder().removeConnection(dataSource);  
    }  
             
    private static ConnectionHolder getConnectionHolder()  
    {  
        ConnectionHolder connectionHolder = localConnectionHolder.get();  
        if (connectionHolder == null)  
        {  
            connectionHolder = new ConnectionHolder();  
            localConnectionHolder.set(connectionHolder);  
        }  
        return connectionHolder;  
    }  
             
}  

文中提到每当事务提交后,都会调用这个类的remove方法,防止连接threadlocal内存泄露。

References:
本文引用博客链接1
本文引用博客链接2
本文引用博客链接3

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值