java源码学习---ThreadLocal

不想你离开。 提交于 2020-02-26 05:08:52

ThreadLocal 是一个线程安全副本,用于储存仅允许当前线程能访问/修改的值,不知从何时起看到了”线程安全“这种字眼就会不自觉想到性能问题,但是ThreadLocal是实现线程安全的另外一种方案"空间换时间"。

先看2个小Demo

使用ThreadLocal

public class ThreadLoacalTest{

    // 定义线程安全副本
    private final static ThreadLocal<Integer> THREAD_NUMBER = new ThreadLocal<Integer>();

    // 继承Thread重写run()
    static class ThreadTest extends Thread{
        @Override
        public void run() {
            for (int i=0; i < 3 ;i++){
                // 如果THREAD_NUMBER null,赋值0,否则+1
                THREAD_NUMBER.set(THREAD_NUMBER.get() == null ? 0 : THREAD_NUMBER.get() + 1);
                // 打印信息
                System.out.println(Thread.currentThread().getName() + ":" + THREAD_NUMBER.get());
            }
            THREAD_NUMBER.remove();
        }
    }

    public static void main(String[] args) {
        ThreadTest t1 = new ThreadTest();
        ThreadTest t2 = new ThreadTest();
        ThreadTest t3 = new ThreadTest();
        t1.start();
        t2.start();
        t3.start();
    }
}

不使用ThreadLocal

public class Test {

    // 定义普通常量
    public static Integer THREAD_NUMBER = 0;

    // 继承Thread重写run()
    static class ThreadTest extends Thread{
        @Override
        public void run() {
            for (int i=0; i < 3 ;i++){
                // 打印信息
                System.out.println(Thread.currentThread().getName() + ":" + THREAD_NUMBER++);
            }
        }
    }

    public static void main(String[] args) {
        ThreadTest t1 = new ThreadTest();
        ThreadTest t2 = new ThreadTest();
        ThreadTest t3 = new ThreadTest();
        t1.start();
        t2.start();
        t3.start();
    }
}

从上面的打印结果可以看出,使用了ThreadLocal的常量不会与其他线程共享,而没有使用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);
}

1.先获取当前线程对象

2.通过当前线程对象获取一个ThreadLocalMap 实例(以下简称map)

3.如果map不为null 则直接向map插入数据

4.如果为null则调用createMap()创建一个map并且向map插入数据

该map以当前对象作为key,这样我们就只需要关注value而不用维护key

getMap()

可以看到这个threadLocals属性是Thread的,但是类是属于ThreadLocal的一个内部类ThreadLocalMap

因为这个map对象属于Thread的实例,每个Thread都是特有的map,所以能提供整个线程使用且不与其他线程共享

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

createMap()

2个参数,一个是key(当前ThreadLocal实例),一个是value(我们维护的对象)

下面代码可以看出值都是保存在一个Entry对象的数组里面,以及一些初始化的工作

Entry又是ThreadLocal.ThreadLocalMap 的一个内部类 ThreadLocal.ThreadLocalMap.Entry

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap.set()

在这里向Entry[]添加元素,计算出下标,如果该下标位置没有元素,则直接插入元素。如果有元素,则遍历数组直到没有元素的下标位置才停下进行存储,如果遇到相同key则更新元素并且遇到key为null的Entry时,会删除元素。存储完之后判断当前数组容量是否需要扩容,如果进行了扩容,所有元素都会重新存储一遍

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) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

接下来看看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();
}

我们获取ThreadLocal维护的变量都是直接通过ThreadLocal的get()获取

1.获取当前线程对象

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

3.通过当前对象向map获取值(Entry对象),如果map与Entry对象都不为null则直接返回存储的值

4.如果值为null,则调用setInitialValue()初始化键值对并返回null

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()初始化值,然后保存到map并且返回初始化的值

(判断是否需要创建map这个步骤实在不想写了,ThreadLocal里面太多这样的操作了)

protected T initialValue() {
    return null;
}

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

首先是计算出key对应值储存的下标,当元素不为null且key相等,则返回对应的值,如果下标与key不对应,则遍历数组查询,整个数组都不存在该key,则返回null,如果遇到key为null的Entry时,会删除元素

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

以上源码学习总结:

1.ThreadLocal原理是该类主要操作一个Thread类的 ThreadLocalMap threadLocals属性,该属性是属于每个Thread实例特有的,所以能提供整个线程使用且不与其他线程共享

2.ThreadLocalMap 的底层其实是一个素组,当计算到的存储下标已存在元素,则循环判断是否为null(能否存储),如果存在相同key则更新元素

3.get()、set()操作的时候如果碰到了key为null的情况都会删除key为null的 Entry 对象

都说ThreadLocal是以"空间换时间",以上代码也证实了确实如此。但是有个问题,就是空间用得越多,就越容易OOM。所以Entry继承了WeakReference弱引用。

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

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

这里的key是弱引用,就是key指向的对象(ThreadLocal)如果没有其他强引用的情况下,下次GC的时候就会被回收,这保证了一定的垃圾回收效率,但是如果存在其他强引用情况下,GC并不会回收该对象,所以我们使用ThreadLocal的时候需要注意有没有被强引用

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!