A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

一、结构:
(这里没有画图,将就到看哈)

(1).ThreadLocal跟很多集合一样是一个泛型类

(2).Thread包含一个ThreadLocal.ThreadLocalMap的成员变量(组合关系),也就是每个线程维护的独立副本的变量。

(3).ThreadLocal包含一个内部类ThreadLocalMap,ThreadLocalMap又包含一个内部类Entry,Entry最终就是给当前线程存储变量数据的。Entry的key存储ThreadLocal的弱引用,value指向Object对象值。

二、示例:
public static void main(String[] args) {
    final ThreadLocal<Integer> local = new ThreadLocal<Integer>();
    new Thread() {
        @Override
        public void run() {
            local.set(10);
            System.out.println("ThreadName: " + Thread.currentThread().getName() + "; value: " + local.get());
        }
    }.start();
    new Thread() {
        @Override
        public void run() {
            local.set(20);
            System.out.println("ThreadName: " + Thread.currentThread().getName() + "; value: " + local.get());
        }
    }.start();
}
当前执行的线程中,Entry的key存储的就是local(ThreadLocal实例对象),而value就是当前线程执行set方法时传入的值,线程在执行get方法时最终就会拿到自己set的值,从而做到线程安全。

三、核心API理解
3.1 set方法
在讲set方法之前,先来看下ThreadLocal的构造方法

public 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);
}
显而易见,这个方法就做了下面几件事:

1.首先获取当前线程对象

2.通过当前线程对象获取ThreadLocalMap对象

3.如果map==null,就调用createMap方法。

4.如果map!=mull,调用set方法



先来看看简单的getMap方法:

ThreadLocalMap getMap(Thread t) {
   return t.threadLocals;
}
该方法直接调用当前执行的线程的threadLocals成员变量(即:ThreadLocal.ThreadLocalMap类型变量),但是Thread类并没有对该变量初始化,也就是说线程在第一次调用set方法时,map是为空的。

但是博主有个疑问:在测试过程中发现,主线程在启动初始化的过程中会有各种值被set方法传入到Entry中,而且获取到的map并不为空,而当创建的子线程第一次执行到set方法时map才等于空。望知道的大神门可以给我解释下啊!博主感激不尽。

接下来看看map==null的情况

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


createMap就是给Thread的成员变量threadLocals赋初始值,并传入当前ThreadLocal对象和value值

再看ThreadLocalMap的构造方法:



/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
     table = new Entry[INITIAL_CAPACITY];
     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
     table = new Entry(firstKey, firstValue);
     size = 1;
     setThreshold(INITIAL_CAPACITY);
}
1.创建了一个Entry数组,初始化大小为16。

2.根据firstKey来计算该变量存储在Entry数组变量table的下标位置。(具体的计算原理请参考:http://www.cnblogs.com/ilellen/p/4135266.html)

3.赋值到table中下标位置为i的地方。

4.size=1设置数组的大小。

5.设置阈值,即:当Entry数组table大于INITIAL_CAPACITY(16) * 2/3的时候就需要对table进行扩容。

到这里ThreadLocalMap构造方法初始化结束了。
接下来就是map!=null情况下调用ThreadLocalMap的set方法

        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;
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
1.首先根据方法传入的key进行hash计算数组下标的值,根据这个值获取该数组的位置的元素e,判断e是否为空

2.当元素e为空的时候,也就是没有出现hash冲突,直接创建Entry(key, value)赋值给tab

3.当元素e不为空的时候,即出现hash冲突,进入for循环,通过e.get()获取当前元素的ThreadLocal对象引用k。如果k等于key,也就是tab中已经存在这个值,直接将其覆盖重新赋值。如果k等于空,但是此时e不等于空,就意味着value不为空,这里出现了陈旧元素(即:因为key是ThreadLocal的弱引用,如果这个对象没有被其他地方引用,就有可能被jvmGC回收了,而value不是),所以这里要对进行remove操作,并且要重新整理Entry数组。而这个replaceStaleEntry方法就是做这件事的,replaceStaleEntry方法较为复杂难以理解,有兴趣的开发者可以自行查看源码。但有一点还是说,给方法里面调用了expungeStaleEntry方法,这个方法就做一件事就是删除陈旧的元素。

4.最后看下倒数第二和第三行代码,调用cleanSomeSlots方法,这个方法就是去找Entry数组中是否有陈旧元素,如果有则将其删除并返回true,然后调用rehash方法对Entry数组进行扩容,重新调整Entry数组;如果没有找到则继续判断数组大小是否超过阈值了,如果超过阈值,也调用rehash方法调整Entry数组;否则不做任何操作。

至此,set方法粗略的逻辑分析完毕,给大家整理下主要的逻辑图



3.2 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方法体前两行和set方法一样,接下来分2种情况:

1.map为空,直接调用setInitialValue方法

    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;
    }
setInitialValue方法体第一行调用initialValue方法(如下面代码所示),这个方法直接返回null赋值给value;接下来还是获取map,此时map是为空的,所以调用creatMap方法创建新的ThreadLocalMap对象。最后直接返回null。

protected T initialValue() {
    return null;
}
2.map不为空,通过map调用getEntry方法,传入的参数是当前ThreadLocal对象,getEntry方法如下:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table;
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
首先通过keyhash计算获取数组下标再拿到Entry元素e,判断e不等于null而且e元素的key等于当前传入的ThreadLocal对象,直接返回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;
    }
    return null;
}
这个getEntryAfterMiss方法个人认为做了三件事:

1.通过while循环解决hash冲突

2.判断是否有陈旧元素,有的话将其删除

3.拿到最终元素e返回

最后,get方法拿到元素e后,调用e.value获取value值返回。至此,get方法分析结束

3.3remove方法
先上源码:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
相当于执行ThreadLocalMap的remove方法:

        /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab;
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
for循环是为了解决hash冲突,解决hash冲突就是将i+1,如果e元素的key等于当前传入的ThreadLocal对象,那么就清空这个元素。结束

四、总结
set、get、remove方法体都在开头获取当前线程对象来作为操作的对象,跟其他任何线程都无关,从而做到了线程安全和变量的隔离

虽然使用起来很简单,但是我们还是要注意一些细节问题,比如用线程池使用ThreadLocal时,虽然线程使用完了,但是该线程如果没有没有调用remove方法,并且该线程在线程池继续处于活跃状态的话,会出现两个隐藏的致命问题:

1.没有调用remove方法意味着没有把使用过的线程本地变量删除,因为线程的成员变量threadLocals可能还存储着上一次使用的对象,下次再使用这个线程资源的话,就有可能出现线程安全的问题

2.根据1的情况就有可能引发内存泄漏,最终导致严重的后果。

所以切记,在使用ThreadLocal以后一定要记得手动调用remove方法。


0 个回复

您需要登录后才可以回帖 登录 | 加入黑马