JDK1.8源码-14-ThreadLocal

JDK1.8-14-ThreadLocal

前序

参考博客: https://www.cnblogs.com/fsmly/p/11020641.html

总结几个字:线程自己的本地变量

1. 简介

  • 多线程访问同一个共享变量时候容易出现并发问题,特别是多个线程对同一个变量进行写入的时候,为了确保线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程的安全性。
  • ThreadLocal 是除了加锁这种同步方式以外的一种保证一种规避多线程访问出现线程不安全的方法,当我们创建一个共享变量后,如果每个线程对其进行访问的时候访问都是线程自己的变量就不会存在线程不安全的问题。
  • ThreadLocal 是 JDK包提供的,它提供线程的本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每一个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而避免了线程安全性问题。

mark

2.简单使用

  • 在下面的例子中,在每个线程的内部都设置了本地变量的值,然后调用print 方法打印当前本地变量的值。
  • 如果在打印之后调用本地变量的remove方法后会删除把本地内存中的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ThreadLocalTest {

static ThreadLocal<String> localVar = new ThreadLocal<>();

static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}

public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar1");
//调用打印方法
print("thread1");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});

t1.start();
t2.start();
}
}

下面是运行后的结果:

mark

3. 实现原理

  • 下面是ThreadLocal的类图结构,从图中可知,java.lang.Thread中有两个变量threadLocalsinheritableThreadLocals 。两者都是ThreadLocal内部类 ThreadLocalMap 类型的变量。

mark

mark

  • 在默认情况下,每个线程中这两个变量都是null,之后我们通过原码后可以看到 ThreadLocalMap 实际上类似于一个 HashMap. 这两个变量只有当线程第一次调用ThreadLocal的 set和 get方法的时候才会创建它们。
  • 需要注意的是:每个线程的本地变量不是存放在 new 出来的TheadLocal 实例中,而是存放在调用线程的ThreadLocals 变量中。(前面说过,这个变量在 java.lang.Thread类中)
  • 也就是说,ThreadLocal 类型的本地变量实际是存放在具体的线程空间上,其本身相当于装在本地变量的工具壳,通过 set 方法将value 添加到 threadlocals 变量中,当调用线程的时候能够将它从 threadLocals变量中取出。
  • 如果调用的线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals 中,所以不使用本地变量的时候需要从它的threadLocals 取出变量。
1
2
3
4
5
6
7
8
9
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

mark

3.1 set 方法

  • 源码分析(重点是类似与Hashmap 的 ThreadlocalMap)
1
2
3
4
5
6
7
8
9
10
11
12
public void set(T value) {
//(1) 获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2) 以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//(3) 如果map不为null,就直接添加本地变量,key为当前线程的引用,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4) 如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}

在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals变量(参照上面的图示和文字说明),该方法代码如下

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}
  • 如果调用getMap 的返回值不为null,就直接将 value 设置到 threadLocals变量中(此时 key 是当前线程的引用,值是本地变量)
  • 如果调用 getMap 方法返回null 说明是第一次调用 set 方法(前面说到过,threadLocals默认值是 Null , 只有调用 set 方法的时候才会创建map),这个时候就需要调用 createMap方法来创建ThreadLocalMap
1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • createMap 方法不仅创建了 threadLocals变量 ,通过是也将本地变量的值放到了 threadLocals 中.

3.2 get 方法

  • 在get 方法的实现中,首先获取当前调用者线程

    • 如果当前线程的threadLocals 不为 null ,就直接返回当前线程绑定的本地变量值,
    • 否则执行 setInitialValue的方法初始化 threadLocals 变量。
  • setInitialValue 方法中,类似于 set 方法的实现,都是判断当前线程的threadLocals 变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值是null),否则创建的是threadLocals变量,同样添加的是null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public T get() {
//(1)获取当前线程
Thread t = Thread.currentThread();
//(2)获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
//(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
}

private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}

3.3 remove 方法

  • remove 方法判断该当前线程对应的threadLocals变量是否是null,不是 null 的话就直接删除当前线程中的threadLocals变量。
1
2
3
4
5
6
7
public void remove() {
//获取当前线程绑定的threadLocals
ThreadLocalMap m = getMap(Thread.currentThread());
//如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
if (m != null)
m.remove(this);
}

注意 : 这里可能有内存泄露问题

mark

  • 如上图所示,每个线程内部都有一个名为threadLocals 的成员变量,该变量的类型是ThreadLocal.ThreadLocalMap的类型(类似于一个HashMap) ,其中key是当前定义的ThreadLocal 变量 this的引用, value 是 使用 set 方法设置的值。
  • 每个线程的本地变量存放在自己的本地变量 threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在(所以会内存溢出),因此使用完毕需要remove 掉。

