一起读源码之ThreadLocal

关于ThreadLocal

Thisclassprovides thread-local variables. These variables differ from their normalcounterparts in that each thread that accesses one (via its {@code get} or{@code set} method) has its own, independently initialized copy of thevariable. {@code ThreadLocal}instances are typically private static fields in classes that wish to associatestate with a thread (e.g.,a userID or Transaction ID)

       翻译过来的大概意思就是这个类提供“thread-local”变量,这些变量与线程的局部变量不同,每个线程都保存一份该变量的副本,可以通过get或者set方法访问。如果开发者希望将类的某个静态变量(userID或者transactionID)与线程状态关联,则可以考虑使用ThreadLocal

总结ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,也可以说是每个线程持有一个对象的实例。

ThreadLocal应用场景

ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

ThreadLocal整体结构

线程变量存放在每个线程的threadLocals属性中,由每个线程自己来维护属于自己的线程变量,而对线程变量的具体操作,则由ThreadLocal类来提供方法进行操作,所以ThreadLocal类更像是一个工具类。

线程变量的初始化

第一种,通过ThreadLocalset方法,代码如下:

publicvoid set(T value) {
   
Thread t = Thread.currentThread();
   
ThreadLocalMap map=getMap(t);
   
if (map != null)
        map.set(this,value);
   
else
        createMap(t,value);
}

参数是一个泛型的参数,所以可以把任意类型的变量作为线程变量。

第二种,是延迟初始化的方式,只有在调用get方法的时候才会进行初始化的操作,这种方式需要继承ThreadLocal,在子类中覆盖initialValue这个方法,initialValue方法默认实现是这样的:

protectedT initialValue(){
   
return null;
}

所以需要我们在子类中返回一个我们定义的线程变量。

线程变量的存储结构

线程变量就存储在一个Entry类型的数组中,那么存储的位置是怎么决定的呢?

线程变量的定位:哈希映射

ThreadLocal对象计算hash值,意味着使用同一个ThreadLocal对象创建的线程变量会被互相覆盖,不同的ThreadLocal对象的线程变量会被存放到不同的位置,但是创建太多的ThreadLocal对象会导致冲突触发扩容

哈希值的计算:位与运算

看下源码的计算过程:

private static final intINITIAL_CAPACITY= 16;//Entry数组的初始大小,必须为2N次方

private static final intHASH_INCREMENT= 0x61c88647;//16进制的神奇数字

private static AtomicIntegernextHashCode=
   
new AtomicInteger();//起始为0

private static intnextHashCode() {
   
return nextHashCode.getAndAdd(HASH_INCREMENT);//每次增加0x61c88647
}

private final intthreadLocalHashCode =nextHashCode();//创建ThreadLocal对象时初始化

inti =firstKey.threadLocalHashCode & (INITIAL_CAPACITY- 1);//ThreadLocal对象与数组的长度减1进行二进制的位与运算,得出存放位置的下标


1Entry[]table 的大小必须是2N次方,那 len-1 的二进制表示就是低位连续的N1,那 key.threadLocalHashCode& (len-1) 的值就是 threadLocalHashCode 的低N,这样就能均匀的产生均匀的分布

2、其中 1640531527就是 0x61c88647的十进制表示,使用这个数字的目的也是为了产生均匀的分布,至于这个数为什么这么神奇,这就涉及到一些数学知识了,这个数与黄金比例、斐波那契散列法有些关系


Entry的弱引用ThreadLocal

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

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

Entry弱引用的是对象ThreadLocal,也就是key值,所以每次执行GC后,Entry里的这个k变量引用的对象ThreadLocal如果没有外部变量引用的话都会被回收,效果就是在新的ThreadLocal执行set方法的时候,遍历map中的table时可能会发现好多Entry.get()为空的Entry,即key为空。这说明这条记录已经可以被删掉了,这个时候就可以用新的Entry替换掉旧的。所以,在一些操作时,如set等,我们都会对其进行一个判断,判断其是否已经被GC回收。如果ThreadLocalMap知道Entry里的keyThreadLocal对象)已经被回收,那么它对应的值也就没有用处了。然后调用cleanSomeSlots()来清除相关的值,来保证ThreadLocalMap总是保持尽可能的小。

