ThreadLocal principle exploration

foreword

In java concurrent programming, ThreadLocal can be seen in many places, and its role is to solve thread safety, that is, to achieve multi-thread isolation. Today, the author mainly analyzes and analyzes the learning process of Threadlocal.

1. What is ThreadLocal?

To understand the principle of this thing, you must first know what it is?

ThreadLocal is called a thread variable , which means that the variable filled in ThreadLocal belongs to the current thread, and the variable is isolated from other threads, which means that the variable is unique to the current thread.

ThreadLocal creates a copy , so each thread can access its own internal copy variable .

After looking at the basic concepts, we will see an example to make it clear:

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());
    }
}
复制代码
  • Let's take a look at the results:

It can be seen that the value of the main thread has not changed, while the thread t1 has been modified.

image.png

Second, ThreadLocal principle analysis

Since it has been seen through the above case, threadlocal can achieve isolation between multiple threads, so that the copy of the modified shared variable between each thread is only visible to itself. Solved the problem of sharing between threads

2.1 Method Analysis

Looking at the official set source code, you can find a very critical one: 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);
}
复制代码

It seems that before understanding the set method, we have to look at ThreadLocalMap

2.2ThreadLocalMap

ThreadLocalMap is actually a member variable that each thread maintains.

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;
        }
        //.....
    }
复制代码

It can be seen from the source code that there is also an array of entry objects that inherit weak references inside ThreadLocalMap.

The key of ThreadLocalMap is actually the weak reference of the ThreadLocal instance , and the value is the initial value of ThreadLocal or the value set by the thread .

image.png

2.3 set method

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设计成弱引用:也是为了防止内存泄漏的发生。

Guess you like

Origin juejin.im/post/7074127533603553294