当前位置: 首页>后端>正文

ThreadLocal

ThreadLocal 概述

概述

  • ThreadLocal类用来提供线程内部的局部变量,不同的线程之间不会相互干扰
  • 这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
  • 在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度

常用方法

方法名 描述
ThreadLocal() 创建ThreadLocal对象
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public T remove() 移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC
protected T initialValue() 返回当前线程局部变量的初始值

为什么要用ThreadLocal?

但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。

为了解决线程安全问题,JDK出现了很多技术手段,比如:使用synchronized或Lock,给访问公共资源的代码上锁,保证了代码的原子性。

但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。

因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。

它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

ThreadLocal的原理是什么?

set(T value)和get()方法


    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据threadLocal对象从map中获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取保存的数据
                T result = (T)e.value;
                return result;
            }
        }
        //初始化数据
        return setInitialValue();
    }

    /**
     * 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对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
    }
    
     /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        //获取要初始化的数据
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
        return value;
    }
    
    
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。

每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行get方法中,是从当前线程的threadLocals变量获取。

ThreadLoalMap


    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    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;
        ...
    }

ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。

在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。


ThreadLocal,第1张
图片.png

从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。

这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

为什么用ThreadLocal做key?

如果在你的应用中,一个线程中只使用了一个ThreadLocal对象,那么使用Thread做key也未尝不可。

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}  

但实际情况中,你的应用,一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用Thread做key不就有问题?

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
}

一个使用类,有两个共享变量,也就是说用了两个ThreadLocal成员变量的话。如果用线程id作为ThreadLocalMap的key,怎么区分哪个ThreadLocal成员变量呢?因此还是需要使用ThreadLocal作为Key来使用。每个ThreadLocal对象,都可以由threadLocalHashCode属性唯一区分的,每一个ThreadLocal对象都可以由这个对象的名字唯一区分。

ThreadLocal,第2张
图片.png
ThreadLocal,第3张
图片.png

Entry的key为什么设计成弱引用?

前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。


ThreadLocal,第4张
图片.png

那么,为什么要这样设计呢?

我们先来回忆一下四种引用:

  • 强引用:我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。

  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。

  • 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。

  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

假如key对ThreadLocal对象的弱引用,改为强引用。

ThreadLocal,第5张
图片.png

我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。

即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal对象还是强引用。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

当ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal对象的强引用的话那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。

ThreadLocal,第6张
图片.png

如何避免内存泄露?

ThreadLocal,第7张
图片.png

ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("占小狼");
    // 其它业务逻辑
} finally {
    localName.remove();
}

假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get、set或remove方法。

那么,Entry的value值一直都没被清空。

所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。

其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露。


ThreadLocal,第8张
图片.png

实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:

  • 一种就是,使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除

  • 另外一种方式就是:ThreadLocalMap的自动清除机制去清除过期Entry.(ThreadLocalMap的get(),set()时都会触发对过期Entry的清除)

ThreadLocal是如何解决hash冲突的呢?

我们看看getEntry是怎么做的:

 /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            //通过hash算法获取下标值
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果下标位置上的key正好是我们所需要寻找的key
            if (e != null && e.get() == key)
               //说明找到数据了,直接返回
                return e;
            else
                //说明出现hash冲突了,继续往后找
                return getEntryAfterMiss(key, i, e);
        }

再看看getEntryAfterMiss方法:


        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
           //判断Entry对象如果不为空,则一直循环
            while (e != null) {
                ThreadLocal<?> k = e.get();
               //如果当前Entry的key正好是我们所需要寻找的key
                if (k == key)
                   //说明这次真的找到数据了
                    return e;
                if (k == null)
                    //如果key为空,则清理脏数据
                    expungeStaleEntry(i);
                else
                   //如果还是没找到数据,则继续往后找
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

关键看看nextIndex方法:

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) i + 1 : 0);
        }

当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1。。。

ThreadLocal是如何定位数据的?

在ThreadLocal的get、set、remove方法中都有这样一行代码:

int i = key.threadLocalHashCode & (len-1);

通过key的hashCode值与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。

ThreadLocal从数组中找数据的过程大致是这样的:

1.通过key的hashCode取余计算出一个下标。

2.通过下标,在数组中定位具体Entry,如果key正好是我们所需要的key,说明找到了,则直接返回数据。

3.如果第2步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。

4.如果第3步中找key的正好是我们所需要的key,说明找到了,则直接返回数据。

5.如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为0的位置,继续从前往后找数据。

6.直到找到第一个Entry为空为止。

父子线程如何共享数据?

在Thread类中除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals

 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread类中的init方法


    /**
     * Initializes a Thread.
     *
     * @param g the Thread group
     * @param target the object whose run() method gets called
     * @param name the name of the new Thread
     * @param stackSize the desired stack size for the new thread, or
     *        zero to indicate that this parameter is to be ignored.
     * @param acc the AccessControlContext to inherit, or
     *            AccessController.getContext() if null
     * @param inheritThreadLocals if {@code true}, inherit initial values for
     *            inheritable thread-locals from the constructing thread
     */
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
        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();
    }



    /**
     * Factory method to create map of inherited thread locals.
     * Designed to be called only from Thread constructor.
     *
     * @param  parentMap the map associated with parent thread
     * @return a map containing the parent's inheritable bindings
     */
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
    }

可以发现,当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,数据从父线程那里来的。

public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子线程获取数据:" + threadLocal.get());
        }).start();
    }
}

父线程获取数据:6
子线程获取数据:null

public class ThreadLocalTest {

    public static void main(String[] args) {
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子线程获取数据:" + threadLocal.get());
        }).start();
    }
}

父线程获取数据:6
子线程获取数据:6

ThreadLocal有哪些用途?

1.在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。

2.在hiberate中管理session。

3.在JDK8之前,为了解决SimpleDateFormat的线程安全问题。

4.获取当前登录用户上下文。

5.临时保存权限数据。

6.使用MDC保存日志信息。

ThreadLocal的应用场景和使用注意点

ThreadLocal的很重要一个注意点,就是使用完,要手动调用remove()。

而ThreadLocal的应用场景主要有以下这几种:

使用日期工具类,当用到SimpleDateFormat,使用ThreadLocal保证线性安全

全局存储用户信息(用户信息存入ThreadLocal,那么当前线程在任何地方需要时,都可以使用)

保证同一个线程,获取的数据库连接Connection是同一个,使用ThreadLocal来解决线程安全的问题

使用MDC保存日志信息。

https://www.xamrdz.com/backend/3nb1939405.html

相关文章: