Java并发之ThreadLocal

来源:互联网 发布:怎么读java源码 编辑:程序博客网 时间:2024/05/17 21:54

    在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会导致内存泄漏,原因如下:

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



    

原创粉丝点击