对Java ThreadLocal的理解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sunhongbing1024/article/details/81557529

一,ThreadLocal简介

描述:ThreadLocal,是Thread Local Variable(线程局部变量)的意思,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立的改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量一样。

看看JDK中的源码是怎么写的:
This class provides thread-local variables.
These variables differ from their normal counterparts in that each thread
that accesses one (via its {@code get} or {@code set} method) has its own,
independently initialized copy of the variable.
{@code ThreadLocal} instances are typically private static fields in classes
that wish to associate state with a thread (e.g., a user ID or Transaction ID).
  ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。

二,源码分析

最早期的ThreadLocal设计:每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
  ThreadLocal的设计思路:每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。
优势:
  这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高很大的性能
  当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

为什么不直接用线程id来作为ThreadLocalMap的key?
  这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
  而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分(int i = key.threadLocalHashCode & (len-1);)

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?
查看源码,可以看到:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
  但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。

扫描二维码关注公众号,回复: 3684396 查看本文章

get()

ThreadLocal的get()方法实现:

public T get() {
    // 取得当前线程
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 注意:这里用的是this
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

     第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是  this,而不是当前线程t。

  如果获取成功,则返回value值。

  如果map为空,则调用setInitialValue方法返回value。

getMap()

getMap()方法分析:

// 是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// java.lang.Thread类下, 实际上就是一个ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
static class ThreadLocalMap {
    // 继承了WeakReference,并且使用ThreadLocal作为键值。
    static class Entry extends WeakReference<ThreadLocal> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
}

通过上面的代码可以看出:

在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals,该变量实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类,可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。

setInitialValue()

方法分析:

private T setInitialValue() { 
    T value = initialValue();   // 该方法下方有分析
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 因为get的参数是this, 所以set的key也得是this
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
protected T initialValue() {
    return null;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

通过上面的代码可以看出:

     如果map不为空,就设置键值对,为空,再创建Map。

     至此,ThreadLocal是如何为每个线程创建变量的副本的呢:

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

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

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

initialValue()方法

该函数在调用get函数的时候会第一次调用,但是如果一开始就调用了set函数,则该函数不会被调用。
  通常该函数只会被调用一次,除非手动调用了remove函数之后又调用get函数,这种情况下,get函数中还是会调用initialValue函数。
  该函数是protected类型的,很显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重载,以指定初始值,比如:

public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return Integer.valueOf(1);
        }
    };
}

默认情况下,initialValue方法返回的是null。

 protected T initialValue() {
        return null;
    }

三,ThreadLocalMap及本身如何避免内存泄漏

ThreadLocalMap是使用ThreadLocal的弱引用作为Key的
  ThreadLocalMap和WeakHashMap实现有点类似,也是利用了WeakReference来和GC建立关联,因为ThreadLocal对象被线程对象引用,如果一个线程的生命周期比较长,那可能会出现内存泄露的问题,ThreadLocalMap借助弱引用巧妙的解决了这个问题,源码如下所示:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        //这里的Entry并不是一个链表,如果出现hash碰撞,会放到数组的下一个位置
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用: 

 如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。

实际是不会,该类已经处理了。

private Entry getEntry(ThreadLocal<?> key) {
    // set方法  int i = key.threadLocalHashCode & (len-1);
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        // 当前位置没有找到,可能Hash碰撞了
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            // 找到了直接返回
            return e;
        if (k == null)
            // 检测到有个ThreadLocal对象被回收了,这个时候去清理后面所有Key为null的Entry
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

从上面的代码可以得到:
ThreadLocalMap的getEntry函数的流程大概为:

首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
  但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

参考文章:

https://www.cnblogs.com/xzwblog/p/7227509.html

http://www.cnblogs.com/dolphin0520/p/3920407.html

猜你喜欢

转载自blog.csdn.net/sunhongbing1024/article/details/81557529