ThreadLocal实现分析

ThreadLocal 可以让用户很方便的保存线程隔离的变量,每个线程只能保存一份,多次set新值会覆盖旧值。下面分几个角度来分析一下ThreadLocal。

使用方法

简单附上使用方法:

public class TestThreadLocal {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static Object lock = new Object();
    public static void main(String[] args) {
        threadLocal.set("a string run in main");
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                TestThreadLocal.threadLocal.set("a string run in sub");
                synchronized (TestThreadLocal.lock){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("sub thread get: "+threadLocal.get());
            }
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (TestThreadLocal.lock){
            lock.notify();
        }
        System.out.println("main thread get: " +threadLocal.get());
    }
}


运行结果

main thread get: a string run in main
sub thread get: a string run in sub

Process finished with exit code 0

可以看到两个线程间设置的值互不影响,均可以取出正确值。

源码解析

ThreadLocal主要有3个方法,get,set,remove。我们只要了解了插入的原理,另外两个自然不攻自破,附上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);
    }

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

主要结构是ThreadLocalMap,把它的数据部分以及set方法贴出来。

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

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

使用之前例子中的情况,可以看到,当我们调用set方法时,实际是以threadLocal变量本身和这个“a string run in main”字符串生成一个Entry,根据对象的hash值计算位置,放到table数组中。

一些细节

线程安全性。

ThreadLocal变量是存储在线程对象中的,即对于同一个ThreadLocal变量,在不同的线程中操作时,修改的不是同一个内存空间,因此ThreadLocal是线程安全的类。

泛化存储类型

ThreadLocal<Integer> 和 ThreadLocal<String>都是放在同一个Thread相关联的map中,类型不一样怎么存储?因此需要在调用set方法的时候将存入的对象转为Object对象,在调用get方法的时候再转换回来。

如何保证hash冲突后功能的正确性(重点)

首先我们可以看到THreadLocal处理冲突的算法:如果hash冲突了,ThreadLocal采取的是线性探测法。
考虑set方法:
从hash值的位置往后寻找到最近的一个可用位置(可用即当前位置为null,table结尾位置也不为null的话将从位置0继续查找,直到找到)。
考虑get方法:
调用get方法时,如果hash值对应的位置非空,但存储的引用和当前的ThreadLocal对象不一致,那么还有可能是因为冲突了存储在这个位置以后,从当前位置往后查找,依次对比是否是当前ThreadLocal对象,找到则返回,遇到null则说明没找到。

这对ThreadLocal提出了两点要求:

  1. table不能是满的,否则get一个未set的ThreadLocal对象将死循环。
    这个问题好解决,ThreadLocal的rehash因子是2/3,因此无论何时table必定有位置的值是null。

  2. 具有相同hash值的对象,存储必须是连续的,即中间不能有null间隔开来。这就要求在remove一个对象的时候,需要将这个空位以后直到null中间的值都重新计算位置。

弱引用

java中GC的方法是判断对象的引用计数是否等于0,等于0则回收这个对象。正常情况下我们调用如下代码将产生强引用,两句代码执行过后,threadlocal指向的对象的引用计数是2。弱引用的作用就是不参与引用计数的计算。使用弱引用保证了当使用者将ThreadLocal对象置空或指向一个新的对象时,旧对象能得到被GC的机会。
同时我们也就可以解释ThreadLocal代码中多处replaceStaleEntry,expungeStaleEntry(清除过期节点)的原因。

ThreadLocal<String> threadlocal = new ThreadLocal<>();

ThreadLocal<String> threadlocal2 = threadlocal;

InheritableThreadLocal

InheritableThreadLocal继承自ThreadLocal,它和ThreadLocal不同的是,父线程保存的InheritableThreadLocal变量可以在子线程中存留。这个是如何实现的呢?

从以下Thread类的代码可以看到(最后几行),线程类中有一个成员变量inheritableThreadLocals,在构造函数中,会调用init方法,里面以父类的inheritableThreadLocals对象为模版构造了一份子类的inheritableThreadLocals。

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
    原文作者:msrpp
    原文地址: https://www.jianshu.com/p/4ba09bfad028
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