原文链接:ThreadLocal原理及使用场景_小机double的博客-CSDN博客_threadlocal

 

1)什么是ThreadLocal:

​ ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。

所谓的共享变量指的是在堆中的实例、静态属性和数组;

在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。

常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock),原理是同步机制,只此一份共享变量,让不同线程排队访问。但是这种方式对性能的耗费比较大。

在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,即为每一个线程都提供一份共享变量的副本,从而实现互相不干扰。这样就可以做到线程之间对于共享变量的隔离问题​​​​​​

  –  依赖于ThreadLocal本身的特性,对于需要进行线程隔离的变量可以使用ThreadLocal进行封装

·

2)ThreadLocal的使用及原理

1.1 使用

一般都会将ThreadLocal声明成一个静态字段,同时初始化如下:

static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

其中Object就是原本堆中共享变量的数据。

例如,有个User对象作为一个共享变量,需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下:

public class Test {
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}

1.2、ThreadLocal常用的方法

  • set(T value):设置线程本地变量的内容。
  • get():获取线程本地变量的内容。
  • remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
public class Test {
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();
    
    //传过来的user为共享变量,将其生成副本存到线程本地内存中
    public void m1(User user) {
        threadLocal.set(user);
    }
    
    public void m2() {
        //获取线程本地的共享变量副本
        User user = threadLocal.get();

        // 使用
        //...

        // 使用完清除
        threadLocal.remove();
    }
}

·

1.3 ThreadLocal设计

JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型,其具体过程如下:

  • 1、每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);
  • 2、Map里面存储ThreadLocal对象(key)和线程的变量副本(value);
  • 3、Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
  • 4、对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。

JDK8之后这样设计的好处在于:

  • 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。
  • 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。

·

1.4 原理-ThreadLocalMap

​ 那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?

首先,在Java中的线程是什么呢?是的,就是一个Thread类的实例对象!而一个实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存ThreadLocal线程本地变量作为一个Thread类的成员字段,这个成员字段就是:

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

​ 是一个在ThreadLocal中定义的Map对象,保存了该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    // key为一个ThreadLocal对象,v就是我们要在线程之间隔离的对象
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

​ ThreadLocalMap和Entry都在ThreadLocal中定义。

1.5、ThreadLocal::set方法的原理

set方法的源码如下:

//value:要存储在此线程本地内存的共享变量副本中的值
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的threadLocals字段
    ThreadLocalMap map = getMap(t);
    // 判断线程的threadLocals是否初始化了
    if (map != null) {
        //创建共享变量的副本存入到当前线程的本地内存中
        map.set(this, value);
    } else {
        // 没有则创建一个ThreadLocalMap对象进行初始化
        createMap(t, value);
    }
}

createMap方法的源码如下:

void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

map.set方法的源码如下:

/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
    // map中就是使用Entry[]数据保留所有的entry实例
    Entry[] tab = table;
    int len = tab.length;
    // 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
    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;
        }
    }
    //新建一个entry,存入到本地
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 扩容逻辑
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

1.6、Entry的构造:是一个弱引用对象:

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

    Entry(ThreadLocal<?> k, Object v) {
        //对key进行了弱引用的修饰
         super(k);
         value = v;
    }
 }

 

 ·

1.7、Thread::get方法的原理

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 获取ThreadLocal对应保留在Map中的Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取ThreadLocal对象对应的值
            T result = (T)e.value;
            return result;
        }
    }
    // map还没有初始化时创建map对象,并设置null,同时返回null
    return setInitialValue();
}

· 

1.8、ThreadLocal::remove()方法原理

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 键在直接移除
    if (m != null) {
        m.remove(this);
    }
}

·

1.9、ThreadLocalMap的类结构体系如下:

image-20201227212035680

2.0、使用ThreadLocal的好处

保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

·


二、ThreadLocal内存泄露问题

ThreadLocal的内存泄露问题一般考虑和Entry对象有关,

在上面的Entry定义可以看出ThreadLocal::Entry被弱引用所修饰。

JVM会将弱引用修饰的对象在下次垃圾回收中清除掉。这样就可以实现ThreadLocal的生命周期和线程的生命周期解绑。

使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。

因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象

·

四、总结

ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离和当前线程全局共享

当key的引用被回收之后,key变为了null,引起内存泄露——Entry中的Value对象无法被回收,因此要确保每次使用完之后都remove掉Entry!
 


ThreadLocalMap的结构图:

 ·

1、什么是ThreadLocal:

ThreadLocal是一个用来解决线程安全性问题的一个工具,它相当于让每个线程都开辟了一块内存空间,用来存储共享变量的一个副本,然后每个线程只需要去访问和操作自己的共享变量的一个副本就可以了。从而去避免多线程竞争同一个共享资源。

2、ThreadLocal如何解决线程安全问题:

每个线程都有一个成员变量,叫ThreadLocalMap,当线程访问ThreadLocal修饰的共享变量的时候,这个线程就会在自己的成员变量ThreadLocalMap里边去保存一份数据副本,key是指向这个共享变量的一个引用,并且是一个弱引用的关系,而value保存的是共享变量的一个副本,因为每个线程都持有一份数据副本,所以线程之间就不存在对于共享数据的一个并发操作,所以就解决了线程安全问题。

3、为什么ThreadLocal会存在内存泄漏问题:

ThreadLocal里边的成员变量ThreadLocalMap里边的key是指向ThreadLocal这个共享变量的一个引用,并且是一个弱引用,一旦被回收,key就变成了一个null,就会导致这块内存永远无法被访问,从而造成了内存泄漏的问题。

换句话说:ThreadLocalMap里边的key是一个弱引用,从而导致key可能变为null,造成这块内存永远无法被访问,出现内存泄漏的问题。

有人会问:如果线程都被回收了,那么线程里边的成员变量就会被回收,那就不会存在内存泄露的问题了呀?

这样理解是没问题的,但是在实际开发中,我们一般是使用线程池,而线程池本身是一个重复利用的,所以还是会存在内存泄漏的这样一个问题

4、ThreadLocal使用弱引用的原因如下:

ThreadLocal的设计者为了避免内存泄漏的问题,当我们在进行数据的读写的时候——在ThreadLocalMap的set/getEntry中,ThreadLocal默认会去尝试做一些清理的动作,会在Entry数组里边对key进行判断,如果key为null,那么value也会被设置为null,

但是它仍然不能完全避免内存泄漏的问题。所以我们的办法还是:使用完毕之后需要手动的去remove一下去移除当前的数据

5、把ThreadLocal声明为全局变量的解决方式:

第二个办法是把ThreadLocal声明为全局变量,使得它无法被GC回收,但是这种方式虽然不会造成key为null的现象,

但是如果后续线程不再继续访问这个key,也还是会导致这块内存一直释放不掉,还是会造成内存溢出的问题。所以最好的方式还是在使用完之后调用remove方法去移除掉这个数据