ThreadLocal原理探寻

前言

在java并发编程中,很多地方都可以看到ThreadLocal,其作用是为了解决线程安全,即实现多线程隔离的。今天笔者主要来分析分析对Threadlocal的学习历程。

一、什么是ThreadLocal?

要搞懂这玩意的原理,得先知道它是个啥?

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量

看了基本的概念,我们看一个例子便就清楚:

public class Demo {
    //定义一个全局的ThreadLocal变量
    public static final ThreadLocal<String> STRING_THREAD_LOCAL = ThreadLocal.withInitial(() -> "HELLO");

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-->start:" +STRING_THREAD_LOCAL.get());

        Thread t1 = new Thread(() -> {
            String value =  STRING_THREAD_LOCAL.get();
            if(Objects.nonNull(value) && value.equals("HELLO")){
                STRING_THREAD_LOCAL.set("WORLD");
            }
            System.out.println(Thread.currentThread().getName() + "-->" + STRING_THREAD_LOCAL.get());
        },"t1");

        t1.start();
        t1.join();

        System.out.println(Thread.currentThread().getName() + "-->end:" +STRING_THREAD_LOCAL.get());
    }
}
复制代码
  • 我们看一下结果:

可以看出main线程的值并没有发生变化,而线程t1却已经是修改。

image.png

二、ThreadLocal原理分析

既然已经通过上述案例看出,threadlocal可以实现多线程之间的隔离,使每个线程之间修改共享变量变的副本仅自己可见。解决了线程之间共享的问题

2.1 方法解析

看一下官方的set源码,可以发现一个很关键的:ThreadLocalMap

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
复制代码

看来了解set方法之前,我们得先看看ThreadLocalMap

2.2ThreadLocalMap

ThreadLocalMap其实是每一个线程都会维护的一个成员变量。

static class ThreadLocalMap {


    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
        //.....
    }
复制代码

从源码可以看出ThreadLocalMap内部还有一个继承弱引用的entry对象数组。

ThreadLocalMap的key其实就是ThreadLocal实例的弱引用,而value便是ThreadLocal的初始值或者线程set进去的值

image.png

2.3 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);
}
复制代码

不难看出:set方法其实就是给当前线程中设置一个值,并且存放于ThreadLocalMap中。

createMap方法则是在getMap为空时,去对Map进行一个初始化并设置值

  • ThreadLocalMap未初始化
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化一个长度为16的table数组
    table = new Entry[INITIAL_CAPACITY];
    //通过路由算法,进行索引下标的计算
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
复制代码
  • ThreadLocalMap已初始化

如果已经初始化过,则直接调用ThreadLocalMap.set()保持即可。

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //根据索引下标遍历Entry数组
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //key相等,则覆盖原有的value
        if (k == key) {
            e.value = value;
            return;
        }
        //key为空,则用key、value覆盖。
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    //如果超过了阈值、进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //for循环,rehash();
        rehash();
}
复制代码

2.4ThreadLocalMap中的hash冲突

看到这的读者同学应该会发现,其实ThreadLocalMap的set方法与HashMap的put方法很像;但是同时也有一个问题:

ThreadLocalMap并没有链表结构,那么hash冲突又是怎么解决的呢?

  • 根据key计算出当前元素存储的索引下标i。
  • tab[i] != null
    • tab[i] 存放的key与当前key不一致,则继续向下寻找为空的位置
    • tab[i] 存放的key一致,但value不一致,则更新value
    • tab[i] 存放的key为null,则ThreadLocal实例可能已被被回收。
  • tab[i] == null,则直接将key、value存储即可。

其实说白了,就是线性for循环,遍历为空的位置解决hash冲突。

2.5替换并清理replaceStaleEntry方法

当key==null的时候,使用replaceStaleEntry方法将当前的key、value去覆盖空key和value

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

    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码

三、其他问题

3.1 内存泄漏

内存泄漏:简单说就是:开辟的空间在使用完成后未释放,导致内存一直占据。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

Threadlocal把自己的弱引用实例对象当作key存在了ThreadLocalMap中,这样就导致在没有外部强引用的情况下,key会被回收,而如果创建ThreadLocal的线程一直持续运行,Entry对象中的value将可能产生内存泄漏(一直无法被回收)

  • 解决办法:

在使用Threadlocal的时候,在代码最后去使用remove()方法即可。

3.2弱引用

弱引用:即非必需内存,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示

image.png

其实key设计成弱引用:也是为了防止内存泄漏的发生。

猜你喜欢

转载自juejin.im/post/7074127533603553294