原文:原文出处
ThreadLocal即线程局部变量或线程本地存储,是一种多线程间并发访问变量的解决方案。与synchoronized与Lock等枷锁的方式不同,ThreadLocal完全不提供锁,而是使用空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此它不是数据共享的解决方案。在高并发量或者锁竞争激烈的情况下,使用ThreadLocal可以在一定程度上减少锁的竞争,减少CPU开销。
举个栗子:在一个简单的单体服务架构中,用户通过浏览器访问web服务器,登陆之后从服务器获取用户信息,然后会把用户信息使用cookie等方式保存到浏览器本地。我们可以把web服务器可以看成初始化ThreadLocal数据的那个线程,而多个浏览器看成其他的多个线程。当多个浏览器想要获取同一用户信息的时候,需要从唯一的服务器中获取到初始化的用户数据,获取到之后会保存到浏览器本地,当浏览器端如何修改cookie中的用户信息都不会影响到服务器以及其他浏览器的用户数据,这就相当于保证了用户信息的并发访问。
在创建ThreadLocal的时候,可以通过实现initialValue()方法或者调用withInitial()方法对其值进行初始化。
/**
* @Author Fluffy Catkin
* @Date 2020/12/18 23:44
* @Version 1.0
* @Description 初始化ThreadLocal
*/
public class InitThreadLocal {
public static void main(String[] args) throws InterruptedException {
//方式一 重写initialValue(方法)
ThreadLocal<String> threadLocal1 = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "way1:重写initialValue(方法)";
}
};
//方式二 Lambda表达式
ThreadLocal<String> threadLocal2 =
ThreadLocal.withInitial(() -> "way2:Lambda 使用withInitial()方法 ");
//打印
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
}
}
当主线程初始化一个ThreadLocal之后,其他线程也可以拿到主线程初始化的数据值,但是在其他线程中对拿到的数据进行修改并不会影响到主线程以及其他线程的数据,即多个线程所获取到的数据是相互独立的。
/**
* @Author Fluffy Catkin
* @Date 2020/12/18 23:44
* @Version 1.0
* @Description 初始化ThreadLocal
*/
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
//在主线程里创建一个threadLocal
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
System.out.println(Thread.currentThread().getName()+":初始值==>"+threadLocal.get());
new Thread(()->{
//在这个新的线程中可以拿到初始化的数据
System.out.println(Thread.currentThread().getName()+":新的线程是否可以拿到==>"+threadLocal.get());
//在这个新的线程中修改他的值
threadLocal.set(2);
//修改后,在这个线程中可以拿到修改后的值
System.out.println(Thread.currentThread().getName()+":新的线程修改后的值==>"+threadLocal.get());
},"myThread").start();
Thread.sleep(1000);
//其他线程中修改修改后,原线程中得值并没有被改变
System.out.println(Thread.currentThread().getName()+":其他线程中修改后,主线程中的值还是初始值为==>"+threadLocal.get());
//在主线程中修改值
threadLocal.set(3);
//在主线程中看到的值发生了变化
System.out.println(Thread.currentThread().getName()+":主线程中修改后,主线程中的值发生了改变==>"+threadLocal.get());
}
}
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.
*/
//类似于Map的key value存储结构,不同的是,ThreadLocalMap的key
//必须是ThreadLocal对象,value值是ThreadLocalMap 所持有的数据
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始化长度为16
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
//Entry数组 长度总是2的整数次幂
private Entry[] table;
...........
}
public
class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
//通过key value的形式保存每个ThreadLocal的数据
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
这里我们应该清楚了,每个线程都使用ThreadLocalMap 类型的变量threadLocals保存了所有ThreadLOcal的数据,以保证每一个线程数据的隔离性。
protected T initialValue() {
return null;
}
下面就是通过lambda方式初始化,返回一个继承ThreadLocal的对象SuppliedThreadLocal,并重写其初始化方法initialValue,只是增加了lambda表达式,使用户使用起来更方便:
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中的变量threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
//以当前对象为key获取对应的Entry值
ThreadLocalMap.Entry e = map.getEntry(this);
//如果有值,直接返回获取到的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果没有值则调用当前threadLocal对象的初始化方法initialValue()
//获取到初始化的对象
return setInitialValue();
}
...
//以当前对象threadlocal为key,threadlocal的值为value,
//放入当前线程的threadLocals中
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果已经初始化,直接将key、value放入
if (map != null)
map.set(this, value);
else
//如果没有初始化则创建,并把key、value放入
createMap(t, value);
return value;
}
...
//返回线程的threadLocals变量
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中的threadLocals变量
ThreadLocalMap map = getMap(t);
//如果不为空,直接将值放入
if (map != null)
map.set(this, value);
else
//如果为空创建一个对象,并将值放入
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
//将entry的key置为null
e.clear();
//将该entry的value也置为null
expungeStaleEntry(i);
return;
}
}
}
上面我们说过,在使用完threadLocal之后要使用remove()方法将资源释放,否则可能会造成内存泄漏,下面来分析为什么会造成内存泄漏。
所谓的内存泄漏是指有无用的对象一直占在内存中,从而导致这块内存一直无法使用,造成内存浪费的现象。
在java中,运行时数据区的内存都是由JVM管理,堆内存都是通过垃圾回收器进行自动回收,无需开发人员手动释放。JVM是通过可达性分析去标识对象是否是垃圾,然后将垃圾进行回收。而往往开发人员会因为代码的问题造成一些没用的对象仍然可达,这就造成JVM认为这些没用的对象不是垃圾,就不会对其内存进行回收而造成内存一直被占用,从而造成内存泄漏。
代码如下:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Author Fluffy Catkin
* @Date 2020/12/22 0:06
* @Version 1.0
* @Description 测试ThreadLocal内存泄漏
*/
public class ThreadLocalOOM {
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5,30,
TimeUnit.SECONDS,new LinkedBlockingQueue<>());
private static final int LOOP_TIMES = 100;
private static final ThreadLocal<BigParam> threadLocal =
ThreadLocal.withInitial(BigParam::new);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < LOOP_TIMES; i++) {
int finalI = i;
executor.execute(
() -> {
threadLocal.get();
System.out.println("add thread ==="+ finalI);
// threadLocal.remove();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
Thread.sleep(20000);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(new Date())+"gc..................");
System.gc();
// Thread.sleep(2000);
//销毁线程
// executor.shutdown();
// System.out.println(simpleDateFormat.format(new Date())+"销毁线程后 gc ..................");
// System.gc();
// Thread.sleep(20000);
}
static class BigParam{
private final byte[] a = new byte[1024*1024*50];
}
}
在执行前,先设置JVM参数,方便对比:
-XX:+PrintGC -Xms330m -Xmx330m
在前面源码分析中说到过,每个线程Thread都持有一个ThreadLocalMap类型的对象threadLocals,ThreadLocalMap中的键值对结构Entry继承了WeakReference,它的key是一个被WeakReference弱引用包住的ThreadLocal对象,我们知道,弱引用的对象在发生GC的时候一定会被回收掉,当发生GC的时候,key被回收掉,然而key所对应的value仍然是有值的,他的引用始终被这个线程所持有,如果这个线程不销,那么对于这个value来说根是永远可达的,因此不会垃圾回收器回收,从而产生内存泄漏。
/**
* 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;
//key是被WeakReference包住的ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//调用ThreadLocalMap的remove方法
m.remove(this);
}
继续往下看:
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
//查找所有key为null的节点,把其value也置为null
expungeStaleEntry(i);
return;
}
}
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//将当前节点的value置为null
tab[staleSlot].value = null;
//将当前节点置为null
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
//再遍历一次所有节点,因为别的地方也调用了这个方法
//但是其他方法没有循环调用,所以内部再遍历一遍
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果key为空则把对应的value置为空,并且把节点置空
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//重新排列数组
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
Thread.sleep(2000);
// 销毁线程
executor.shutdown();
System.out.println(simpleDateFormat.format(new Date())+"销毁线程后 gc ..................");
System.gc();
Thread.sleep(20000);
执行后,控制台打印:
内存情况:
可以看到,在2020-12-25 12:30:54左右进行了一次GC,此时线程并未销毁,仍有250m内存无法回收,在2020-12-25 12:30:56左右进行了最后一次GC,此时已经通过executor.shutdown();方法销毁了线程,泄漏的内存成功被回收。OK,对于ThreadLocal的介绍就到这里。