多线程中的ThreadLocal

1.ThreadLocal概述

多线程的并发问题主要存在于多个线程对于同一个变量进行修改产生的数据不一致的问题,同一个变量指的值同一个对象的成员变量或者是同一个类的静态变量。之前我们常听过尽量不要使用静态变量,会引起并发问题,那么随着Spring框架的深入人心,单例中的成员变量也出现了多线程并发问题。Struts2接受参数采用成员变量自动封装,为此在Spring的配置采用多例模式,而SpringMVC将Spring的容器化发挥到极致,将接受的参数放到了注解和方法的参数中,从而避免了单例出现的线程问题。今天,我们讨论的是JDK从1.2就出现的一个并发工具类ThreadLocal,他除了加锁这种同步方式之外的另一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。我们先看一下官方是怎么解释这个变量的?

大致意思是:此类提供了局部变量表。这些变量与普通变量不同不同之处是,每一个通过get或者set方法访问一个线程都是他自己的,将变量的副本独立初始化。ThreadLocal实例通常作用于希望将状态与线程关联的类中的私有静态字段(例如,用户ID或交易ID)。

只要线程是活动的并且可以访问{@code ThreadLocal}实例, 每个线程都会对其线程局部变量的副本保留隐式引用。 线程消失后,其线程本地实例的所有副本都将进行垃圾回收(除非存在对这些副本的其他引用)。也就是说,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。而每个线程的副本全部放到ThreadLocalMap中。

2. ThreadLocal简单实用

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Double> threadLocal = new ThreadLocal();
        private Double variable;

        @Override
        public void run() {
            threadLocal.set(Math.floor(Math.random() * 100D));
            variable = Math.floor(Math.random() * 100D);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }

            System.out.println("ThreadValue==>"+threadLocal.get());
            System.out.println("Variable==>"+variable);
        }
    }
    
    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }

}

通过上面的例子,我们发现将Double放入ThreadLocal中,不会出现多线程并发问题,而成员变量variable却发生了多线程并发问题。

3.ThreadLocal的内部原理

通过源码我们发现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.
  * 
  * 返回此线程局部变量的当前线程副本中的值。
  * 如果该变量没有当前线程的值,则首先将其初始化为调用{@link #initialValue}方法返回的值。
  * @return the current thread's value of this thread-local
  */
public T get() { }

/**
  * 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.
  *
  * 将此线程局部变量的当前线程副本设置为指定值。
  * 大多数子类将不需要重写此方法,而仅依靠{@link #initialValue}方法来设置线程局部变量的值。
  *
  * @param value the value to be stored in the current thread's copy of
  *              this thread-local.
  *              要存储在此本地线程的当前线程副本中的值。
  */
public void set(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.
  * 删除此线程局部变量的当前线程值。
  * 如果此线程局部变量随后被当前线程{@linkplain #get read}调用,
  * 则其值将通过调用其{@link #initialValue}方法来重新初始化,
  * 除非当前值是在此期间被设置{@linkplain #set set}。
  * 这可能会导致在当前线程中多次调用{@code initialValue}方法。
  * @since 1.5
  */
public void 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}.
  * 返回此线程局部变量的当前线程的“初始值”。
  * 除非线程先前调用了{@link #set}方法,
  * 否则线程第一次使用{@link #get}方法访问该变量时将调用此方法,
  * 在这种情况下,{@ code initialValue}方法将不会为线程被调用。
  * 通常,每个线程最多调用一次此方法,
  * 但是在随后调用{@link #remove}之后再调用{@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.
  * 此实现仅返回{@code null};如果程序员希望线程局部变量的初始值不是{@code null},
  * 则必须将{@code ThreadLocal}子类化,并重写此方法。通常,将使用匿名内部类。
  *
  * @return the initial value for this thread-local
  */
protected T initialValue(){ }

3.1 get方法

 public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //通过当前线程获取ThreadLocalMap
    //Thread类中包含一个ThreadLocalMap的成员变量
    ThreadLocalMap map = getMap(t);
    //如果不为空,则通过ThreadLocalMap中获取对应value值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果为空,需要初始化值
    return setInitialValue();
}
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;
}

首先是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。 如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。

在setInitialValue方法中,首先执行了initialValue方法(我们上面提到的最后一个方法),接着通过当前线程获取ThreadLocalMap,如果不存在则创建。创建的代码很简单,只是通过ThreadLocal对象和设置的Value值创建ThreadLocalMap对象。

3.2 set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

这个方法和setInitialValue方法的业务逻辑基本相同,只不过setInitialValue调用了initialValue()的钩子方法。这里代码简单,我们就不做过多解释。

3.3 remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

这个方法是从jdk1.5才出现的。处理逻辑也很很简单。通过当前线程获取到ThreadLocalMap对象,然后移除此ThreadLocal。

3.4 initialValue方法

protected T initialValue() {
    return null;
}

是不是感觉简单了,什么也没有处理,直接返回一个null,那么何必如此设计呢?当我们发现他的修饰符就会发现,他应该是一个钩子方法,主要用于提供子类实现的。追溯到源码中我们发现,Supplier的影子,这就是和jdk8的lamda表达式关联上了。

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

4. 总结

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。之所以这里是一个map,是因为通过线程会存在多个类中定义ThreadLocal的成员变量。初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals; 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

5. ThreadLocalMap引发的内存泄漏

ThreadLocal属于一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量。下面我们来看看ThreadLocalMap这个类的一个entry:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object val
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

public WeakReference(T referent) {
    super(referent); //referent:ThreadLocal的引用
}

//Reference构造方法     
Reference(T referent) {
    this(referent, null);//referent:ThreadLocal的引用
}

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

  考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

  总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

发布了88 篇原创文章 · 获赞 97 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/oYinHeZhiGuang/article/details/104739906