zoukankan      html  css  js  c++  java
  • ThreadLocal 工作原理、部分源码分析

    1.大概去哪里看

    ThreadLocal 其根本实现方法,是在Thread里面,有一个ThreadLocal.ThreadLocalMap属性

    ThreadLocal.ThreadLocalMap threadLocals = null;


    ThreadLocalMap 静态内部类维护了一个Entry 数组

    private Entry[] table;
    

    查看Entry 源码,它维护了两个属性,ThreadLocal 对象 与一个Object

    static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
    }

    那么,这几项似乎可以这么串下来: Thread. currentThread().threadLocals. table{当前线程,的ThreadLocalMap对象,的Entry数组}(忽略访问权限的事儿)

    ------------------------------------------------我是分割线------------------------------------------------

    2.代码实现分析

    ThreadLocal 提供set(),get()方法,用于数据的写入与读取。数据的存储与获取的位置,即
    Thread. currentThread().threadLocals. table {当前线程,的ThreadLocalMap对象,的Entry数组}

    public void set(T value) {
            Thread t = Thread.currentThread();//获取当前线程t
            ThreadLocalMap map = getMap(t);//获取threadLocals 对象
            if (map != null) 
                map.set(this, value);//调用 ThreadLocalMap 的set方法向 threadLocals 中写入一条数据
            else
                createMap(t, value);//如果threadLocals 为null 则为当前线程t 创建一个map,并插入数据
    }

    map.set(this, value);注意,这里,传入的第一个参数为this 即 ThreadLocal 对象自身,

    假如我们声明了一串代码:

    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    

    然后我们又执行了 threadLocal.set(“string 1234”);
    那么,在Thread. currentThread().threadLocals. table 中,应该有这么一个Entry :ThreadLocal指向threadLocal,value 为 “string 1234”

    分析源码(这里,所有的源码都来自于jdk1.7.0_71):

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

    分析两处:
    1. int i = key.threadLocalHashCode & (len-1);
    根据当前的ThreadLocal 的threadLocalHashCode 跟 ThreadLocalMap.table的长度-1 ,按位与,获得目标索引值 i , 如果tab[i] 为空的话,将会在 tab[i] 处插入一个Entry ;

    2. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)])

    如果如果tab[i] 不为空,则调用i = nextIndex(i, len) 将i值进行+1或者置为0,然后判断e是否为null 如果e!=null 判断e 中的 ThreadLocal对象,跟传入的ThreadLocal 对象,是否为同一个对象。如果是同一个对象,则对e的value 进行重新赋值。如果在遍历的过程中发现某个e的ThreadLocal 对象为空,则将Entry(threadLocal,” string 1234”) 设置在此时的tab[i]处。
    (如果一开始进来的时候e 为null 即 tab[i]==null 。是不会走for循环的,会直接把Entry(threadLocal,” string 1234”) 赋值到table[i]);

    private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
    }

    分析,为什么会有i = nextIndex(i, len) 这样的设定。(2017-01-04 ps:这是一种hash防冲撞的方案)
    执行int i = key.threadLocalHashCode & (len-1);的时候,很可能不同的key.threadLocalHashCode得到了相同的 i 值,那么,就从 i 开始,遍历table对象,找到一个可以放置Entry(threadLocal,” string 1234”) 的位置,

    比如:

        System.out.println(626627285 & 16-1);//5
        System.out.println(626627317 & 16-1);//5
        System.out.println(626627573 & 16-1);//5

    这三个,获取到的i值,都为5(当然实际用到的hashCode的算法不是这样的,不会产生这么接近的数)。

    在同一个线程中,626627285先set(value1)了,得到5,table[5]为空,那就填进去 table[5]=new Entry(626627285,value1);
    626627317接着set(value2),算出来i=5,
    但是table[5]已经有人占了,那就只能看table[6]有没有空闲位置,一看table[6]==null,好,就放这儿了table[6]=new Entry(626627317,value2);
    626627573接着set(value3),算出来i=5,
    但是table[5]已经有人占了,那就只能看table[6]有没有空闲位置,一看table[6]也被占了,再看table[7]==null,好,就放这儿了table[7]=new Entry(626627573,value3)
    假如有个线程算出来i=15 但是table[15]!=null,需要向后找空闲位置,table[16]是越界的,nextIndex返回0,从table[0]开始找

    下面这串代码,维护了table 的长度,避免了遍历了一圈table 却找不到 table[i]==null 的情况,即保证table的某些索引处肯定为null,因为还没填满的时候就已经扩容了。

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

    1. 获取当前线程的threadLocals 并传入 ThreadLocal 对象,获取对应的值。
    2. 如果当前线程的threadLocals 为null ,则为当前线程t 创建一个map,并插入数据setInitialValue ()=null,并返回null

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

    分析map.getEntry(this)

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

    这里看到,也是先使用 int i = key.threadLocalHashCode & (table.length - 1); 得到一个索引值,然后去table获取 Entry对象,得到几种结果:
    1. e!=null && e.get()!=key 因为是通过“int i = key.threadLocalHashCode & (table.length - 1);”获取的索引值,不同的ThreadLocal 对象,可能获取到相同的索引值,所以,这种情况是存在的。
    2. e==null 当前的ThreadLocal 对象 压根儿没有set值。
    3. e!=null&&e.get()==key 如果Entry对象的key值与当前传入的ThreadLocal 对象,是同一个对象,则返回e 然后在 get()方法中,返回e.value;
    以上 1、2 两种情况的时候,会执行getEntryAfterMiss(key, i, e);

    在同一个线程中,626627285、626627317、626627573算到的i值均为5,但是只有626627285==table[5].get(),
    626627317、626627573这两个,都需要走getEntryAfterMiss(key, i, e)方法了
    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. 如果初始出入的e==null 则不进入while 循环直接返回null

    2. 在while循环里面,如果e.get()==key 即,e 的Entry对象的key值与当前传入的ThreadLocal 对象,是同一个对象,则返回e 。
    3. 如果e.get()==null 的情况下,先将table[i]置为空,然后向后遍历直到
    table[nextIndex(i, len)]==null,将table[nextIndex(i, len)]!=null的对象,重新写入table中。
    4. k!=key&&key!=null的时候,则调用i = nextIndex(i, len) 将i值进行+1或者置为0,然后判断e是否为null 如果e!=null 判断e 中的 ThreadLocal对象,跟传入的ThreadLocal 对象,是否为同一个对象。如果是同一个对象,返回当前的 Entry对象,如果遇到了e==null的时候,还没有找到目标的Entry ,就返回null 。
    为什么找到e==null的地方就可以跳出了呢?
    如果如果tab[i] 不为空,则调用i = nextIndex(i, len) 将i值进行+1或者置为0,然后判断e是否为null 如果e!=null 判断e 中的 ThreadLocal对象,跟传入的ThreadLocal 对象,是否为同一个对象。如果是同一个对象,则对e的value 进行重新赋值。如果在遍历的过程中发现某个e的ThreadLocal 对象为空,则将Entry(threadLocal,” string 1234”) 设置在此时的tab[i]处。
    这里,插入的Entry(threadLocal,” string 1234”) ,要么,在初始的i 处,要么,往后+1顺延,不会跳过某个索引值,然后进行赋值。所以,当table[i]==null的时候,已经可以不继续找了。


    ------------------------------------------------我是分割线------------------------------------------------

    3 还有一点我觉得很重要的东西

    分析到这里的时候,我们应该有发现一个很重要的问题,即:
    int i = key.threadLocalHashCode & (table.length - 1);
    获得的i值是固定的吗?
    很明显不是的,因为table 会扩容,table.length 会变,得到的i值也是不一样的:

    System.out.println(626627285 & 16-1);//5
    System.out.println(626627285 & 32-1);//21

    这样的话,table[i]岂不是会出错?

    然后,仔细阅读源码,在进行扩容的时候,会调用resize()方法:

    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;
    
        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; // Help the GC
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }
    
        setThreshold(newLen);
        size = count;
        table = newTab;
    }

    在resize() 方法中,我们看到,这里新建了一个长度为原本的长度2倍的Entry数组,然后,将原Entry数组的所有元素,

    挨个儿的重新计算索引 int h = k.threadLocalHashCode & (newLen - 1);

    然后赋值原有的Entry 到新的Entry 数组中,这样,就保证了数组扩容之后,获取到的 i 值,是适配新数组的正确的值。

    继续拿626627285、626627317、626627573举例,这三个,在数组扩容为长度=32的时候,算出来的i值,均为21,那么他们将会是这么算的:
    oldTab[5]!=null oldTab[5].get=626627285 算得i=21,table[21]==null table[21]=oldTab[5];
    oldTab[6]!=null oldTab[6].get=626627317 算得i=21,table[21]!=null table[22]==null table[22]=oldTab[6];
    oldTab[7]!=null oldTab[7].get=626627573 算得i=21,table[21]!=null tanle[22]!=null table[23]==null table[23]=oldTab[7];

    ------------------------------------------------我是分割线------------------------------------------------

     以上,是我个人查看jdk源码,分析出来的ThreadLocal得实现原理与工作原理。欢迎大家批评讨论斧正。谢谢

  • 相关阅读:
    安卓官方ViewPager与android.support.design.widget.TabLayout双向交互联动切换 。
    安卓系统设置选项的框架。
    Android动画效果生动有趣的通知NiftyNotification(Android Toast替代品)
    Android第三方开源对话消息提示框:SweetAlertDialog(sweet-alert-dialog)
    百度地图可视化定位效果,可以输入目的地定位。
    利用百度地图开源sdk获取地址信息。
    可伸缩的textview。
    两个activity之间传递数据用startActivityForResult方法。
    Android 主页面顶部栏的通知Notification ,可以自定义通知消息栏的风格,并且点击通知栏进人本程序。
    获取手机通讯录放入PinnedSectionListView中,按名字首字母排序,并且实现拨打电话功能。
  • 原文地址:https://www.cnblogs.com/fri-yu/p/6229930.html
Copyright © 2011-2022 走看看