欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ThreadLocal实现分析

程序员文章站 2022-07-03 14:59:08
...

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