4. ThreadLocal 不支持继承性

  • 同一个Threadlocal 变量在父线程中被设置后,在子线程中是获取不到的
  • threadLocals 中为当前调用线程对应的本地变量,所以二者自然是不能共享的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadLocalTest2 {

//(1)创建ThreadLocal变量
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
//在main线程中添加main线程的本地变量
threadLocal.set("mainVal");
//新创建一个子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程中的本地变量值:"+threadLocal.get());
}
});
thread.start();
//输出main线程中的本地变量值
System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
}
}

5. InheritableThreadLocal类

  • 在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

protected T childValue(T parentValue) {
return parentValue;
}

ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}

void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
  • 从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。
  • 其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。

下面我们看看重写的childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从Thread类开始说起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//判断名字的合法性
if (name == null) {
throw new NullPointerException("name cannot be null");
}

this.name = name;
//(1)获取当前线程(父线程)
Thread parent = currentThread();
//安全校验
SecurityManager security = System.getSecurityManager();
if (g == null) { //g:当前线程组
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}

g.addUnstarted();

this.group = g; //设置为当前线程组
this.daemon = parent.isDaemon();//守护线程与否(同父线程)
this.priority = parent.getPriority();//优先级同父线程
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
//(2)如果父线程的inheritableThreadLocal不为null
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;

tid = nextThreadID();
}
  • 在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,
  • 然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。
  • 下面是createInheritedMap方法和ThreadLocalMap的构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//调用重写的方法
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
  • 在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。
  • 返回之后赋值给子线程的inheritableThreadLocals。
  • 总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。
  • 而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中

6. 从ThreadLocalMap 看 ThreadLocal 的内存泄露问题

6.1 基本概念

  • 首先我们看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal 是一个工具类,它为用户提供了 set,get,remove 接口操作实际存放本地变量的threadLocals (调用线程的成员变量), 也知道threadLocals 是一个 ThreadLocalMap 类型的变量
  • 下面我们来看看ThreadLocalMap 这个内部类,在此之前,我们先回忆以下Java中的四种引用类型,以及相关GC的工作机制

强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

软引用:简言之,如果一个对象具有软引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

弱引用(这里讨论ThreadLocalMap中的Entry类的重点) :如果一个对象只有弱引用,那么这个对象就会被垃圾回收器GC(弱引用的引用的对象只能到下一次之前,无论当前内存是否足够,弱引用所引用的对象都会被GC回收掉)。 弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收掉,JVM会将这个引用加入到与之关联的弱引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用对象被回收掉之后,在调用get方法就会返回null.

虚引用 : 虚引用是所有引用中最弱的一种引用,目的就是将关联虚引用对象在GC回收掉之后得到一个通知(不能通过get方法获得指引对象。)

mark

6.2 ThreadLocalMap的内部实现

  • ThreadLocalMap 其实就是一个Entry 数组

mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 是继承自WeakReference的一个类,该类中实际存放的key是
* 指向ThreadLocal的弱引用和与之对应的value值(该value值
* 就是通过ThreadLocal的set方法传递过来的值)
* 由于是弱引用,当get方法返回null的时候意味着坑能引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** value就是和ThreadLocal绑定的 */
Object value;

//k:ThreadLocal的引用,被传递给WeakReference的构造方法
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
super(referent); //referent:ThreadLocal的引用
}

//Reference构造方法
Reference(T referent) {
this(referent, null);//referent:ThreadLocal的引用
}

Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
  • 从上面给的代码中可以看出,当前threadLocal 的引用k被传递给了WeakReference,所以ThreadLocalMap中的key为ThreadLocal的弱引用。

  • 当一个线程调用ThreadLocal 的 set 方法设置变量的时候,当前线程ThreadLocalMap 中会存放一个记录,这个记录的key值就是ThreadLocalMap 的弱引用,value就是 set进去的值,这个值可以是任意的对象。

  • 如果当前线程一直存在且没有调用ThreadLocal的remove方法,如果这个时候别的地方还有ThreadLocal的引用,那么当前线程的ThreadLocalMap 中会存在对ThreadLocal变量的引用和value 对象的引用,这个弱引用是不会被释放的,就会造成内存泄漏。

  • 考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于ThreadLocalMap 中的key是弱引用,所以当前线程ThreadLocalMap里面的ThreadLocal变量的弱引用在gc时候就被回收,但是对应的value 还是存在这就可能导致内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:

  • ThreadLocalMap 中的Entry数组 的 key 使用的是 ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉
  • 但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项
  • 这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信