Java并发编程之ThreadLocal(二)

213 阅读4分钟

前言

上一篇文章介绍了ThreadLocal的相关类结构和基本使用案例,这篇文章以这个案例为基础介绍下常用的相关方法。

  • ThreadLocal构造方法
  • set方法
  • get方法
  • remove()方法

ThreadLocal构造方法

public ThreadLocal() {
}

ThreadLocal只有一个无参构造方法,只是创建了对象没有做其他操作。

set方法

// java.lant.ThreadLocal
public void set(T value) {
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
// java.lant.ThreadLocal
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;  // 获取Thread类中的ThreadLocalMap类型属性
}
// java.lant.Thread
ThreadLocal.ThreadLocalMap threadLocals = null;

首先获取当前线程,然后根据当前线程去获取线程中存储的ThreadLocalMap,而Thread类中threadLocals默认是null,所以上面的if条件判断会有以下两种情况。

首次调用

首次调用set方法时threadLocalsnull,所以执行createMap方法进行初始化。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];  // 初始化table
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  // hash值计算索引
    table[i] = new Entry(firstKey, firstValue);  // 存储数据
    size = 1;
    setThreshold(INITIAL_CAPACITY);  // 设置阈值
}
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

createMap方法只做了一件事,就是创建一个ThreadLocalMap然后赋值给当前线程的threadLocals属性。

继续向下看ThreadLocalMap的构造方法,首先创建了一个Entry数组对table属性进行初始化,然后根据ThreadLocal对象的hash值来计算在数组中的索引,创建一个Entry对象来存储数据放入该索引位置。

size = 1设置的是当前table中存储的数据数量。

setThreshold方法设置table的阈值为数组长度的2/3,当达到该阈值时会触发扩容。

非首次调用

非首次调用时获取的ThreadLocalMap对象不为null,所以直接尝试存储数据。

private void 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)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {  // 找到了之前存的
            e.value = value;
            return;
        }
        if (k == null) {  // 过时的Entry,已经被移除
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

这个方法整体上分为两个步骤:for循环部分和for循环之后的处理。

第一步:根据keyhash值计算出索引位置,获取该位置的Entry,并取出存储的key

  • 如果刚好是要存储的key,那就直接用新的value覆盖掉旧数据;
  • 如果keynull,表明该Entry已经过期,就清除掉过期的Entry并替换为新的EntryreplaceStaleEntry方法后面会有文章单独介绍)。
  • 如果上面两个条件都不满足,就获取下一个索引位置的Entry一直重试。

第二步:如果不满足for循环的条件,说明该索引位置是空的,所以就可以创建一个Entry直接放进去,数组中元素数量size1。然后清除过期的Entry并判断数组中元素数量是否超过了阈值,如果超过了就进行扩容。

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方法来获取当前线程中存储的数据。首先获取线程本地存储的ThreadLocalMap对象,如果是null就调用setInitialValue方法进行初始化并把初始值返回。如果不为null就根据ThreadLocal对象去查找Entry,如果找到了就取出里面的值返回。

// 初始化
private T setInitialValue() {
    T value = initialValue();  // 获取初始值
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);  // 获取当前线程的ThreadLocalMap
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
// 初始值
protected T initialValue() {
    return null;
}

setInitialValue方法首先通过initialValue方法获取一个初始值,默认是null,然后获取当前线程的ThreadLocalMap对象,获取成功就把初始值保存进去,否则就创建一个新的ThreadLocalMap把初始值保存起来。这里的初始值可以通过子类重写的方式来返回想要的对象。

remove方法

 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);  // 移除操作
 }
 private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);  // 计算hash
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {  // 找到了
            e.clear();  // 清除key
            expungeStaleEntry(i);  // 清除陈旧的Entry
            return;
        }
    }
}
// 祖父类Reference
public void clear() {
    this.referent = null;
}

首先获取当前线程中存储的ThreadLocalMap变量,如果存在就尝试进行移除操作。

根据hash值找到当前key所在的索引位置,然后取出该位置的Entry,如果刚好就是我们要找的对象,就调用clear方法执行清除操作,然后清除掉陈旧的Entry。如果不是要找的对象,就获取下一个索引继续重试,直到找到该对象或者遍历完数组为止。

这里可以看到clear方法其实就是把referent(原来存的ThreadLocal对象,referent介绍)置为null,所以调用Entryget()方法时就会出现null的情况。expungeStaleEntry方法后面会单独介绍。