zoukankan      html  css  js  c++  java
  • 干了这杯java之ThreadLocal

    ThreadLocal

    Java篇

    1. 是什么
    2. 怎么用
    3. 源码
    4. 缺点
    5. 总结

    是什么

    ThreadLocal是一个关于创建线程局部变量的类,这个变量只能当前线程使用,其他线程不可用。
    ThreadLocal提供get()和set()方法创建和修改变量。

    怎么使用

    ThreadLocal threadLocal = new ThreadLocal();
    
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    ThreadLocal threadLocal = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "初始化值";
        }
    };
    

    源码

    类结构图

    get(),set()

    查看ThreadLocal中的get(),set()中有一个ThreadLocalMap对象

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

    ThreadLocalMap

    ThreadLocalMap 就是一个内部静态类,没有继承也没有接口,是一个自定义的Hash映射,用户维护线程局部变量。

    static class ThreadLocalMap
    

    ThreadLocalMap的内部类Entry,继承WeakReference 弱引用

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            //key放在WeakReference<ThreadLocal<?>>中
            super(k);
    		  //变量放在Object value中
            value = v;
        }
    }
    

    ThreadLocalMap中存放线程局部变量的数据结构

    private Entry[] table;
    

    小结:

    1. ThreadLocal ——> ThreadLocalMap——> Entry[]
    2. Entry维护一个ThreadLocal 作为key,value对应ThreadLocal的值

    初始化方法

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    	 //默认容量为16
        table = new Entry[INITIAL_CAPACITY];
    	  //threadLocalHashCode是一个原子类AtomicInteger的实例,每次调用会增加0x61c88647。&位移操作使存放分布均匀
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    	  //放入数组
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    
    //nextHashCode实现
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    

    小结:

    1. ThreadLocalMap默认容量为16,每次计算索引位置会加0x61c88647然后和长度-1取模
    2. 索引是原子类

    Entry的get

    private Entry getEntry(ThreadLocal<?> key) {
    	  //定位i的位置
        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);
    }
    
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    	  //hashcode索引相同所以查找下一个,用循环比对取出
        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. get方法中先计算索引位置,如果key相同则返回,不同则用线性探测法取出,当key为null的时候清理i所在位置直到不为null的数据。如果找不到key的数据则返回null

    Entry的Set

    	private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
    	  //hashcode索引
        int i = key.threadLocalHashCode & (len-1);
    
    	  //线性探测法,如果在有值的情况下,key不同则继续下一个
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
    		  //如果当前有值&&key相同则更新value
            if (k == key) {
                e.value = value;
                return;
            }
    		 //如果key空,则key-value重新替换
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    	 //索引位置找到,插入key-value,对size+1
        tab[i] = new Entry(key, value);
        int sz = ++size;
    	  //cleanSomeSlots清理key关联的对象被回收的数据,如果没有被清理的&&size大于扩容因子,刷新
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    

    小结:

    1.计算索引位置
    2.如果当前位置有值则索引+1判断是否为空,不为空继续+1,直到找到位置插入
    3.size+1
    4.是否清理key为null的数据,如果没有被清理&& size大于列表长度的2/3则扩容

    清理key关联的对象被回收的数据

    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
    		  //key为null,被清理
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
    			  //移除i位置之后为key为null的元素
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }
    

    expungeStaleEntry方法

    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
    	  //将上面staleSlot的数据清空,大小减去1
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;
    
        Entry e;
        int i;
    	  //以staleSlot往后找key为null的
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
    		  //key为null清空
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
    			  //key不为null,计算当前hashCode索引位置,如果不相同则把当前i清除,当前h位置不为null,再向后查找key合适的索引
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }
    

    小结:

    1. 从staleSlot开始,清除key为null的Entry,并将不为空的元素放到合适的位置,最后遍历到Entry为空的元素时,跳出循环返回当前索引位置

    rehash方法

    private void rehash() {
        expungeStaleEntries(); //调用expungeStaleEntries()方法
    	  //size的长度超过容量的3/4,则扩容
        if (size >= threshold - threshold / 4)
            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();
    			  //key为null,value也设置为null,清理
                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;
    }
    
    private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }
    
    

    小结:

    1. 调用expungeStaleEntries方法,清理整个table中key为null的Entry
    2. 如果清理后size超过阈值的1/2,则进行扩容。
    3. 新表长度为老表2倍,创建新表。
    4. 遍历老表所有元素,如果key为null,将value清空;否则通过hash code计算新表的索引位置h,如果h已经有元素,则调用nextIndex方法直到寻找到空位置,将元素放在新表的对应位置。
    5. 设置新表扩容的阈值、更新size、table指向新表

    缺点

    内存泄露

    从Entry源码中可以看出,Entry继承了WeakReference弱引用,如果外部没有引用ThreadLocal,则Entry中作为Key的ThreadLocal会被销毁成为null,那么它所对应的value不会被访问到。当线程一直在执行&&没有进行remove,rehash等操作时,value会一直存在内存,从而造成内存泄露

    总结

    1. Thread中都有一个ThreadLocalMap
    2. ThreadLocalMap的key是ThreadLocal实例
    3. 默认容量大小为16,当size超过2/3容量&&没被清理就rehash,
    4. 当size超过扩容因子3/4的时候扩容为原来的2倍
    5. 当发现一个key为null的时候,会进行清理,直到下一个key不为null
    6. has冲突的解决方法和hashMap不相同,ThreadLocal是找这个冲突索引的下一个元素直到找到,hashMap是转换为红黑树
  • 相关阅读:
    IE9的兼容性
    element 弹框关闭报错
    时间选择器moment格式化存在时差问题
    项目常见bug
    函数封装——函数封装——函数封装
    element-ui 日期选择器范围时间限制
    vue + element 创建教程
    Html基础学习
    HTML、PHP、CSS、JS之间的关系
    vs连接MySQL
  • 原文地址:https://www.cnblogs.com/imeng/p/10782986.html
Copyright © 2011-2022 走看看