深入理解ThreadLocal

线程间数据共享和隔离的问题

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性

什么是ThreadLocal

ThreadLocal是JDK包提供的,ThreadLocal 提供了一种变量与线程绑定的机制,它提供线程本地变量,比如在线程A中设置了一个变量ThreadLocalA 其中存储的值为ABC 在线程B中想拿到ThreadLocalA 中存储的ABC是拿不到的返回结果是空。
如下图所示:
每个线程的值只能自己线程访问到 其他线程拿不到,这样就实现了隔离
深入理解ThreadLocal_第1张图片

ThreadLocal在多线程环境下的作用

ThreadLocal 在多线程环境下的作用是提供一种机制,允许每个线程在共享的资源上拥有自己独立的变量副本。这意味着每个线程可以访问一个线程局部的变量,而不必担心线程间的数据干扰或竞争条件。ThreadLocal 有以下主要作用:
线程数据隔离:ThreadLocal 允许每个线程在访问共享资源时,拥有其自己的变量副本。这确保了线程间的数据隔离,不同线程之间的变量互不干扰。
线程上下文管理:ThreadLocal 经常用于存储线程上下文信息,如用户会话、数据库连接、事务状态等。这使得线程可以在不同部分之间传递数据,而不必通过参数传递。
性能提升:ThreadLocal 变量在多线程环境中提供了更快的访问速度,因为每个线程都可以直接访问自己的副本,而无需争夺锁。
减少锁的使用:通过使用 ThreadLocal,可以减少对全局锁或同步机制的依赖。这有助于降低多线程程序中的锁竞争,提高并发性能。

ThreadLocal的使用

在这个示例中,我们创建了一个 ThreadLocal 变量 threadLocal,并使用 withInitial 方法初始化为默认值 “Guest”。然后,我们创建了两个线程,每个线程分别设置了不同的用户名。在主线程中,我们也尝试获取用户名。由于 ThreadLocal 的特性,每个线程的变量副本都是独立的,因此线程之间不会互相干扰,最后输出的结果将显示每个线程所设置的用户名。

public class ThreadLocalExample {

    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Guest");

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            threadLocal.set("User1");
            System.out.println("Thread 1: User is " + threadLocal.get());
            threadLocal.remove(); // 移除 ThreadLocal 变量
        });

        Thread thread2 = new Thread(() -> {
            threadLocal.set("User2");
            System.out.println("Thread 2: User is " + threadLocal.get());
            threadLocal.remove(); // 移除 ThreadLocal 变量
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main Thread: User is " + threadLocal.get());

        // 主线程在完成后也应该移除 ThreadLocal 变量
        threadLocal.remove();
    }
}

ThreadLocal的应用场景

ThreadLocal 主要用于解决多线程环境下的线程局部变量需求以下是一些 ThreadLocal 的应用场景:
1.线程安全的对象实例:当多个线程需要访问一个对象的不同实例,每个线程可以通过 ThreadLocal 获取自己的实例,避免竞态条件。
2.数据库连接管理:在多线程应用中,每个线程通常需要一个数据库连接。使用 ThreadLocal 可以确保每个线程都有自己的数据库连接,避免了连接共享和竞态条件。
3.会话管理:Web应用中,ThreadLocal 可用于存储用户会话信息,确保每个用户的会话数据不会被混淆。
4.用户身份验证:在需要验证用户身份的应用中,ThreadLocal 可以用于存储用户的认证信息,以便后续的请求可以访问该信息。

ThreadLocal原理

Java中的引用类型

强引用:被应用到的类型不允许被回收
软件用(SoftReference):垃圾回收器会根据内存酌情考虑,只有在内存不足即将OOM的时候
(最后一次FullGC)如果被引用的对象只有SoftReference指向的引用,才会回收。
弱引用(WeakReference):当发生GC时 如果当前对象时弱引用类型,则会被GC回收掉
虚引用(PhantomReference):他是一种特殊的引用类型,不能通过虚引用获取到其关联的对象,但当GC时如果其引用的对象被回收

ThreadLocal结构

深入理解ThreadLocal_第2张图片

深入理解ThreadLocal_第3张图片

    public ThreadLocal() {
    }

