讲完了Thread以及ThreadGroup,接下来进行对ThreadLocal的认识。
文章目录
1. 认识ThreadLocal
基于JDK8查看ThreadLocal的解释:
/** * This class provides thread-local variables. These variables
differ from their normal counterparts in that each thread that
accesses one (via its * {@code get} or {@code set} method) has its
own, independently initialized * copy of the variable. {@code
ThreadLocal} instances are typically private * static fields in
classes that wish to associate state with a thread (
e.g., > a user ID > or Transaction ID).For example, the class below generates unique identifiers local to each * thread. * A thread's id is assigned the first time it invokes {@code ThreadId.get()} * and remains unchanged on subsequent calls. */
对此我的理解是,ThreadLocal用于对线程的局部变量进行读写,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
我们通过一个小demo去体会ThreadLocal是怎么一回事。
编写一个用户上下文类,用于获取/写入当前用户
public class UserContext {
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
public User getThreadLocalUser(String name){
return threadLocalUser.get();
}
public void setThreadLocalUser(User user){
threadLocalUser.set(user);
}
}
编写线程类,实现线程当前用户读写
static class MyThread extends Thread {
static UserContext userContext = new UserContext();
public void run() {
User user = new User();
//使用线程名作为用户名称
user.setName(this.getName());
userContext.setThreadLocalUser(user);
System.out.println("获取"+this.getName()+"线程用户:"+this.getCurUser().getName());
}
private User getCurUser(){
if( userContext.getThreadLocalUser()==null)
return null;
return userContext.getThreadLocalUser();
}
}
main调用实现:
public static void main(String[] args) throws Exception {
// 创建2个MyThread A,B
MyThread mtA = new MyThread();
mtA.setName("A");
mtA.start();
MyThread mtB = new MyThread();
mtB.setName("B");
mtB.start();
}
控制台输出结果如下:
这里可以看到A线程的用户是A,B线程用户是B。
到这里可能说,你的UserContext是每个线程里面的局部变量,怎么也说明不了隔离。那我们把UserContext放出去,通过线程构造方法设置进去。
具体实现
UserContext userContext = new UserContext();
MyThread mtA = new MyThread(userContext);
mtA.setName("A");
mtA.start();
System.out.println(userContext.getThreadLocalUser());
MyThread mtB = new MyThread(userContext);
mtB.setName("B");
mtB.start();
System.out.println(userContext.getThreadLocalUser());
控制台输出结果:
这里说明UserContext并不是同一个UserContext,在main方法里面的UserContext是属于主线程的ThreadLocal,同理AB,因此这里说明了ThreadLocal是属于线程的局部变量并不共享,且相互隔离。
2. ThreadLocal原理
在##1说了,ThreadLocal是实现对线程局部变量的读写,使得存在ThreadLocal的局部变量每个线程之间是互相隔离,通过get/set读写,通过ThreadLocal的get/set探究ThreadLocal的原理。
2.1 ThreadLocal.set
具体实现
public void set(T var1) {
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if (var3 != null) {
var3.set(this, var1);
} else {
this.createMap(var2, var1);
}
}
可以看到ThreadLocal的实现实际是一下这么一个链路:
Thread --> ThreadLocal --> ThreadMap --> map.set(ThreadLocal,val)
Thread里面都有一个变量 ThreadLocal.ThreadLocalMap ,用于存储线程的局部变量Map
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
而ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储,我们的值都是存储到这个Map上的,key是当前ThreadLocal对象
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
其中Entry是一个弱引用对象,这里只需要知道就行,我们下面再详细说。
2.2 ThreadLocal.get
有了set的基础,就能知道get实际上是调用Thread中ThreadLocalMap.get
链路如下:
Thread --> ThreadLocal --> ThreadMap --> map.get(ThreadLocal)
2.3 原理总结
1.ThreadLocal是用于维护线程局部变量,线程间互不影响。
2.实际实现线程局部变量的读写的是ThreadLocalMap,ThreadLocalMap被Thread所维护。
3.ThreadLocal是作为ThreadLocalMap的存储对象的Key值。
3. ThreadLocal OOM 解决
大家都说 :
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
想要避免内存泄露就要手动remove()掉
我也想直接这样提示一下就行了,但是我还是试一下,到底怎么回事。
首先看一下Thread ThreadLocal在JVM的存储以及引用关系,可以结合Java线程基础-认识并区分Thread ThreadGroup ThreadLocal ThreadGroupContext(一)
这个图说明了ThreadLocalMap是归Thread管理,而ThreadLocal是ThreadLocalMap的key,他自己管理自己。也就是跟上面说的如果没有及时删除key,有可能导致内存溢出OOM。
为什么有可能呢?一般来说我们创建线程并不会无限制的创建,因此由于过量线程创建导致ThreadLocal OOM的情况是很少的,但是我们会使用线程池,他可比我们自己手动创建线程用得要多,在线程池的应用下,是否会造成ThreadLocal OOM呢?我们通过一个小demo实验一下。
创建一个大小为5的线程池,每次运行线程先输出线程里面读取的用户,然后再设置新值。
UserContext userContext = new UserContext();
ExecutorService exec = Executors.newFixedThreadPool(5);
int loop= 100;
for (int i = 0; i < 100; i++) {
exec.execute(()->{
System.out.println("当前线程:"+Thread.currentThread()+"==="+"用户:"+userContext.getThreadLocalUser().getName());
userContext.setThreadLocalUser(new User(Thread.currentThread().getName()));
});
}
控制台输出如下结果
这个说明在线程池中线程的复用并不会销毁线程里面的ThreadLocal,当ThreadLocal积累得差不多就可以OOM了,为此我们设置jvm参数-Xms100m -Xmx100m
,增大线程池大小以及创建次数,同时增大ThreadLocal存储对象大小。
ExecutorService exec = Executors.newFixedThreadPool(99);
for (int i = 0; i < 1000; i++) {
exec.execute(() -> {
threadLocal.set(new byte[1024 * 1024]);
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
System.out.println("当前线程:"+Thread.currentThread());
e.printStackTrace();
}
});
}
很快控制台就出现OOM报错
java.lang.OutOfMemoryError: Java heap space
怎么避免OOM,用完把key也就是threadLocal处理就行了。
添加如下代码:
finally {
threadLocal.remove();
}
线程执行结束后,手动remove掉。
但是不方便啊,我们可以通过实现AutoCloseable 接口自动remove
public class MyThreadLocal implements AutoCloseable {
static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public byte[] get(){
return threadLocal.get();
}
public void set(byte[] user){
threadLocal.set(user);
}
@Override
public void close() {
System.out.println("自动关闭");
threadLocal.remove();
}
}
调用方式
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
exec.execute(() -> {
try ( MyThreadLocal threadLocal = new MyThreadLocal();){
threadLocal.set(new byte[1024 * 1024]);
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
System.out.println("当前线程:"+Thread.currentThread());
e.printStackTrace();
}
});
}
这样就会执行一次循环后remove。
当然过大的loop还是会内存溢出,这时候适当控制线程池大小。