美文网首页
学习笔记:ThreadLocal使用及注意事项

学习笔记:ThreadLocal使用及注意事项

作者: 大力papa | 来源:发表于2020-07-01 23:45 被阅读0次

    本文仅供学习交流使用,侵权必删。
    不作商业用途,转载请注明出处。

    自JDK1.2版本起,java提供了java.lang.ThreadLocal。ThreadLocal为使用该变量的线程都提供了相互独立的副本,实现线程间的数据隔离。

    ThreadLocal的使用场景和源码分析

    • 在对象跨层传递的时候,可以使用ThreadLocal,避免方法多次参数传递
    • 进行事务操作,用于存储线程事务信息。Spring中的AbstractRoutingDataSource实现动态数据源切换就会用到ThreadLocal
     public static void main(String[] args) throws InterruptedException {
            ThreadLocal<String> tl = new ThreadLocal();
            ThreadLocal<String> tl2 = new ThreadLocal();
            new Thread(() -> {
                tl.set("t1_threadlocal");
                tl2.set("t1_threadlocal_2");
                System.out.println("t1: " + tl.get());
                System.out.println("t1: " + tl2.get());
                System.out.println("t1: " + tl.get());
                tl.remove();
            }, "t1").start();
    
            new Thread(() -> {
                tl.set("t2_threadlocal");
                System.out.println("t2: " + tl.get());
                System.out.println("t2: " + tl2.get());
            }, "t2").start();
        }
    

    initialValue()

    initialValue()为ThreadLocal要存储的数据类型指定一个初始化值,不重写该方法默认是返回null

     public static void main(String[] args) {
    
            ThreadLocal<String> tl = new ThreadLocal<String>() {
                @Override
                protected String initialValue() {
                    return "hello";
                }
            };
    
            new Thread(()->{
                System.out.println("tl::get : "+tl.get());
            }).start();
            //返回我们定义的初始化值hello
        }
    

    set(T value)

    set方法把当前ThreadLocal和我们需要设置的值绑定设置到当前线程的ThreadLocalMap中

    ThreadLocal::set
     public void set(T value) {
            Thread t = Thread.currentThread();//获取当前线程
            ThreadLocalMap map = getMap(t);//从当前线程中获取ThreadLocalMap
            if (map != null)
                map.set(this, value);//调用ThreadLocalMap set方法
            else
                createMap(t, value);//否则创建ThreadLocalMap并set
        }
    
    ThreadLocalMap::set
        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);
    
                /*
                寻找当前ThreadLocal是否已经设置过value
                 */
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    //Entry获得的ThreadLocal等于当前ThreadLocal
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    /*
                    Entry里面的ThreadLocal为null
    
                    有可能是ThreadLocal已经没有被强引用指向,gc已经把它回收了
                     */
                    if (k == null) {
                        //替换旧的Entry,key为当前ThreadLocal
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
                int sz = ++size;
                /*
               清理ThreadLocal为null的Entry,value指向null,防止memory leak
                 */
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
    ThreadLocal::createMap
     void createMap(Thread t, T firstValue) {
            // 初始化ThreadLocalMap,并把firstValue设置到Map中
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    1. 获取当前线程以及当前线程对象指向的ThreadLocalMap
    2. 如果ThreadLocalMap不为null,调用第3步 ThreadLocalMap的set方法。否则调用第4部 createMap
    3. ThreadLocalMap的set方法
    • 遍历查询当前线程的ThreadLocalMap的Entry[],如果找到对应的Entry对象且通过Entry::get获取的ThreadLocal跟当前的ThreadLocal相等,那么就将新值替代旧值。如果Entry::get为null,直接将其逐出并使用新值占用被逐出数据的位置
    • 如果从Entry[]数组中没有遍历查询到对应的Entry对象,则创建一个新的Entry,使用ThreadLocal为key,要存放的值为value。存储到数组中
    1. createMap,为当前线程创建一个ThreadLocalMap,并将当前需要存放的值存储进去

    get()

    ThreadLocal::get
     public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);//获取线程中的ThreadLocalMap
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T) e.value;
                    return result;
                }
            }
            /*
            如果当前线程的ThreadLocal的Entry为null,
            或者ThreadLocalMap未初始化,
            就调用setInitialValue,返回一个初始值
             */
            return setInitialValue();
        }
        }
    
    ThreadLocalMap::getEntry
    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);
            }
    

    1.根据当前线程获取ThreadLocalMap,如果map为null,调用 第2步,否则调用第3步
    2.调用setInitialValue返回一个初始值
    3.调用ThreadLocalMap的getEntry方法获取对应的值

    ThreadLocal内存泄漏问题

    image.png image.png
    • 图一是正常引用了ThreadLocal变量的图,而当ThreadLocalRef显式地指向null时,就成为图二的情况。gc就会将heap中的ThreadLocal回收。Entry.key就会为null,无法获取到value,但ThreadRef到Entry.value的引用链路是可达的。所以Entry.value 一直不会被gc回收而导致内存泄漏。
    • ThreadLocal内部针对内存泄漏问题是实现了相应的解决方案的。
      1. ThreadLocal的set方法里面cleanSomeSlots调用了expungeStaleEntry清除key为null的Entry
      2. ThreadLocal的get方法里面getEntry调用了getEntryAfterMiss方法,这个方法同样调用了expungeStaleEntry清除key为null的Entry
    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];
                    if (e != null && e.get() == null) {
                        n = len;
                        removed = true;
                        i = expungeStaleEntry(i);//清除key为null的Entry
                    }
                } while ((n >>>= 1) != 0);
                return removed;
            }
    
      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;
            }
    
    • 但即便ThreadLocal有相应的处理,其实也会有可能存在内存泄漏的情况。当程序既不调用get,又不调用set的情况下就触发不了ThreadLocal的处理机制。所以日常开发中,当使用完ThreadLocal的时候调用一下remove()

    总结

    ThreadLocal的用法相对简单,但由于有可能出现内存泄漏的情况,所以开发过程中需要注意。

    相关文章

      网友评论

          本文标题:学习笔记:ThreadLocal使用及注意事项

          本文链接:https://www.haomeiwen.com/subject/zvdlhhtx.html