一.ThreadLocal是什么

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy83NDMyNjA0LWFkMmZmNTgxMTI3YmE4Y2MuanBnP2ltYWdlTW9ncjIvYXV0by1vcmllbnQvc3RyaXAlN0NpbWFnZVZpZXcyLzIvdy84MDYvZm9ybWF0L3dlYnA.jpg

  • ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的一个局部变量。
  • 每个Thread线程内部都有一个Map。
  • Map里面存储线程本地对象(key)和线程的变量副本(value)
  • 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

二、深入源码分析

(1)ThreadLocal类提供如下几个核心方法:

public T get()     //用于获取当前线程的副本变量值。
public void set(T value)   //用于保存当前线程的副本变量值。
public void remove()      //移除当前前程的副本变量值。
protected T initialValue()   //为当前线程初始副本变量值,一般是用来在使用时进行重写的,它是一个延迟加载方法

(2)首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。

public T get() {
    Thread t = Thread.currentThread();           
    ThreadLocalMap map = getMap(t);              //1.获取当前线程的ThreadLocalMap对象threadLocals
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);   //2.从map中获取线程存储的K-V Entry节点。
        if (e != null)
            return (T)e.value;               //3.从Entry节点获取存储的Value副本值返回。         
    }
    return setInitialValue();             
}
 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;             //调用当期线程t,返回当前线程t中的一个成员变量threadLocals。
                                       //实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类
}
 
private T setInitialValue() {         //4.如果map不为空,就设置键值对,为空,再创建Map
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
 
protected T initialValue() {
    return null;
}
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);      //1.获取当前线程的成员变量map
    if (map != null) 
        map.set(this, value);         //2.map非空,则重新将ThreadLocal和新的value副本放入到map中。
    else
        createMap(t, value);
}
 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
 
void createMap(Thread t, T firstValue) {  //3.map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);
}
 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

至此,可能大部分朋友已经明白了ThreadLocal是如何为每个线程创建变量的副本的:

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

(3)ThreadLocalMap

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy83NDMyNjA0LTViYmUwOTBkNDY3ODkwODQucG5nP2ltYWdlTW9ncjIvYXV0by1vcmllbnQvc3RyaXAlN0NpbWFnZVZpZXcyLzIvdy81NzYvZm9ybWF0L3dlYnA.jpg

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。

Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。

 
static class ThreadLocalMap {
    
    private static final int INITIAL_CAPACITY = 16;
    
    private Entry[] table;
 
    private int size = 0;
 
    private int threshold; // Default to 0
}

Hash冲突怎么解决 :

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式
并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果
发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
 
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
 
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
 
所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程
要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

ThreadLocal内存泄漏问题:

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key
会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
 
JVM解决方案:把ThreadLocal里的Entry设置为弱引用,当垃圾回收的时候,回收ThreadLocal。
 
什么是弱引用?
1.Key使用强引用:也就是上述说的情况,引用的ThreadLocal的对象被回收了,ThreadLocal的引用ThreadLocalMap的Key
  为强引用并没有被回收,如果不手动回收的话,ThreadLocal将不会回收那么将导致内存泄漏。
2.Key使用弱引用:引用的ThreadLocal的对象被回收了,ThreadLocal的引用ThreadLocalMap的Key为弱引用,如果内存回收,
  那么将ThreadLocalMap的Key将会被回收,ThreadLocal也将被回收。value在ThreadLocalMap调用get、set、remove的时候就会被清除。
3.比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是
  使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
4.但是如果线程迟迟无法结束,也就是ThreadLocal对象将一直不会回收,那么也将导致内存泄漏。

三.ThreadLocal的应用场景

1.数据库连接

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  

2.Session管理

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

面试——ThreadLocal知多少

ThreadLocal被称为线程局部变量,就是线程工作内存的一小块内存,用于存储数据

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

ThreadLocal应用在数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。

ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。


版权声明:文章转载请注明来源,如有侵权请联系博主删除!
最后修改:2019 年 12 月 25 日 01 : 39 PM
如果觉得我的文章对你有用,请随意赞赏
评论打卡也可以哦,您的鼓励是我最大的动力!