简析线程上下文ThreadLocal

在这里插入图片描述

前言

在有些时候,单个线程执行任务非常多的时候,后一个步输入是前一个步骤的输出,我们有时候会采用责任链模式,但是调用链路长了以后,这种传参方式
会显得冗余,于是就有了线程上下文的设计,每个线程会有不同的参数实例。原因是每个线程Thread.currentThread()作为key,这样就可以保证线程的
独立性。

需要注意的是,我们会一般用Map存储,用当前的线程作为key,当线程生命周期结束后,Map中的实例并不会释放,因为还是根可达的,久而久之,会产生内存溢出。

ThreadLocal 简单介绍

在展开深入分析之前,咱们先来看一个官方示例:


public class ThreadId {

    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
                return nextId.getAndIncrement();
            }
        };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
                }
            }).start();
        }
    }

}

运行如下,

threadName=Thread-2,threadId=0
threadName=Thread-3,threadId=1
threadName=Thread-0,threadId=2
threadName=Thread-1,threadId=3
threadName=Thread-4,threadId=4

可以看到每个线程不会相互影响,每个线程存入threadLocal的值是完全互相独立的。

我再把类注释放上去

 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).

大概意思就是几条:

  • 它能让线程拥有了自己内部独享的变量

  • 每一个线程可以通过get、set方法去进行操作

  • 可以覆盖initialValue方法指定线程独享的值

  • 通常会用来修饰类里private static final的属性,为线程设置一些状态信息,例如user ID或者Transaction ID

  • 每一个线程都有一个指向threadLocal实例的弱引用,只要线程一直存活或者该threadLocal实例能被访问到,都不会被垃圾回收清理掉

ThreadLocal常用方法介绍

ThreadLocal方法有如下:

ThreadLocal、
childValue、
createInheritedMap、
createMap、
get、
getMap、
initialValue、
nextHashCode、
remove、
set、
setInitialValue、
withInitial、
HASH_INCREMENT、
nextHashCode、
threadLocalHashCode

ThreadLocal常用方法有initialValue、get、set、remove。

initialValue


    protected T initialValue() {
        return null;
    }

就是在为每个线程变量赋一个默认值,如果没有设的话,默认为null。

set

就是如果没有调用set方法,数据的初始值就是initialValue的值。


   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

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

  • 获取当前现成的Thread.currentThread()

  • 根据当前线程获取到线程的ThreadLocalMap,可以看下面Thread源码,
    每个线程有一个ThreadLocalMap的引用,先记下,后面有用。


public
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

  • 如果没有获取到就创建,获取到就赋值

  • 先说下这个创建map的方法,首先会new一个ThreadLocalMap,创建entry,用当前的ThreadLocal作为key,要存的数据作为value,结束

  • 如果map存在,就遍历entry,找到如果有相同的ThreadLocal,那就用新的值替换掉,结束。

  • 如果没有找到相同的ThreadLocal,则创建entry,用当前的ThreadLocal作为key,要存的数据作为value

注意,如果遍历过程中,entry中key的值为null,那就用新的key替代,还会根据当前的size和阈值进行比较,超过阈值,则进行key值为null的清理。

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

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

  • 获取当前线程内部的ThreadLocalMap

  • map存在则获取当前ThreadLocal对应的value值

  • map不存在或者找不到value值,则调用setInitialValue,进行初始化

看看ThreadLocalMap


    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

EntryWeakReference类型的,Java垃圾回收时,看一个对象需不需要回收,就是看这个对象是否可达。什么是可达,
就是能不能通过引用去访问到这个对象。

jdk1.2以后,引用就被分为四种类型:强引用、弱引用、软引用和虚引用。强引用就是我们常用的Object obj = new Object(),obj就是一个强引用,指向了对象内存空间。当内存空间不足时,Java垃圾回收程序发现对象有一个强引用,宁愿抛出OutofMemory错误,也不会去回收一个强引用的内存空间。而弱引用,即WeakReference,意思就是当一个对象只有弱引用指向它时,垃圾回收器不管当前内存是否足够,都会进行回收。反过来说,这个对象是否要被垃圾回收掉,取决于是否有强引用指向。ThreadLocalMap这么做,是不想因为自己存储了ThreadLocal对象,而影响到它的垃圾回收,而是把这个主动权完全交给了调用方,一旦调用方不想使用,设置ThreadLocal对象为null,内存就可以被回收掉。

看个例子


public class MemoryLeak {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    TestClass t = new TestClass(i);
                    t.printId();
                    t = null;
                }
            }
        }).start();
    }


    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}

运行的结果为: java.lang.OutOfMemoryError: Java heap space

稍作修改

public class MemoryLeak {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    TestClass t = new TestClass(i);
                    t.printId();
                    t.threadLocal.remove();
                }
            }
        }).start();
    }


    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}

正常完成,比代码只有一处不同:t = null改为了t.threadLocal.remove();

我们来看下remove的源码


     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

就一句话,获取当前线程内部的ThreadLocalMap,存在则从map中删除这个ThreadLocal对象。

分析下为什么?

   ThreadLocalMap.class

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

Entry中的key指向的是ThreadLocal,说明key是弱引用,当线程的周期结束后,会自动回收,也就是key这时为null,但是value是Object,这个是强引用,没有办法回收,这也是之前看set方法源码的时候,会清除key为null的数据,我们在
实际应用的时候记得线程结束后,调用remove方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章