Entry数组的扩容

如果table中的元素数量达到阈值threshold3/4,会进行扩容操作。源码如下:

privatevoid setThreshold(int len) {
   
threshold = len * 2 / 3;//阈值为数组长度的2/3
}

if(size >= threshold - threshold /4)
   
resize();//数组长度达到阈值threshold3/4,就扩容

privatevoid resize() {
   
Entry[] oldTab= table;
   
int oldLen =oldTab.length;
   
int newLen =oldLen* 2;
   
Entry[] newTab= new Entry[newLen];
   
int count = 0;

for (int j = 0; j <oldLen; ++j) {
   
Entry e = oldTab[j];
   
if (e != null) {
        ThreadLocal k =e.get();//获取ThreadLocal对象的引用
       
if (k == null) {
            e.value = null; // Help theGC
       
} else {

  //数组有元素,重新hash
           
int h =k.threadLocalHashCode & (newLen - 1);
           
while (newTab[h] != null)//冲突了
               
h = nextIndex(h,newLen);//开放定址法
           
newTab[h] = e;
           
count++;
        }
    }
}

  setThreshold(newLen);//设置阈值
   
size = count;
    table = newTab;
}

开放定址法:相当于逐个探测存放地址的表,直到查找到一个空单元,把散列地址存放在该空单元。

线程变量的设置ThreadLocal.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);
}

void createMap(Thread t, TfirstValue) {
   
t.threadLocals = newThreadLocalMap(this,firstValue);
}

ThreadLocalMap(ThreadLocalfirstKey, ObjectfirstValue) {
   
table = new Entry[INITIAL_CAPACITY];
   
inti =firstKey.threadLocalHashCode & (INITIAL_CAPACITY- 1);
   
table[i] = new Entry(firstKey,firstValue);
   
size = 1;
    setThreshold(INITIAL_CAPACITY);
}

线程变量的设置ThreadLocalMap.set

privatevoid set(ThreadLocal key,Object value){
   
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)])

        ThreadLocalk =e.get();
       
if (k == key) {{//如果当前位置已经有了元素,并且是当前这个ThreadLocal对象的,则覆盖
           
e.value =value;
           
return;
        }
       
if (k == null) {//因为是弱引用,说明已经被回收了,调用replaceStaleEntry()方法接着循环寻找相同的key,如果存在,直接替换旧值。如果不存在,则在当前位置上重新创建新的Entry.
           
replaceStaleEntry(key,value,i);
           
return;
        }
    }

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

线程变量获取ThreadLocal.get

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);//由于位置冲突,key对应的值存储的位置并不在i位置上,

}

//Entry null ThreadLocal不对应则向下一个位置遍历,直到Entry ThreadLocal和当前 ThreadLocal对应或遍历结束

private Entry getEntryAfterMiss(ThreadLocal key,inti, 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); );//清理过期的Entry,也就是被key被回收的
       
else
            i = nextIndex(i,len);
       
e = tab[i];
   
}
    return null;
}

从该方法中可知,在Map中拿到的Entry或引用的key可能为空。可是,理论上在线程上下文中肯定是可以获取到的,这是为什么呢。仔细分析后,发现ThreadLocal中的Entry是继承了WeakReference弱引用的,这代表JVM在发起垃圾回收时,可能会回收掉该Entry引用的key,threadlocal对象。如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必可能会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:

ThreadRef -> Thread -> ThreaLocalMap-> Entry -> value

于是,可能会造成内存泄漏,因此在getEntry方法中会调用getEntryAfterMiss的方法,主要用于擦除Entry中的key为空所对应的Entry,让垃圾收集器回收,避免内存泄漏。类似的,在ThreadLocalMap中添加Entry,也有类似的处理。将keynull的这些Entry都删除,防止内存泄露。所以很多情况下需要使用者手动调用ThreadLocalremove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成privatestatic的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entryvalue值。


发布了19 篇原创文章 · 获赞 16 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/matt8/article/details/75670512