Java并发之ThreadLocal

    在Java中,对象是线程共享的,当我们想让线程独自拥有专属于自己的变量时,可以使用ThreadLocal类。

    ThreadLocal类提供了线程局部(Thread-Locak)变量。这些变量不同于他们的普通对应物,因为访问某个变量的每个线程都有自己的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本。所以,每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

    从线程的角度看,目标变量就是线程的本地变量,这也是Local所代表的含义。 

一、使用方法

      ThreadLocal只有4个方法,如下:
  • public T get():返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用 initalValue() 方法返回的值。
  • protected T initialValue():返回此线程局部变量的当前线程的”初始值“。线程第一次使用 get() 方法会调用此方法。但如果线程之前调用了 set() 方法,则不会对该线程再调用 initialValue() 方法。通常,此方法对每个线程最多调用一次,但如果在调用 get() 后又使用 remove() 方法,则可能再次调用此方法。  
  • public void remove():移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取,且这期间线程没有设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue() 方法。
  • public void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。

二、ThreadLocal实现原理

    其实在ThreadLocal类中有一个静态内部类 ThreadLocalMap ,用键值对的形式储存每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而Value对应线程的变量副本。每个线程对象t都拥有自己的ThreadLocalMap字段。线程t的ThreadLocalMap中存有多个ThreadLocal-Value键值对。
    ThreadLocal.ThreadLocalMap threadLocals = null; //Thread对象中含有字段 ThreadLocalMap

    get方法

    get方法返回当前线程中,存有的ThreadLocal局部变量。如果当前线程中没有存入该值,将调用initialValue方法返回的值。源码如下:

    public T get() {
        Thread t = Thread.currentThread();			//当前线程
        ThreadLocalMap map = getMap(t);				//获取该线程的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);	//获取map中键为该ThreadLocal的entry
            if (e != null) {					//若有值 则返回该值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();				//若没有该值 则调用initialValue方法
    }

    getMap(Thread t) 方法用来获取线程中的ThreadLocalMap:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;		//获取线程t的局部变量表
    }

    setInitialValue()方法将获取初始值,并往线程的局部变量表中填入threadLocal-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;
    }

    set方法

    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);		
    }

   remove方法

    remove方法将清除线程局部变量表中的该threadLocal键值对。

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

以上几个方法的核心都是对线程的ThreadLocalMap进行操作,下面我们将查看ThreadLocalMap的源码。

三、ThreadLocalMap实现原理

static class ThreadLocalMap {
  //map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
 static class Entry extends WeakReference<ThreadLocal<?>> {
           Object value;
           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
   }
    /**
     * 初始化容量为16,以为对其扩充也必须是2的指数 
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
     */
    private Entry[] table;


    ///....其他的方法和操作都和map的类似
}

    总之,为不同线程创建不同的ThreadLocalMap,用线程本身为区分点,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独new了一个对象。

四、内存泄漏问题

    网上大家讨论说ThreadLocal会导致内存泄漏,原因如下:

扫描二维码关注公众号,回复: 1484532 查看本文章
  • 首先ThreadLocal实例被线程的ThreadLocalMap实例持有,也可以看成被线程持有。
  • 如果应用使用了线程池,那么之前的线程实例处理完之后出于复用的目的依然存活
  • 所以,ThreadLocal设定的值被持有,导致内存泄露。
    上面的逻辑是清晰的,可是ThreadLocal并不会产生内存泄露,因为ThreadLocalMap做选择key的时候,并不是直接选择ThreadLocal实例,而是ThreadLocalMap实例的弱引用。



    

猜你喜欢

转载自blog.csdn.net/u010771890/article/details/75044035