zoukankan      html  css  js  c++  java
  • 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本身的使用

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

    由于本人才疏学浅,若有错误遗漏之处,望指出,以免误导其他同学。
  • 相关阅读:
    为什么java使用对象序列化到文件,打开之后是乱码,而且更换编码格式查看也不能正常显示呢
    String类能够导入IDEA,但是里面的构造方法无法使用,一直显示报错
    IDEA不能提示导入java.io.File类,但是自己手写import没问题,最后找到了问题所在
    扑克牌发牌,排序的功能实现
    TFS 生成定义
    Git-tfs工具
    日期和时间
    调用惯例
    优化查找和排序
    使用更好的库
  • 原文地址:https://www.cnblogs.com/daochang/p/7065616.html
Copyright © 2011-2022 走看看