对threadlocal了解多少?

简介: 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程绑定自己的值,可以将 ThreadLocal 类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

写在前面

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程绑定自己的值,可以将 ThreadLocal 类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

什么是 ThreadLocal?

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。 ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

下图可以增强理解:

网络异常,图片无法展示
|

线程本地存储:在线程生命周期内作为上下文存储共享对象

这里的上下文指的是线程存活期间内,调用多个方法,各个方法之间共享的“上下文空间”。

我们知道,每个线程对应着它的线程栈,线程栈由栈帧组成,用这套数据结构来跟踪线程的方法调用。 每个栈帧里边存放着一个方法内的局部变量,进入一个方法则压入一个栈帧,从一个方法返回则弹出一个栈帧。

考虑一个问题:如果想在一个thread生命周期内,在多个栈帧或者说多个方法之间共享对象呢?

用局部变量显然不行,其作用域只在方法里或者栈帧内,每个栈帧维护自己的局部变量表,另一个栈帧不认识。thread里边弄个静态变量当然可以,但是这是类级别的、就对别的thread实例可见,要考虑并发问题了。

想来想去,在Thread类的内部的成员变量中搞个Map来存放这些值是个不错的主意:作用域是每个thread实例,能够被线程生命周期内各个方法调用所共享。我想这就是ThradLocalMap和ThreadLocal的由来。

示例

使用ThreadLocal在线程的多个方法调用之间共享参数

public class WorkerThread implements Runnable{
    public static ThreadLocal<Map> paramA = new ThreadLocal<>();
    private CountDownLatch latch;
    public WorkerThread(CountDownLatch latch) {
        this.latch = latch;
    }
    @Override
    public void run() {
        ThreadLocalTest tlt = new ThreadLocalTest();
        tlt.initParamA();
        tlt.useParamA();
        latch.countDown();
    }
}
复制代码
public class ThreadLocalTest {
    private static Logger logger = LoggerFactory.getLogger(ThreadLocalTest.class);
    private static int N = 100;
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(N);
        for(int i=0; i<N; i++) {
            new Thread(new WorkerThread(latch)).start();
        }
        try {
            latch.await(); //等所有WorkerThread线程都执行完
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void initParamA() {
        HashMap<String,String> map = new HashMap<>();
        map.put("name", Thread.currentThread().getName());
        WorkerThread.paramA.set(map);
    }
    public void useParamA() {
        String name = (String)WorkerThread.paramA.get().get("name");
        String threadName = Thread.currentThread().getName();
        logger.info("当前线程名{},通过 ThreadLocal传递的线程名{}", threadName, name);
        if(!threadName.equals(name))
            logger.error("出现并发问题");
    }
}
复制代码

这个程序的意图是这样的:worker线程会去调用initParamA和useParamA两个方法,使用ThreadLocal在两者之间传递一个参数,这里传递的是一个Map,里边放了当前worker线程的线程名。最后会通过比较Thread.currentThread().getName()与ThreadLocal里的线程名是否相等来证明ThreadLocal的线程私有性。 运行结果是不会打印出"出现并发问题"。

实现原理

每一个线程都有一个自己的 ThreadLocalMap 实例,其中每个键值对的键是一个 ThreadLocal 对象,值为每个线程专属的值,我们可以结合下图来理解。

网络异常,图片无法展示
|

ThreadLocal

我们的ThreadLocal是找了一个类声明了一个静态成员变量

public static ThreadLocal<Map> paramA = new ThreadLocal<>();
复制代码

然后分别是在不同的方法里调用了set()和get()方法来放和取我们的参数Map。 我们来看一下源码分析一下:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
复制代码

由以上两个方法源码可以知道,实际上ThreadLocal的set和get是把自己ThreadLocal对象作为key,和我们的参数作为value,组成k-v对,放在当前Thread的ThreadLocalMap这个Map里的。每个线程都有自己的ThreadLocalMap,这是定义在Thread.java里的。

ThreadLocalMap

ThreadLocal<Map> paramA = new ThreadLocal<>();
复制代码

实例化了一个ThreadLocal,paramA,当调用paramA.set(Map)时,这个Map最终存放在当前线程的ThreadLocalMap里。ThreadLocalMap是每个线程Thread实例内部都有的一个存储结构,里边实际上是个Entry数组,每个Entry由ThreadLocal和Map这样一个k-v对来实例化。

也就是说ThreadLocal和Map这样一个k-v在ThreadLocalMap中存放时,是封装成Entry存放的,而Entry是存放在ThreadLocalMap的private Entry[] table这个数组中的,存放时先根据key的hash找到对应的Entry数组下标,然后找到对应的Entry,如果Entry的key等于当前这个ThreadLocal,那么就用我们的Map替换Entry的value,否则直接new Entry然后放到Entry数组的下标位置。

ThreadLocalMap的set方法:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {
    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.
    Entry[] tab = table;
    int len = tab.length;
    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;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
复制代码

内存泄漏

内存泄漏和内存溢出是不同的:

内存泄漏:一块内存一直被占用着,无法另作他用,就好像丢了一样,被称作内存泄漏。 内存溢出:内存用完了,递归时很有可能出现内存溢出问题。

内存泄漏的多的话会加大内存溢出的概率。

网络异常,图片无法展示
|

上图中的 tl 是一个强引用。

为什么 ThreadLocalMap 中的 key 指向 ThreadLocal 对象时使用弱引用?

在线程结束后,tl 引用会销毁,假如 key 使用强引用的话会导致在垃圾回收时无法回收 ThreadLocal 对象,会产生内存泄漏(此时导致内存泄漏的是 ThreadLocal 对象及其对应的键值对)。

就算使用弱引用,还有可能会导致内存泄漏(此时导致内存泄漏的是 ThreadLocalMap 中的 value),ThreadLocal 被回收后,key 会变为 null,会导致整个 value 再也无法被访问到,value 是强引用,会导致它指向的对象无法被回收,从而导致内存泄漏。

为了避免上述两种内存泄漏的发生,对于那些不再使用的 ThreadLocal 我们一定要调用 remove() 方法移除。

相关文章
|
4月前
|
存储 Java
ThreadLocal应用及理解
ThreadLocal应用及理解
45 10
|
5月前
|
Java
ThreadLocal 场景题
ThreadLocal 场景题
35 1
|
6月前
|
存储 Java
ThreadLocal 有什么用
ThreadLocal 有什么用
52 0
|
存储 算法 安全
深入详解ThreadLocal
在我们日常的并发编程中,有一种神奇的机制在静悄悄地为我们解决着各种看似棘手的问题,它就是 ThreadLocal 。
21513 9
深入详解ThreadLocal
|
存储 SQL Java
ThreadLocal的其他应用
request对象跟PageHelper
100 0
|
存储 Java
|
存储 Java 数据库连接
对ThreadLocal的一点了解
ThreadLocal是线程变量,它为每个线程提供单独的存储空间。其主要作用是做线程间的数据隔离,也可以用于在同一个线程间方便地进行数据共享。(对于多线程资源共享,加锁机制采用“时间换空间”,ThreadLocal采用“空间换时间”)
126 0
|
存储 Java
ThreadLocal相关使用
ThreadLocal相关使用
192 0
ThreadLocal相关使用
|
存储 Java
ThreadLocal理解
ThreadLocal理解
285 0
ThreadLocal理解