美文网首页
Java并发编程三之ThreadLocal

Java并发编程三之ThreadLocal

作者: echoSuny | 来源:发表于2020-05-25 14:22 被阅读0次

ThreadLocal--线程本地变量,也称为线程本地存储。ThreadLocal可以让每个线程 拥有一个属于自己的变量副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。 在Android的Looper中就有对ThreadLocal的使用。

基本使用
public class UseThreadLocal {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) {
        Thread1 t1 = new Thread1();
        Thread2 t2 = new Thread2();
        t1.start();
        t2.start();
    }

    public static class Thread1 extends Thread {

        @Override
        public void run() {
            threadLocal.set("Thread1");
            System.out.println(Thread.currentThread().getName() + "-----" + threadLocal.get());
        }
    }

    public static class Thread2 extends Thread {

        @Override
        public void run() {

            System.out.println(Thread.currentThread().getName() + "-----" + threadLocal.get());
        }
    }
}

可以看到对于同一个ThreadLocal对象而言,我们在Thread1中set了一个数据,且Thread1先于Thread2执行,但是在Thread2中却get不到数据。这就是ThreadLocal的线程隔离,每个线程都拥有自己单独的变量副本,其他线程是访问不到的。
熟悉数据结构的童鞋可能会想着用HashMap就可以自己实现一个自己的ThreadLocal。

public class MyThreadLocal<T> {
    private Map<Thread, T> map = new HashMap<>();

    public synchronized void set(T t){
        map.put(Thread.currentThread(),t);
    }

    public synchronized T get(){
        return map.get(Thread.currentThread());
    }
}

下面把上面UseThreadLocal中的ThreadLocal换成我们自己的MyThreadLocal来看一下效果:



可以看到效果是一样的,同样做到了线程隔离。那么系统是不是这样做的呢?肯定不是!其实我们自己实现的ThreadLocal存在很大的性能问题。在线程比较多的时候会出现很激烈的竞争行为。打个比方,有十个人在操场上打一个篮球,那么这十个人就会存在对这一个篮球产生竞争的问题。那怎么解决呢?给他们每个人都发一个篮球,每个人都玩自己的篮球,那么就不会产生竞争了。但现在的情况是篮球都放在柜子里,而柜子是锁着的,那么想要拿到篮球就需要首先拿到锁的钥匙,才能打开柜子拿到篮球。虽然不争篮球了,但是现在都要去竞争柜子的锁的钥匙了。下面我们就分析一下JDK中的ThreadLocal是如何实现的。

基本原理
/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

源码的注释已经写的很清楚了,就是设置指定的值给当前线程的副本。方法的逻辑也很简单,首先获取当前正在执行的线程,然后根据当前线程获得一个ThreadLocalMap对象实例。

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到ThreadLocalMap其实就是Thread类的一个成员属性



而ThreadLocalMap则是ThreadLocal的内部类。接着看map的set()方法:

        private void set(ThreadLocal<?> key, Object value) {

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

第一行出现了一个Entry数组table。既然是数组肯定是用来存放数据的,那么到底是存放什么呢?

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

首先Entry是ThreadLocalMap的内部类,其次以ThreadLocal为键,Object为值来存储数据。熟悉HashMap源码的一定对第三行的代码不陌生,这行代码的目的是为了获取Entry在数组中的位置。拿到下标之后就从下标位置开始遍历数组。如果数组中存在Entry中的key与当前set时传入的key是同一个,则把旧值替换成新的值。存在Entry但是key为null,那么则进行替换。如果不存在上述的情况,则一直遍历直到数组中有空的位置,那么新建一个Entry对象存放到数组中。

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

get()方法的逻辑则很简单,不再进行分析。

ThreadLocal的内存泄露问题

上面我们得知Entry中只对ThreadLocal做了弱引用。弱引用的特点是如果系统中没有强引用指向它,那么下一次GC的时候是一定会被回收的。同样的我们根据上文得知Thread中持有ThreadLocalMap的引用,ThreadLocalMap则持有Entry的引用,而Entry有持有value的引用,也就是说Thread是直接持有value的。这是一种强引用,是不会被GC回收的。而线程的生命周期是很长的,比如线程池中的线程可以一直被重复利用,如果一直持有value时间长了则肯定会导致OOM。虽然在ThreadLocal的set()和get()方法中有对key为null的脏Entry进行处理,但是最好还是显式的调用remove()方法:

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

ThreadLocal的remove方法会调用ThreadLocalMap的remove()方法:

        private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    // 清理脏Entry
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // value置为null 断开引用链 当下次GC的时候回收
            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)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    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;
        }

相关文章

网友评论

      本文标题:Java并发编程三之ThreadLocal

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