文章目录
1 线程上下文设计
单个线程执行的任务步骤会非常多,后一个步骤的输入有可能是前一个步骤的输出,比如在单个线程多步骤(阶段)执行时,为了使得功能单一,有时候我们会采用GoF职责链设计模式:
有些时候后一个步骤未必会需要前一个步骤的输出结果,但是都需要将context从头到尾进行传递,假如方法参数比较少还可以容忍,如果方法参数比较多,在七八次的调用甚至十几次的调用,都需要从头到尾地传递context,很显然这是一种比较烦琐的设计,那么我们就可以尝试采用线程的上下文设计来解决这样的问题:
private ConcurrentHashMap<Thread, ThreadContext> contexts =
new ConcurrentHashMap<>();
public ThreadContext getThreadContext(){
ThreadContext threadContext = contexts.get(Thread.currentThread());
if(threadContext == null){
threadContext = new ThreadContext();
contexts.put(Thread.currentThread(),threadContext);
}
return threadContext
}
通过这种方式定义线程上下文很可能会导致内存泄漏,contexts是一个Map的数据结构,用当前线程做key,当线程的生命周期结束后,contexts中的Thread实例不会被释放,与之对应的Value也不会被释放,时间长了就会导致内存泄漏(Memory Leak)
2 ThreadLocal
自JDK1.2版本起,Java就提供了java.lang.ThreadLocal,ThreadLocal为每一个使用该变量的线程都提供了独立的副本,可以做到线程间的数据隔离,每一个线程都可以访问各自内部的副本变量。
2.1 ThreadLocal的使用场景及注意事项
一般在以下情况中会使用到ThreadLocal:
- 在进行对象跨层传递的时候,可以考虑使用ThreadLocal,避免方法多次传递,打破层次间的约束。
- 线程间数据隔离,比如21.2节中描述的线程上下文ActionContext。
- 进行事务操作,用于存储线程事务信息。
ThreadLocal并不是解决多线程下共享资源的技术,一般情况下,每一个线程的Thread-Local存储的都是一个全新的对象(通过new关键字创建),如果多线程的Thread-Local存储了一个对象的引用,那么其还将面临资源竞争,数据不一致等并发问题
2.2 ThreadLocal的方法详解及源码分析
2.2.1 简单认识
public static void main(String[] args) {
// 1 创建一个ThreadLocal的实例
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
// 2 创建10个线程
IntStream.range(0,10).forEach(i -> {
new Thread(()->{
// 每个线程都设置threadLocal,但是彼此之间是独立的
threadLocal.set(i);
System.out.println(Thread.currentThread().getName() + " set value " + threadLocal.get());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " get value " + threadLocal.get());
},"线程" + i).start();
});
}
上面的代码中定义了一个全局唯一的ThreadLocal<Integer>,然后启动了10个线程对threadLocal进行set和get操作,通过下面的输出可以发现,这10个线程之间彼此不会相互影响,每一个线程存入threadLocal中的i值也是完全不同彼此独立的:
线程0 set value 0
线程3 set value 3
线程4 set value 4
线程2 set value 2
线程1 set value 1
线程6 set value 6
线程5 set value 5
线程7 set value 7
线程9 set value 9
线程8 set value 8
线程6 get value 6
线程8 get value 8
线程0 get value 0
线程3 get value 3
线程4 get value 4
线程1 get value 1
线程9 get value 9
线程2 get value 2
线程7 get value 7
线程5 get value 5
2.2.2 常用方法
最常用的方法就是:
initialValue();
set(T t);
get();
initialValue
为ThreadLocal要保存的数据类型指定了一个初始化值,在ThreadLocal中默认返回值为null
protected T initialValue() {
// 默认返回值为null
return null;
}
演示:可以通过重写initialValue方法进行数据的初始化
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
System.out.println(threadLocal.get()); // null
// 重写initialValue
ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1;
}
};
System.out.println(threadLocal1.get()); // 1
}
set方法
set方法主要是为ThreadLocal指定将要被存储的数据
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// set
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();
}
- 获取当前线程Thread.currentThread()。
- 根据当前线程获取与之关联的ThreadLocalMap数据结构。
- 如果map为null则进入第4步,否则进入第5步。
- 当map为null的时候创建一个ThreadLocalMap,用当前ThreadLocal实例作为key,将要存放的数据作为Value,对应到ThreadLocalMap中则是创建了一个Entry。
- 在map的set方法中遍历整个map的Entry,如果发现ThreadLocal相同,则使用新的数据替换即可,set过程结束。
- 在遍历map的entry过程中,如果发现有Entry的Key值为null,则直接将其逐出并且使用新的数据占用被逐出数据的位置,这个过程主要是为了防止内存泄漏
- 创建新的entry,使用ThreadLocal作为Key,将要存放的数据作为Value。
- 最后再根据ThreadLocalMap的当前数据元素的大小和阀值做比较,再次进行key为null的数据项清理工作。
get方法
get用于返回当前线程在ThreadLocal中的数据备份,当前线程的数据都存放在一个称为ThreadLocalMap的数据结构中:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// setInitialValue
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
通过上面的源码我们大致分析一下一个数据拷贝的get过程:
- 首先获取当前线程Thread.currentThread()方法。
- 根据Thread获取ThreadLocalMap,其中ThreadLocalMap与Thread是关联的,而我们存入ThreadLocal中的数据事实上是存储在ThreadLocalMap的Entry中的。
- 如果map已经被创建过,则以当前的ThreadLocal作为key值获取对应的Entry。
- 如果Entry不为null,则直接返回Entry的value值,否则进入第5步。
- 如果在第2步获取不到对应的ThreadLocalMap,则执行setInitialValue()方法。
- 在setInitialValue()方法中首先通过执行initialValue()方法获取初始值。
- 根据当前线程Thread获取对应的ThreadLocalMap。
- 如果ThreadLocalMap不为null,则为map指定initialValue()所获得的初始值,实际上是在map.set(this,value)方法中new了一个Entry对象。
- 如果ThreadLocalMap为null(首次使用的时候),则创建一个ThreadLocalMap,并且与Thread对象的threadlocals属性相关联(通过这里我们也可以发现ThreadLocalMap的构造过程是一个Lazy的方式)。
- 返回initialValue()方法的结果,当然这个结果在没有被重写的情况下结果为null。
2.2.3 ThreadLocalMap
无论是get方法还是set方法都不可避免地要与ThreadLocalMap和Entry打交道,ThreadLocalMap是一个完全类似于HashMap的数据结构,仅仅用于存放线程存放在ThreadLocal中的数据备份,ThreadLocalMap的所有方法对外部都完全不可见。
在ThreadLocalMap中用于存储数据的是Entry,**它是一个WeakReference类型的子类,之所以被设计成WeakReference是为了能够在JVM发生垃圾回收事件时,能够自动回收防止内存溢出的情况出现,**通过Entry源码分析不难发现,在Entry中会存储ThreadLocal以及所需数据的备份。ThreadLocalMap的Entry源码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal的内存泄漏问题分析
在早些的JDK版本中,ThreadLocalMap完全是由HashMap来充当的,在最新的JDK版本中,ThreadLocal为解决内存泄漏做了很多工作,使用了WeakReference来存储数据。
WeakReference在JVM中触发任意GC(young gc、full gc)时都会导致Entry的回收
在get数据时增加检查,清除已经被垃圾回收器回收的Entry(Weak Reference可自动回收)。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// /查找Key为null的Entry
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// //将key为null的Entry删除
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
// **查找Key为null的Entry**
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
// 将key为null的Entry删除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
//执行Entry在ThreadLocalMap中的删除动作
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = 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();
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;
}