Java并发:ThreadLocal详解

前言

最近看多线程的时候看到ThreadLocal这个类,就baidu查了一下。在最开始查到的文章对这个类最多的说明就是ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。现在请忘掉这些说明,因为他彻底地错了!!!看了这些blog后会让你更加混乱,因为他们的对ThreadLocal的描述根本无法推出如何解决多线程并发。这让你看了之后根本搞不清楚ThreadLocal到底是做什么的。

我们看下这些blog中对于ThreadLocal性质的描述:当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本.

看到没ThreadLocal会为每个使用该变量的线程提供独立的副本!!!也就是说每个线程间的操作都不会影响到其他线程,相当于建立了一个线程内部声明周期的局部变量,不会影响其他线程也根本就无所谓的同步了。所以,他们的逻辑都是混乱的,写这些就是误人子弟。

我看了这些之后,就一直不明白ThreadLocal的具体作用是那些,直到我看到winwill2012了一篇博客才明白。这篇博客也是参考了该文章,文章地址在这里[Java并发包学习七]解密ThreadLocal
这篇博客中队ThreadLocal作用的解释是:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocal方法介绍

threadLocal只有4个基本方法,分别是void set(T value),T get(),void remove()以及T initialValue()。下面分别说明:

initialValue()方法

此方法用来返回当前线程在ThreadLocal中的初始值,该函数在调用get()的时候会第一次调用,但是如果一开始就调用了set(),则该函数不会被调用。通常该函数只会被调用一次,除非手动调用了remove()之后又调用get(),这种情况下get()中还是会调用initialValue()。具体实现如下:

protected T  initialValue() {
     return null;
}

该方法是protected类型的,很显然是建议在子类重载该方法的,所以通常该方法都会以匿名内部类的形式被重载,以指定初始值,例如:

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

get()方法

该方法用户获取与当前线程关联的ThreadLocal值,方法声明如下

public T get()

set()方法

该方法用于设置与当前线程关联的ThreadLocal值,方法声明如下

public void set(T value)

remove()方法

该方法用于将当前线程的ThreadLocal绑定的值删除,方法声明如下

public void remove()

示例

用一段代码来演示ThreadLocal的用法

public class Test {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for(int i = 1 ;i<=5;i++){
            new Thread(new LocalThread(i)).start();
        }
    }

    static class LocalThread implements  Runnable{
        private int index;

        public LocalThread(int index) {
            this.index = index;
        }

        public void run() {
              System.out.println("线程" + index + "的初始value:" + value.get());
                for (int i = 0; i < 10; i++) {
                    value.set(value.get() + i);
                }
                System.out.println("线程" + index + "的累加value:" + value.get());
        }
    }

}   

运行结果:

线程1的初始value:0
线程5的初始value:0
线程1的累加value:45
线程2的初始value:0
线程2的累加value:45
线程3的初始value:0
线程4的初始value:0
线程4的累加value:45
线程3的累加value:45
线程5的累加value:45

可以看到各个线程中的value都是独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

源码解析

源码摘自jdk8,我们先看get()源码:

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

可以看到get()方法调用时,首先会调用getMap()方法获取一个ThreadLocalMap对象,我们先来看getMap()的源码:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

从源码上可以看到是获取的当前线程中的threadLocals成员对象,

ThreadLocal.ThreadLocalMap threadLocals = null;

然后我们再看ThreadLocal get()方法中的setInitailValue()方法

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

createMap()方法源码:

 void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

现在我们就大概的知道了get方法获取值得流程:
1. 首先Thread.currentThread()获取当前线程
2. 根据当前线程,获取线程中的一个Map,如果map为空则转4
3. 如果这个map不为空的话,则在map中获取以当前ThreadLocal引用作为key相对应的value e,如果e不为空,返回e.value,若为null则转4
4. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

所以,可以总结一下ThreadLocal的设计思路:
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。

为什么要设计的这么麻烦而不是直接在ThreadLocal中维护一个Map,然后以线程ID作为Map的key。查阅了一下资料,这样设计的主要有以下几点优势:
* 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲测)
* 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

内存泄漏问题

网上流传使用ThreadLocal有可能会导致内存泄漏,具体原因是什么呢?真的是这样吗?

首先ThreadLocal存储数据都是存在一个ThreadLocalMap类型的对象中,这个类是ThreadLocal的一个内部类,我们先看一下ThreadLocalMap的相关源码:

static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ...
        ...
}

可以看到ThreadLocalMap是使用ThreadLocal的弱引用作为Key的

引用关系图

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

然后网上就传言,ThreadLocal会引发内存泄露,他们的理由是这样的:

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

但是实际上ThreadLocalMap的设计已经考虑到这种情况了。请看ThreadLocalMap的getEntry方法的源码:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss()源码:

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)
             expungeStaleEntry(i);
         else
             i = nextIndex(i, len);
         e = tab[i];
     }
     return null;
 }

expungeStaleEntry()源码:

private int expungeStaleEntry(int staleSlot) {
           Entry[] tab = table;
           int len = tab.length;

           // expunge entry at staleSlot
           tab[staleSlot].value = null;
           tab[staleSlot] = null;
           size--;

           // Rehash until we encounter null
           Entry e;
           int i;
           for (i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
               ThreadLocal<?> k = e.get();
               if (k == null) {
                   e.value = null;
                   tab[i] = null;
                   size--;
               } else {
                   int h = k.threadLocalHashCode & (len - 1);
                   if (h != i) {
                       tab[i] = null;

                       // Unlike Knuth 6.4 Algorithm R, we must scan until
                       // null because multiple entries could have been stale.
                       while (tab[h] != null)
                           h = nextIndex(h, len);
                       tab[h] = e;
                   }
               }
           }
           return i;
       }

整理一下ThreadLocalMap的getEntry函数的流程:
1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
2. 如果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它,防止内存泄露。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值