Java多线程–什么是ThreadLocal

1. ThreadLocal是什么

首先看看官方文档给出的对ThreadLocal定义:

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

官方文档给的定义相当明确:ThreadLocal的作用就是来提供线程内的局部变量,使得不同的线程之间不会相互干扰。这么做主要的好处是可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

2. ThreadLocal的基本使用

方法声明

描述

ThreadLocal()

创建ThreadLocal对象

public void set(T value)

设置当前线程绑定的局部变量

public T get()

获取当前线程绑定的局部变量

public void remove()

移除当前线程绑定的局部变量

public class ThreadLocalDemo {

    // 变量
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                threadLocalDemo.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println("-----------------------------------------");
                System.out.println(Thread.currentThread().getName() + "\t  " + threadLocalDemo.getContent());
            }, String.valueOf(i)).start();
        }
    }

}

运行结果:

-----------------------------------------
1	  0的数据
-----------------------------------------
0	  0的数据
-----------------------------------------
3	  3的数据
-----------------------------------------
2	  2的数据
-----------------------------------------
4	  4的数据

通过代码和结果可以观察到,出现了数据不一致问题。即线程之间调用了其他线程的数据,并不是调用自己的数据。

但是,当使用ThreadLocal就可以很好的解决:

public class ThreadLocalDemo {

    // 变量
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                threadLocal.set(Thread.currentThread().getName() + "的数据");
                System.out.println("-----------------------------------------");
                System.out.println(Thread.currentThread().getName() + "\t  " + threadLocal.get());
            }, String.valueOf(i)).start();
        }
    }

}

运行结果:

-----------------------------------------
0	  0的数据
-----------------------------------------
1	  1的数据
-----------------------------------------
2	  2的数据
-----------------------------------------
3	  3的数据
-----------------------------------------
4	  4的数据

使得当前线程只能调用自己线程中存储的数据,线程与线程之间数据相互隔离开了。

3. ThreadLocal与Synchronized的区别

ThreadLocal模式与Synchronized关键字都用于处理多线程并发访问变量的问题,但是两者的处理问题的角度和思路不同。

Synchronized

ThreadLocal

原理

同步机制采用 以空间换时间 的方式,只提供了一份变量,让不同的线程排队访问

ThreadLocal采用以空间换时间的概念,为每个线程都提供一份变量副本,从而实现同时访问而互不干扰

侧重点

多个线程之间访问资源的同步

多线程中让每个线程之间的数据相互隔离

4. ThreadLocal的应用场景

ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接Session管理等。

例如当使用SimpleDateFormat,当调用parse()方法时,内部有一个Calendar对象。其中会先调用Calendar.clear()方法,其次再调用Calendar.add()方法。因为没有加锁,那么可想而知会有这么一种情况产生:一个线程执行了Calendar.clear()方法,而另一个线程执行了Calendar.add()方法。很明显,这会导致日期数据的不一致。这时,就可以使用ThreadLocal来解决这类问题:使用线程池加上ThreadLocal包装SimpleDateFormat,再调用initialValue让每个线程有一个SimpleDateFormat的副本,从而解决了线程安全的问题,也提高了性能。

总而言之,使用ThreadLocal能够突出两个优势:

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

5. ThreadLocal的内部结构

首先需要区分的一点是,在JDK8以前和JDK8之后ThreadLocal的设计是不同的。

在早期JDK中,ThreadLocal的设计和我们所认知的一样:每个TreadLocal会创建一个Map集合,当需要存数据时,将线程Thread作为key,将数据作为value。因为每个线程不同,因此只能取到自己对应的value,从而实现线程之间的局部变量隔离的鲜果。

但是在JDK8之后(包括JDK8)对ThreadLocal的设计就有所改变:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value 才是真正要存储的数据。

java多线程 get方法会阻塞 java多线程thread_ThreadLocal

这么设计有什么好处?

  • 每个Map存储的Entry数量变少,因为原来的Entry数量是由Thread决定,而现在是由ThreadLocal决定的。
  • 真实开发中,Thread的数量远远大于ThreadLocal的数量。
  • 当Thread销毁的时候,ThreadLocalMap也会随之销毁,因为ThreadLocal是存放在Thread中的,随着Thread销毁而消失,能降低开销。

