zoukankan      html  css  js  c++  java
  • Java ThreadLocal 源代码分析

    Java ThreadLocal

    之前在写SSM项目的时候使用过一个叫PageHelper的插件

    可以自动完成分页而不用手动写SQL limit

    用起来大概是这样的

    最开始的时候觉得很困惑,因为直接使用静态成员函数,那么就意味着如果有别的线程同时执行,可能会导致一些并发错误

    答案是不会,因为PageHelper内部实现是使用到了ThreadLocal这个对象的,每个线程单独使用一个Page对象

    百度了一下,发现ThreadLocal是一个提供类似线程内部的局部变量

    我们来看一下ThreadLocal的源代码:初始化的时候涉及到这几个变量

    private static AtomicInteger nextHashCode =
    	new AtomicInteger();
    private final int threadLocalHashCode = nextHashCode();
    
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
    	return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    

    每次创建一个ThreadLocal的时候就会给这个?ThreadLocal分配一个hashcode

    为什么不是每次increateAndGet 注释里面有解释:

    连续生成的散列码的区别

    隐式顺序线程局部ID进入近最优分布

    两个大小表的幂的乘法哈希值。

    首先看一下get方法

    //ThreadLocal.java
    	public T get() {
            //首先获取当前的Thread
            Thread t = Thread.currentThread();
            //通过当前Thread尝试获取 ThreadLocalMap
            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;
                }
            }
            //当前Thread并没有初始化map或者Thread值,进行初始化操作
            return setInitialValue();
        }
    

    通过名字可以知道ThreadLocalMap似乎是个Map

    我们先查看一下getMap

    发现这个map是存储在Thread 里面的,包作用域,用户不可见

    //Tread.java class Thread
    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    我们现在看一下这个ThreadLocalMap的定义

    static class ThreadLocalMap {
        //定义键值对,是一个WeakReference
            static class Entry extends WeakReference<ThreadLocal<?>> {
                //ThreadLocal里面保存变量的值
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
        //默认初始容量
            private static final int INITIAL_CAPACITY = 16;
        //桶
            private Entry[] table;
            private int size = 0;
    

    初始化:

    我们可以看到第一次初始化的时候是使用firstKey的threadLocalHashCode(firstKey指的是外部的this)刚才提到的初始化的时候分配的一个hashcode,具体桶的位置跟hashmap类似都是(桶-1)&hashcode

    //ThreadLocal.java 
    //static class ThreadLocalMap    
    	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里面的 get方法

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

    ThreadLocalMap解决hash冲突的方法并不是用链表,而是使用线性探测法

    这也就解释了为什么分配的hashcode不应该是连续的原因,否则一旦出现hash冲突,线性探测找到一个可用的空间或者key对应的值非常艰难

    我们来看一下是如何实现线性探测的:

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    //当这个位置不是空的时候继续探测
        while (e != null) {
            ThreadLocal<?> k = e.get();
            //判断key是不是相等,如果相等说明找到了
            if (k == key)
                return e;
            //这里不太理解,查了一下别人的分析
            //k==null说明这个key已经被释放掉,需要清理掉
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);//((i + 1 < len) ? i + 1 : 0);就是下一个空间,如果到末尾就从头开始
            e = tab[i];//下一个空间里面的值
        }
        return null;
    }
    //这个函数不太理解
    //我猜大概意思就是需要别的地方有一个ThreadLocal引用否则ThreadLocal可能被清理掉
    //弱引用会被GC标记存活
    //这个做的应该是标记为null之后,把后面的值放到前面,否则再次get的时候碰到null就找不到了
    /**
     * 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
     * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
     * 另外,在过程中还会对非空的entry作rehash。
     * 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
     */
            private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
    
                // expunge entry at staleSlot
                //直接置null
                tab[staleSlot].value = null;
                tab[staleSlot] = null;
                size--;
    
                // Rehash until we encounter null
                Entry e;
                int i;
                //开始从这个位置开始线性探测
                for (i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    //每次线性探测一个格子直到找到null
                    ThreadLocal<?> k = e.get();
                    //key为null说明需要清理掉
                    if (k == null) {
                        e.value = null;
                        tab[i] = null;
                        size--;
                    } else {
                        //重新找到标记桶的位置
                        int h = k.threadLocalHashCode & (len - 1);
                        if (h != i) {
                            //清理当前位置
                            tab[i] = null;
    					
                            // Unlike Knuth 6.4 Algorithm R, we must scan until
                            // null because multiple entries could have been stale.
                            //线性探测一个可以用的位置,然后把自己放进去
                            while (tab[h] != null)
                                h = nextIndex(h, len);
                            tab[h] = e;
                        }
                    }
                }
                return i;
            }
    
    

    并发安全问题:

    代码中没有使用到任何锁和同步,为什么还是安全的

    因为每个线程操作的ThreadLocalMap都是每个线程自带的,当然不用同步啦

    具体使用:比如说解决SimpleDateFormat的问题

    这样每个线程只会创建一个

    具体的线性探测hash可以看着里面的图

    https://www.cnblogs.com/micrari/p/6790229.html

  • 相关阅读:
    navicat连接虚拟机中mysql"Access denied for user'root'@'IP地址'"问题
    Centos6.4 + mysql-5.6.38-linux-glibc2.12-x86_64.tar 实现mysql主从复制
    三、mock测试技术
    二、数据加密
    一.unittest框架初识
    3.Allure报告
    2.pytest参数化
    1.pytest框架初识
    RabbitMQ 几种工作模式---(三) Publish/Subscribe
    RabbitMQ 几种工作模式---(二)work
  • 原文地址:https://www.cnblogs.com/stdpain/p/10661886.html
Copyright © 2011-2022 走看看