ThreadLocal我主要从以下接个方面说明:
- 基础
- 理解
- 接口方法
- 源码分析
- ThreadLocal如何解决内存泄漏
1、基础
ThreadLocal从字面意思为:线程本地。它是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
有一个误区是ThreadLocal的目的是为了解决多线程访问资源时的共享问题 但ThreadLocal 并不解决多线程 共享 变量的问题。既然变量不共享,那就更谈不上同步的问题。
2、理解
ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例)。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题
- 既无共享,何来同步问题,又何来解决同步问题一说?
那 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。这让我想到了Js中的一个特性:闭包.闭包下的所有变量都是只属于自己的,而ThreadLocal就是只属于线程自己的对象. 另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。
3、接口方法
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
- void set(Object value)设置当前线程的线程局部变量的值。
- public Object get()该方法返回当前线程所对应的线程局部变量。
- public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
4、源码分析
demo:启动三个线程,对变量自增并打印
public class ThreadDemo1 {
// ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = ThreadLocal.withInitial(()->0);
// ②获取下一个序列值
public int getNextNum() {
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
public static void main(String[] args) {
ThreadDemo1 sn = new ThreadDemo1();
// ③ 3个线程共享sn,各自产生序列号
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extends Thread{
private ThreadDemo1 sn;
public TestClient(ThreadDemo1 sn){
this.sn = sn;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
// ④每个线程打出3个序列值
System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
+ sn.getNextNum() + "]");
}
seqNum.remove();
}
}
}
执行结果
从结果分析,不同的线程之间的值并没有共享。
源码分析:
set方法:
public void set(T value) {
//这里指向当前线程,在上面的demo中 分别指向Thread-0 Thread-1 Thread-2
Thread t = Thread.currentThread();
// ThreadLocal内部实际封装是ThreadLocalMap
//ThreadLocalMap 有一个key和value
ThreadLocalMap map = getMap(t);
if (map != null)
//这里的key=this 表示当前对象
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread类
ThreadLocal.ThreadLocalMap threadLocals = null;
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类
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();
}
Entry: 它是ThreadLocalMap内部类
//Entry继承WeakReference表示它是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
总结:
- ThreadLocal它内部其实是一个ThreadLocalMap,这个ThreadLocalMap的key指向当前用户执行的线程对象
- ThreadLocalMap.set方法其实tab[i] = new Entry(key, value);
- Entry是一个弱引用。
- 我们知道java弱引用,只要JVM发生GC时候就会被回收,那么问题来啦,ThreadLocalMap引用栈被回收,但它的VALUE且一直存在。在生产环境中,一个线程也许不间断运行。势必会造成内存泄漏问题。
关于《java强引用-软引用-弱引用-虚引用》可以看我之前的文章。java强引用-软引用-弱引用-虚引用
5、ThreadLocal如何解决内存泄漏
这要从两个方面说:
- Entry为什么使用弱引用?
若是强引用,即使tl=null 但key的引用依然指向ThreadLocal对象,所以会有内存泄漏,而使用弱引用则不会。
- Entry中value不能被回收
即使 Entry采用弱引用,但还是有内存泄漏存在,ThreadLocal被回收,key的值变成null,则导致整个value再也无法被访问到,因此依然存在内存泄漏。所以必须调用remove方法。
来源:CSDN
作者:鬼布
链接:https://blog.csdn.net/zhmi_1015/article/details/103688166