ThreadLocal类本身比较简单其支持泛型 本身没什么,其set与get方法上复杂的处理最终都到了内部类类ThreadLocalMap上 ,ThreadLocalMap中静态内部类Entry又继承了WeakReference(弱引用)

ThreadLocalMap结构

从ThreadLocal的get、set、remove方法分析可以看到,最终这些操作都是在ThreadLocalMap上完成

static class ThreadLocalMap {
       //ThreadLocalMap中的Entry持有ThreadLocal的弱引用。
        static class Entry extends WeakReference<ThreadLocal<?>> {
            // ThreadLocal上关联的值
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
         // map的初始容量
         private static final int INITIAL_CAPACITY = 16;

         // Entry数组
        private Entry[] table;

       // Entry元素个数
        private int size = 0;

       // 阈值,用于扩容降低开放寻址时的冲突
        private int threshold; // Default to 0
        }

构造函数

//创建ThreadLocalMap并在其上绑定第一个线程局部变量
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
  	//取模获取引用ThreadLocal的Entry的下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
  	//设置扩容的阈值
    setThreshold(INITIAL_CAPACITY);
}

Set操作原理解析

    public void set(T value) {
        //拿到线程
        Thread t = Thread.currentThread();
        //根据线程信息拿到ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        //如果ThreadLocalMap 不为空则set数据 key ThreadLocal的弱引用的实例 value是这次set的值
        if (map != null)
            map.set(this, value);
        else
        //如果为空则创建ThreadLocalMap 
            createMap(t, value);
    }

获取ThreadLocalMap

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

创建ThreadLocalMap

     //t是当前线程 t.threadLocals   表示当前线程的ThreadLocalMap    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

set设置值

