ThreadLocal用于创建线程局部变量,使用ThreadLocal创建的变量在每个线程中有独立的副本。什么叫线程局部变量?我用方法的局部变量做类比,在方法的第一行定义一个变量,则在方法代码块内的任何一行代码都能使用这个变量。类似的,使用ThreadLocal创建的线程局部变量,只要线程没终止,线程局部变量就存在。一个常见的用法:Servlet给每个http请求分配一个单独的线程处理,一个http请求对应一个线程,可以将request信息使用ThreadLocal存储,Service、Dao的方法不必都定义Request入参也能获取到request信息。
initialValue()方法
为了更好地讲解ThreadLocal的源码,我先讲解initialValue()方法,此方法的作用是返回当前线程的线程局部变量的初值。
class MyRandom{
/**
* ThreadLocal设置初始值。每次执行MyRandom.random都会执行initialValue()。这类似一种延迟加载的功能,在调用方法的时候才创建对象、返回对象
*/
public static ThreadLocal<Integer> random = new ThreadLocal(){
@Override
protected Integer initialValue() {
return new Random().nextInt(10);
}
};
}
class MyRandomTest{
public static void main(String[] args) {
Integer num = MyRandom.random.get();
System.out.println(num);
}
}
在initialValue()、MyRandom.random.get()代码处打断点,查看方法调用栈。
1、MyRandom.random.get()调用ThreadLocal的setInitialValue()方法。
2、setInitialValue()调用initialValue()方法。
ThreadLocal的常见用法
ThreadLocal常用的方法:
set(T value) 设置线程局部变量值
get() 获取线程局部变量值
remove() 删除线程局部变量值
在Spring应用中可使用ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes()获取当前请求的request。这是ThreadLocal的一种经典用法,同一个线程的多个方法之间使用线程局部变量承载数据,下面是一段模仿使用ThreadLocal保存request,然后在Controller、Service中使用RequestContextHolder获取request信息的代码。
@Data
class Request{
private String data;
public Request(String data) {
this.data = data;
}
}
class RequestContextHolder {
public static ThreadLocal<Request> holder = new ThreadLocal<>();
}
class Servlet {
public void process(String data){
Request request = new Request(data);
// 线程局部变量设置值
RequestContextHolder.holder.set(request);
System.out.println("Servlet.request: "+request);
new Controller().process();
}
}
class Controller {
public void process(){
// 获取线程局部变量值
Request request = RequestContextHolder.holder.get();
System.out.println("Controller.request: "+request);
new Service().process();
}
}
class Service {
public void process(){
Request request = RequestContextHolder.holder.get();
System.out.println("Service.request: "+request);
// 最后要移除线程局部变量,避免内存溢出
RequestContextHolder.holder.remove();
}
}
class HolderTest {
public static void main(String[] args) {
new Servlet().process("http请求");
}
}
要理解ThreadLocal的原理,要先搞清楚Thread、ThreadLocalMap、ThreadLocal这3个类的关系,这3个类的关系如下图所示
有一点是我初学ThreadLocal非常困惑的地方,使用ThreadLocal时,我们使用threadLocal.set(data)、threadLocal.get(),threadLocal.remove()方法,导致我先入为主地认为data是存储在ThreadLocal类中,这是非常错误的理解。正确的理解是上图中展示的关系:
1、Thread类有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals
// Thread类部分代码
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
2、ThreadLocal.ThreadLocalMap是ThreadLocal的静态内部类,用于维护线程本地值,使用哈希表(key-value结构)存储数据,key是ThreadLocal类型,value是数据。
public class ThreadLocal<T> {
/**
* 静态内部类
*/
static class ThreadLocalMap {
/**
* ThreadLocalMap使用Entry保存数据,Entry是一个key、value结构
* key是ThreadLocal类型,value是Object类型
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
3、执行threadLocal.set(data)时,data并不是存储在threadLocal对象中,而是存储在当前线程Thread类的threadLocals这个key-value结构的成员变量中,threadLocal实例作为key,对应的value是data。
知道了Thread,ThreadLocalMap、ThreadLocal3者的关系,分析ThreadLocal的set(T value)、get()、remove()方法就非常简单了。
ThreadLocal类的set(T value)源码分析
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// getMap(Thread t)方法只有一行,return t.threadLocals; 返回线程类的threadLocals
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
// 以ThreadLocal作为key,数据作为value存储到threadLocals中
map.set(this, value);
else
// 如果t.threadLocals是空,则使用带参构造函数创建map并保存数据
// createMap(Thread t, T firstValue)源码也只有一行 t.threadLocals = new ThreadLocalMap(this, firstValue);
createMap(t, value);
}
ThreadLocal类的get()源码分析
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程的 t.threadLocals
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取t.threadLocals的Entry中threadLocal对应value
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果t.threadLocals不为空,又找不到value。则调用setInitialValue(),
// setInitialValue()会调用initialValue(),文章开头已经介绍了initialValue()方法
return setInitialValue();
}
从set(T value)、get()方法可以看出来,set(T value)方法的参数value是存储到了Thread的threadLocals中,ThreadLocal创建的对象RequestContextHolder.holder在用法上有点类似于一个工具类,并且RequestContextHolder.holder献祭了自己,把自己作为key和value关联起来。
ThreadLocal类的remove()源码分析
public void remove() {
// 返回当前线程的成员变量threadLocals
ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
// 删除线程成员变量threadLocals中以ThreadLocal对象为key的键值对。
if (m != null)
m.remove(this);
}
线程局部变量使用完后,不要忘记执行删除操作,不然会有内存溢出风险。
内存溢出与弱引用
如果线程是使用new Thread(runnable)这种方式创建的,则线程终止后,线程中的threadLocals会被回收,自然不会发生内存溢出。但是如果线程是使用线程池创建的,并且一直存活,threadLocals会常驻内存,若存活线程中有很多ThreadLocal对象执行了set(value)方法,而不执行remove()方法,将导致存活线程的threadLocals越来越大。
以下是ThreadLocalMap.Entry的部分代码
static class ThreadLocalMap {
/**
* Entry继承了弱引用,ThreadLocal类型的key是弱引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
// Entry中的value仍然是强引用
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal.ThreadLocalMap.Entry继承了弱引用。弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。但只是key为弱引用,value还是强引用,若还有其他变量引用了value,会导致Entry无法被GC回收。java设计者考虑到了ThreadLocal会发生内存溢出的场景,并做了一些处理,比如set(T value),remove()方法会调用resize()方法,resize()方法判断Entry的key是否为null,若key为null,则将value设置为null,帮助JVM在GC时清理内存。
假设key为不被强引用关联,Entry的键值对被回收的过程如下:
虽然java设计者做了一些优化,以避免内存溢出。但如果Entry中的key被强引用关联,并且线程是线程池中一直存活的线程,仍然有内存溢出风险,为了保险起见,使用ThreadLocal仍然要手动调用remove()方法。
ThreadLocal与线程安全
ThreadLocal的官方定义:This class provides thread-local variables。我英文很菜,这句英文的大致意思是这个类可用于创建线程本地变量。我仍然使用方法局部变量做类比。
class VariableDemo {
public static Map commonData = new HashMap();
public void fn1(){
// 线程安全
Map a = new HashMap<>();
// 非线程安全
Map b = commonData;
}
}
上述代码的a是线程安全的,b是非线程安全的(当然,变量b这种使用方式确实是脑残)。使用ThreadLocal也存在此的问题,即执行ThreadLocal的set(T value)方法,value本身必须线程安全的。以下是线程不安全代码演示
@Data
class Request{
private String data;
public Request(String data) {
this.data = data;
}
}
class RequestContextHolder {
public static ThreadLocal<Request> holder = new ThreadLocal<>();
}
class Servlet {
public void process(String data){
// 线程局部变量的值是线程不安全的
RequestContextHolder.holder.set(HolderTest.request);
new Controller().process();
}
}
class Controller {
public void process(){
// 获取线程局部变量值,并将request的data属性改成当前线程的名字
Request request = RequestContextHolder.holder.get();
request.setData(Thread.currentThread().getName());
new Service().process();
}
}
class Service {
public void process(){
// 睡眠一段时间
HolderTest.sleep();
Request request = RequestContextHolder.holder.get();
System.out.println(Thread.currentThread().getName() + " 线程与request " + request);
// 最后要移除线程局部变量,避免内存溢出
RequestContextHolder.holder.remove();
}
}
class HolderTest {
// 线程不安全的变量
public static Request request = new Request("");
public static void sleep(){
try {
TimeUnit.MILLISECONDS.sleep(100 + new Random().nextInt(500));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> new Servlet().process("")).start();
}
}
}
这段代码与文章开头的代码最大的不同之处是RequestContextHolder.holder.set(XX);设置的值不同。
文章开头,线程安全的代码:
public void process(String data){
// request在方法内部创建,是线程安全的,类似于Map a = new HashMap<>(); 这种写法
Request request = new Request(data);
RequestContextHolder.holder.set(request);
}
线程不安全的代码
public void process(String data){
// set方法的入参是线程不安全的,类似于Map b = commonData;这种写法
RequestContextHolder.holder.set(HolderTest.request);
new Controller().process();
}
如果大家还是有点懵,请联系Thread、ThreadLocalMap、ThreadLocal这3个类的关系来理解,比方说有两个线程执行了RequestContextHolder.holder.set(HolderTest.request);这句代码,线程与数据的关系如下图
两个线程执行RequestContextHolder.holder.get();得到的是同一个对象,然后修改这个对象,当然会出现线程不安全的情况。
再啰嗦几句,如果要让上面的代码变成线程安全的,要怎么做呢?答案:给读取、修改HolderTest.request的代码加上锁即可。例如给new Servlet().process("");加上synchronized
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
synchronized (LOCK){
new Servlet().process("");
}
}).start();
}
}
这做法也类似将 Map b = commonData;变为线程安全的做法一样
class VariableDemo {
public static Object LOCK = new Object();
public static Map commonData = new HashMap();
public void fn1(){
// 线程安全
Map a = new HashMap<>();
// 加上锁
synchronized (LOCK){
Map b = commonData;
b.put("xxx", Thread.currentThread().getName());
b.get("xxx");
b.put("aaa", "aa");
b.remove("xxx");
}
}
}