ThreadLocal的原理剖析和使用场景

x33g5p2x  于2022-02-21 转载在 其他  
字(4.1k)|赞(0)|评价(0)|浏览(525)

这又是一道大厂经常问到的Java并发编程方面的面试热点!

一、ThreadLocal的概念

ThreadLocal 主要是做数据隔离,它是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,相对隔离的。
   当使用ThreadLocal维护变量的时候为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本,这样就不存在线程安全问题,也不会影响程序的执行性能。

二、ThreadLocal解决了什么问题?

解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。

三、ThreadLocal原理

每个Thread对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。

1、比较重要的几个方法

  • public T get():从线程上下文环境中获取设置的值。
  • public void set(T value):将值存储到线程上下文环境中,供后续使用。
  • public void remove():清除线程本地上下文环境。

2、源码分析

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 * (将此线程局部变量的当前线程副本设置为指定值。 大多数子类将不需要覆盖此方法,仅依赖于initialValue方法来设置线程initialValue的值)
 *
 * @param value the value to be stored in the current thread's copy of this thread-local.
 * (要存储在此线程本地的当前线程副本中的值。)
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 * (返回此线程局部变量的当前线程副本中的值。 如果该变量对于当前线程没有值,则首先将其初始化为调用initialValue方法返回的值。)
 *
 * @return the current thread's value of this thread-local(此线程本地的当前线程的值)
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

解读:首先看“set”方法,它先获取当前线程,然后获取线程的ThreadLocalMap对象。校验对象是为空,不为空时set,否则创建一个map对象。
关注一下第2行“getMap”,ThreadLocalMap是当前线程Thread中获取的,来源于Thread中⼀个叫threadLocals的变量。

/**
 * Get the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.
 * (获取与 ThreadLocal 关联的map。在InheritableThreadLocal中重写)
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. (与此线程有关的 ThreadLocal 值。该map由 ThreadLocal 类维护)*/
ThreadLocal.ThreadLocalMap threadLocals = null;

看到这里,我们应该明白ThreadLocal数据隔离原理了,每个线程Thread都维护了自己的threadLocals变量,这个是独立的,别人访问不到的,从而实现了隔离。

3、ThreadLocalMap底层结构

ThreadLocalMap是一个定制化的Hashmap。但是没有链表。

  在插⼊过程中,根据ThreadLocal对象的hash值,定位到table中的位置i。如果当前位置是空的,就初始化⼀个Entry对象放在位置i上;如果位置i不为空,如果这个Entry对象的key和要设置的key相等,那么就刷新Entry中的value;如果位置i的不为空,而且key不等于entry,那就找下⼀个空位置,直到为空为⽌。这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key⼀致,如果不⼀致,就判断下⼀个位置,在冲突严重的情况下,效率会比较低。

4、ThreadLocal的实例以及其值存放在栈还是堆?

在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
  所以,是存放在栈上咯?
  然而并不是,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过⼀些技巧将可⻅性修改成了线程可见。

5、如果我想共享线程的ThreadLocal数据怎么办?

使⽤ InheritableThreadLocal 可以实现多个线程访问ThreadLocal的值,我们在主线程中创建⼀个 InheritableThreadLocal 的实例,然后在⼦线程中得到这个 InheritableThreadLocal 实例设置的值。

四、使用场景

最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

// 数据库连接
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}
// 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;  
}

五、内存泄漏问题

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
  ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
  建议回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用try-finally块进行回收。

相关文章