set方法将当前线程的副本写入了一个ThreadLocalMap, map的key是当前的ThreadLocal对象。
每个 Thread 对象维护了一个 ThreadLocalMap 类型的 threadLocals 字段。
ThreadLocalMap 的 key 是 ThreadLocal 对象, 值则是变量的副本, 因此允许一个线程绑定多个 ThreadLocal 对象。

        private void set(ThreadLocal<?> key, Object value) {
            //table 是这个线程存储的数据 这里为什么用数组是因为 一个Thread最多只有一个ThreadLocalMap,ThreadLocalMap底层是一个Entry数组,但是一个Thread可以有多个ThreadLocal,一个ThreadLocal对应一个变量数据,封装成Entry存到ThreadLocalMap中,所以就有多个Entry
            Entry[] tab = table;
            //table的长度 
            int len = tab.length;
            //通过hash 来计算下标
            int i = key.threadLocalHashCode & (len-1);
             //从i开始遍历 直到最后一个
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                 //获取 ThreadLocal
                ThreadLocal<?> k = e.get();
                //如果key相等直接覆盖 相当于ThreadLocal实例相同 直接覆盖value
                if (k == key) {
                    e.value = value;
                    return;
                }
                 //如果key 为null 用新key  value覆盖
                if (k == null) {
                   //覆盖 并清理 key== null的数据
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //未找到之前引用ThreadLocal的Entry,创建Entry并放入Entry数组
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清理槽位失败且Entry数组长度超过阈值,重新rehash对Entry数组扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

首先计算哈希槽的位置, 此时可能有3种情况:
1.哈希槽为空, 直接将新 Entry 填入槽中; 此外调用 cleanSomeSlots 搜索并清理 GC 造成的空洞; 此外检查 Entry 数量是否到达阈值, 必要时调用 rehash 方法进行扩容。
2.哈希槽中为当前 ThreadLocal, 直接进行替换
3.哈希槽中为空 Entry, 说明原有ThreadLocal 被 GC 回收, 调用 replaceStaleEntry 将其替换。

replaceStaleEntry

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            //向前扫描 查找最前面无效的key 
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    //循环遍历 定位最前面无效的key
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            //从i开始向后查找 遍历数组的最后一个Entry
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                //找到匹配的key之后
                if (k == key) {
                    e.value = value;
                    //更新 value值
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //如果最前面无效的key 和当前的key相同 责任将 i作为起点开始清理
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                //如果当前的slot已经无效 并且在向前扫描的过程中没有无效的slot 则更新slotToExpunge当前位置
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            //如果key对应的value不存在 则直接放一个新的Entry
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            //如果有任何无效的slot 则做一次清理 
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

1.向前寻找空的 Entry 将其位置写入 slotToExpunge, 这是为了清理不必继续关注
2.向后进行寻找若是找到与传入的 key 相同 Entry 则更新 Entry 的内容并将其移动到 staleSlot, 然后调用 cleanSomeSlots 进行清理
3.若最终没有找到 key 相同的Entry, 则在 staleSlot 处写入一个新的 Entry, 调用 cleanSomeSlots 进行清理

get方法

  public T get() {
        Thread t = Thread.currentThread();
        //根据当前线程拿到一个 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据当前的ThreadLocal实例的软引用 拿value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocal 的 get 方法在首次获取变量的值时,如果当前线程没有在此 ThreadLocal 对象上设置过值,它会调用 initialValue() 方法来设置初始值。这个机制保证了线程局部变量的初始化。

这种方式是为了确保线程局部变量在每个线程中都有一个合理的初始值。不同线程之间可能有不同的初始值需求,因此在 initialValue() 方法中可以根据线程的需求为变量设置不同的初始值。

如果不设置为null,在某些情况下会引发空指针的问题。

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

remove

     public void remove() {
         //根据线程信息拿到ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         //移除数据
         if (m != null)
             m.remove(this);
     }
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //hash得到下标
            int i = key.threadLocalHashCode & (len-1);
            //若是第一次没有命中,就循环直到null,在此过程中也会调用「expungeStaleEntry」清除空key节点
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

ThreadLocal 内存泄漏问题

为什么会有内存泄漏问题

在 ThreadLocalMap 中的 Entry 的 key 是对 ThreadLocal 的 WeakReference 弱引用,而 value 是强引用。
注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个弱引用

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

上面我们看了ThreadLocal的源码,我们知道 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。所以会导致内存泄漏问题

为什么强调使用完ThreadLocal之后要调用remove

即使ThreadLocal中有那么多时间点可以回收过期的entry,但是由于ThreadLocal实例通常是类中的私有静态字段,也就是说ThreadLocal总是会有强引用指向,上面说的那些方法根本不能处理这些被类持有的ThreadLocal对象。
所以在使用完ThreadLocal之后,最好手动调用remove方法,断开entry对value的引用以及threadlocalMap对entry的引用,释放entry

ThreadLocal如何解决哈希冲突

与其他的HashMap不一样,ThreadLocal是通过开放寻址法中的线性探测法来解决hash冲突的

主要看set方法中

for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) 

在for循环的三句语句里面,描述了三件事情:

找到下标为i的节点e
判断节点e是否为空
下一个循环开始前,把i变成新的下标nextIndex,去下标为新的下标的节点e

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

nextIndex方法是在len的范围内返回后一个下标,如果已经是最后一个下标,那么返回0
另外一个方法,prevIndex同理,是找前一个下标
所以set方法在计算为下标之后,还是要找到下一个空的槽位,再把新的节点放到这个空槽位上
这就是典型的线性探测法解决hash冲突

ThreadLocal怎么清理键值对

探测式清理

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            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;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

1.遍历散列数组,从开始位置(hash得到的位置)向后探测清理过期数据,如果遇到过期数据,则置为null。
2.如果碰到的是未过期的数据,则将此数据rehash,然后重新在 table 数组中定位。
3.如果定位的位置已经存在数据,则往后顺延,直到遇到没有数据的位置。
4.说白了就是:从当前节点开始遍历数组,将key等于null的entry置为null,key不等于null则rehash重新分配位置,若重新分配上的位置有元素则往后顺延。

启发式清理

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

1.根据源码可以看出,启动式清理会从传入的下标 i 处,向后遍历。如果发现过期的Entry则再次触发探测式清理,并重置 n。
2.这个n是用来控制 do while 循环的跳出条件。如果遍历过程中,连续 m 次没有发现过期的Entry,就可以认为数组中已经没有过期Entry了。
3.这个 m 的计算是 n >>>= 1 ,可以理解为是数组长度的2的几次幂。
例如:数组长度是16,那么2^4=16,也就是连续4次没有过期Entry。
4.说白了就是: 从当前节点开始,进行do-while循环检查清理过期key,结束条件是连续n次未发现过期key就跳出循环,n是经过位运算计算得出的,可以简单理解为数组长度的2的多少次幂次。

你可能感兴趣的:(并发编程,java,开发语言)