ThreadLocal解析

一、简介

首先我们需要知道Thread.currentThread()获取当前线程对象,同一个线程每次获取的都是同一个Thread对象。
ThreadLocal主要是用来将数据与线程绑定。
ThreadLocal其实主要思想就是将数据保存在当前线程对象,然后同ThreadLocal对象去操作保存的对象。主要方法包括设置值,取值,移除等操作。

二、类的结构

还有两个内部类的结构

三、主要方法

initialValue()

protected T initialValue() {
        return null;
    }

可以看到这是一个钩子方法,所以我们可以重写该方法,获得更好的实现效果。

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

当前线程已经绑定了ThreadLocalMap,则直接设置值调用ThreadLocalMap的set方法,也就是

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

int i = key.threadLocalHashCode & (len-1);

这里用到了神奇的数字0x61c88647,用它可以将值更优的放到2的幂大小的数组中,举个栗子可以将打印内容作为数组位置。

public static void main(String[] args) {
        int t = 0x61c88647;
        AtomicInteger atomicInteger = new AtomicInteger();
        int size = 1<<4;
        for (int i = 0; i < size; i++) {
            atomicInteger.getAndAdd(t);
            System.out.println(atomicInteger.get()&(size-1));
        }
    }

threadLocalHashCode这里是final修饰的所以直接使用该值不会修改。所以一般同一个threadLocal对象进来获取的是同一个位置。

ThreadLocalMap是保存在Thread中。
关系如下图所示

set是将key为ThreadLocal,value 为我们自定的对象的map。

当map不为空的时候将value与当前线程绑定,创建ThreadLocal.ThreadLocalMap,然后将value与当前线程绑定,其实只有一句代码

t.threadLocals = new ThreadLocalMap(this, firstValue);

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

getMap(t)是获取当前线程绑定的ThreadLocal.ThreadLocalMap对象,这个对象保存在Thread对象里的threadLocals属性,内容值也就是ThreadLocal的内部类ThreadLocalMap的实例,即我们设置过的key是当前ThreadLocal对象,value为我们自定义的对象。
如果为空则返回的是调用setInitialValue获取的值,这里就跟方法initialValue有关联了。我们可以看一下实现。

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

根据此我们可以重写initialValue()方法,放入我们的默认值。

四、项目中使用

情景:老项目因为一些原因很多接口要添加参数,如果接口里面都添加参数,调用方法添加参数,但是不想影响结构。那么可以用ThreadLocal来设置和获取参数的方式,防止对已有内容造成影响,首先得弄清楚,你对数据的处理是不是在同一个线程中。
对于接口添加参数的做法就是在拦截器里获取参数,添加到ThreadLocal中,后面哪里使用哪里取出来就好了。如果后面代码多线程的话不要直接使用。而存取可以用工具类的方式。比如:

public final class TestUtil{
    private static final String KEY= "参数";
    private static final ThreadLocal<Map<String, String>> sessionStore = new ThreadLocal<Map<String, String>>() {
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<>();
        }
    };
    public static void add(String content) {
        Map<String, String> sessionMap = sessionStore.get();
        sessionMap.put(KEY, content);
    }
     
    public static Locale get(){
       return  sessionStore.get().get(KEY);
    } 
    public static void remove() {
        sessionStore.remove();
    }
}

这里用了static修饰ThreadLocal,如果对象很大,线程时间很长的情况下建议不要用static修饰ThreadLocal,因为默认情况下因为用了弱引用的方式,也就是当ThreadLocal只有弱引用没有其他引用的时候,该对象将自动被GC,在调用调用ThreadLocal其他方法的时候可以清理value对象。根据此特性,在消耗比较大的情况下,可以做一定优化。避免内存泄露。

五、引申

其实里面用的一些方式也可以在其他项目中使用

  1. 弱引用的使用
  2. 神奇的数字0x61c88647的使用
  3. 钩子方法的使用
  4. ThreadLocal本身的使用

如果文中有错误的地方,欢迎指出,以免造成误导。

posted @ 2017-06-22 16:15  道常  阅读(198)  评论(0编辑  收藏  举报