扒一扒ThreadLocal原理及应用

  • Post author:
  • Post category:其他




先总述,后分析

深挖过ThreadLocal之后,一句话概括:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。所以ThreadLocal的应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

数据隔离的秘诀其实是这样的,Thread有个TheadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。

接下来采用jdk1.8源码进行深挖一下TheadLocal和TheadLocalMap。



1、ThreadLocal是什么?

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。



2、ThreadLocal原理?

既然ThreadLocal则用于线程间的数据隔离,每个线程都可以独立的操作自己独立的变量副本而不会影响别的线程中的变量。先用一个简单的代码演示一下结论:

public class threadLocalDemo {
    static ThreadLocal<Person> threadLocal = new ThreadLocal<Person>();
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
        },"t1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadLocal.set(new Person());
        },"t2").start();
    }
    static class Person {
        String name = "yangguo";
    }

运行结果:null

一个简单的ThreadLocal演示,开启两个线程t1、t2,线程t2对threadLocal进行了set,但是并没有改变线程t1本地的threadlocal变量值。

扒扒底层源码看看到底做了什么?进入threadLocal.set()方法,源码如下:

 public void set(T value) {
        Thread t = Thread.currentThread();   //获取当前线程
        ThreadLocalMap map = getMap(t);      //根据当前线程获取ThreadLocalMap,getMap()源码接在下面
        if (map != null) 
            map.set(this, value);           //给ThreadLocalMap赋值(ThreadLocal,value)
        else
            createMap(t, value);
    }

---------------**getMap(t)源码**-----------------------
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;       //从当前Thread类中获取到ThreadLocalMap
    }
------------**map.set源码:将map中的key,value组装成一个Entry,而在Entry中继承了WeakReference弱引用**------
 private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            **tab[i] = new Entry(key, value);**
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
------------------------**Entry源码:继承了弱引用,**--------------------------
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

Thread内部变量ThreadLocalMap

扒完源码之后整理一下threadLocal中set方法的过程。

  1. 获取到当前线程
  2. 从当前线程中获取到自己内部的ThreadLocalMap,别的线程无法访问这个map
  3. 往ThreadLocalMap值塞值(ThreadLocal,value)
  4. 塞值的过程中将key和value组装成了一个Entry,继承了弱引用。防止ThreadLocal引用的对象内存泄漏。

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

ThreadLocal的内部结构图如下:

ThreadLocal结构内部



3、ThreadLocal类的核心方法

ThreadLocal类核心方法set、get、initialValue、withInitial、setInitialValue、remove

/**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

    /**
     * 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.
     *
     * @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();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    /**
     * 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.
     *
     * @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);
    }

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
  1. get()方法用于获取当前线程的副本变量值。
  2. set()方法用于保存当前线程的副本变量值。
  3. initialValue()为当前线程初始副本变量值。
  4. remove()方法移除当前线程的副本变量值。



4、 ThreadLocal业务场景能干嘛?

spring中@Transacion注解中使用到。

Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了当中的getConnection()方法,或者说该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。事务操作时,如果插入时出现了异常,依然可以拿到原来的连接进行回滚操作。

为什么要放在ThreadLocal里面呢?由于Spring在AOP后并不能向应用程序传递參数。应用程序的每一个业务代码是事先定义好的,Spring并不会要求在业务代码的入口參数中必须编写Connection的入口參数。此时Spring选择了ThreadLocal,通过它保证连接对象始终在线程内部,不论什么时候都能拿到,此时Spring很清楚什么时候回收这个连接,也就是很清楚什么时候从ThreadLocal中删除这个元素(在5.2节中会具体解说)。



5、ThreadLocalMap的问题

面试相关问题:



5.1为什么ThreadLocalMap 中的key是弱引用? 内存泄漏的第一种场景

ThreadLocal为什么要用弱引用

上图中,若是用强引用,即使t1=null,但key的引用仍然指向ThreadLocal对象,GC时只会把t1给回收掉,ThreadLocal由于被引用了不会被回收,所以使用强引用了之后会导致内存泄漏。



5.2 为什么threadLocal用完必须要进行remove? 内存泄漏的第二种场景

由于ThreadLocalMap的**key是弱引用,而Value是强引用。**这就导致了一个问题,

ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。


既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,

使用完ThreadLocal之后,记得调用remove方法

ThreadLocal<Person> threadLocal = new ThreadLocal<Person>();
try {
     threadLocal.set(new Person());
} finally {
     threadLocal.remove();   //threadlocal用完必须要进行remove,不然会导致内存泄漏。
}



总结

  • 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal
  • ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
  • 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。



版权声明:本文为u013025649原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。