那么问题来了,为什么只用到了数组,没看到链表,它是如何解决Hash冲突的呢?

private void set(ThreadLocal<?> key, Object value) {
           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();
        }

从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)

然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

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

如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。

java多线程 get方法会阻塞 java多线程thread_java_02

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

6. ThreadLocal核心方法源码

ThreadLocal对外暴露的方法有以下4个

方法声明

描述

protected T initialValue()

返回当前线程局部变量的初始值

public void set(T value)

返回当前线程绑定的局部变量

public T get()

获取当前线程绑定的局部变量

public void remove()

移除当前线程绑定的局部变量

首先看一下set()方法

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 通过当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
        // 如果存在,直接存入值,key为ThreadLocal,value为数据
        map.set(this, value);
    else
        // 如果不存在则创建ThreadLocalMap,并将数据存入
        createMap(t, value);
}

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

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

get()方法

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 通过当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null) {
        // 通过当前线程获取存储value的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果Entry不为空
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取value值返回
            T result = (T)e.value;
            return result;
        }
    }
    // 若map不存在或Entry为空均执行此代码
    return setInitialValue();
}

private T setInitialValue() {
    // 获取初始化的值,默认为null,可以被子类重写
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 通过当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
        // 如果存在,直接存入值,key为ThreadLocal,value为数据
        map.set(this, value);
    else
        // 如果不存在则创建ThreadLocalMap,并将数据存入
        createMap(t, value);
    // 返回值r
    return value;
}

remove()方法

public void remove() {
    // 通过当前线程获取ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 判断map是否存在
    if (m != null)
        // 存在则以当前ThreadLocal为key删除Entry
        m.remove(this);
}

initialValue()方法

/**
* 可以通过子类重写此方法,作用是获取ThreadLocal线程局部变量除null以外的第一个初始值
*/
protected T initialValue() {
    return null;
}

7. 为什么ThreadLocalMap当中的key要用弱引用指向ThreadLocal

首先需要明白Java当中的四种引用,即强引用、软引用、弱引用、虚引用

理解Java的强引用、软引用、弱引用和虚引用

假设使用强引用,此时的内存结构图如下,会造成什么问题:

java多线程 get方法会阻塞 java多线程thread_ThreadLocal_03

当ThreadLocalRef使用完被回收,那么ThreadLocalRef指向ThreadLocal的强引用就会断开。

但是由于ThreadLocalMap的Entry强引用了ThreadLocal,造成threadLocal无法被回收。

因此当CurrentThreadRef依然运行的情况下,除非手动删除这个key引用,不然始终Entry得不到回收。

至此就会产生内存泄漏的问题。

那么使用弱引用姐可以解决这个问题了吗?

java多线程 get方法会阻塞 java多线程thread_数据_04

当ThreadLocalRef使用完被回收,那么ThreadLocalRef指向ThreadLocal的强引用就会断开。

由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadloca就可以顺利被gc回收,此时Entry中的key=null。

但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry-> value,value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。

即ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。

首先总结下避免内存泄漏的两种方式

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry。
  2. 使用完ThreadLocal,当前Thread也随之运行结束。

第一种方法简单易理解,也是我们经常这么使用的,而第二种方法由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长,因此不好控制。

那使用弱引的作用到底是什么呢?

其实在ThreadLocalMap中的 set / getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为nul的。

这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

8. 小结

通过本篇文章,理解了什么是ThreadLocal,它的作用以及使用场景。

讨论了它的内部结构,并对其get、set、remove、initialValue方法进行了分析。

面对经常出现的面试问题:Hash冲突、为什么使用弱引用等做了详细的解答。

总结ThreadLocal的优点为以下几个方面:

  1. 线程隔离:每个线程的变量都是独立的,不会互相影响。
  2. 传递数据:通过ThreadLocal在同一线程,不同组件之间传递公共变量。