【Java并发编程】深入分析ThreadLocal(八)

时间:2020-12-29 17:37:08

   我们在使用一个类时,首先要知道它能做什么,然后再去深入分析它的工作原理。ThreadLocal如果从名字上来看像是“本地线程"的意思,其实ThreadLocal并不是一个线程,而是线程的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
在Java多线程面试题中,作者对ThreadLocal的回答是:
  ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。

官方对ThreadLocal的描述:

    该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

一、ThreadLocal源码分析

   1.1 属性

/**
* threadLocalHashCode用来查找Entry数组中的元素
*/
private final int threadLocalHashCode = nextHashCode();

/**
* 一个原子类,用来保证threadLocalHashCode值的安全性
* .
*/
private static AtomicInteger nextHashCode = new AtomicInteger();

/**
* 表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;

   1.2 内部类ThreadLocalMap

  ThreadLocalMap的实现原理跟HashMap差不多,内部有一个Entry数组,一个Entry通常至少包括Key,Value, 查找时通过key的Hash值和数组长度进行计算来得到Entry在数组中的位置,进而得到相应的value。但是比较特殊的是Entry继承了软引用WeakReference ,也就是Entry只能生存到下一次垃圾回收之前,并且它实际上真正弱引用的是key而不是value。 

 /**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

   1.2.1 为什么在ThreadLocalMap中弱引用ThreadLocal对象呢?

  使用弱引用的好处是能够减少内存使用。在set()、put()等待操作中都有出现这段代码ThreadLocal k = e.get(), 通常在弱引用使用中,我们都会对其进行一个判断,判断其是否已经被GC回收。如果ThreadLocalMap知道Entry里的key(ThreadLocal对象)已经被回收,那么它对应的值也就没有用处了。然后调用cleanSomeSlots()来清除相关的值,来保证ThreadLocalMap总是保持尽可能的小。 

   1.2.2 cleanSomeSlots()操作

 private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
 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;        }

   1.3 set()操作

  hash散列的键值数据在存储过程中可能会发生碰撞,大家知道HashMap存储的是一个Entry链,当hash发生冲突后,将新的Entry存放在链表最前端。但是ThreadLocalMap不一样,采用index+1作为重散列的hash值写入。另外有一点需要注意key出现null的原因是由于Entry的key是继承了软引用,在下一次GC时不管它有没有被引用都会被回收掉而Value没有被回收。当出现null时,会调用replaceStaleEntry()方法接着循环寻找相同的key,如果存在,直接替换旧值。如果不存在,则在当前位置上重新创建新的Entry.
//设置当前线程的线程局部变量的值。private void set(ThreadLocal key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//替换掉旧值
if (k == key) {
e.value = value;
return;
}
//和HashMap不一样,由于Entry key继承了软引用,会出现k是null的情况!所以会接着在replaceStaleEntry重新循环寻找相同的key
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
//调用cleanSomeSlots()对table进行清理,如果没有任何Entry被清理,并且表的size超过了阈值,就会调用rehash()方法
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

   1.4 get()操作

  get()是获得当前线程所对应的线程局部变量。ThreadLocal和HashMap一样也是通过hash计算Entry在table数组中的位置,再使用key拿到对应的value。如果value为null,它会调用setInitialValue返回值。
 public T get() {        Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
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); }
 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; }

  就不举例说明了,在网上找了几关于ThreadLocal的例子和应用场景,大家可以参考下。
http://www.cnblogs.com/dolphin0520/p/3920407.html
http://blog.csdn.net/JaCman/article/details/50458801



作者: 小毛驴,一个游戏人 
梦想:世界和平   
原文地址: http://blog.csdn.net/liulongling
若有错误之处,请多多谅解并欢迎批评指正。     
本博客中未标明转载的文章归作者 小毛驴所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。