概述
ThreadLocal无论是在平常的项目中还是面试都会比较频繁的出现,今天星期天,抽时间总结一下。首先ThreadLocal的出现是为解决多线程程序的并发问题的,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。各个线程通过ThreadLocal的get和set方法就可以设置和得到当前线程对应的值。这样说起来比较抽象,我们下面通过实际的案例来解释说明。
目录
一、ThreadLocal的基本使用
为了说明ThreadLocal的作用,我们先看一下没有ThreadLocal的情况
public class ThreadLocalBDemo{
static Integer num ;
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +":" + num);
num = 1;
System.out.println(Thread.currentThread().getName() + ":" + num);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +":" + num);
num = 2;
System.out.println(Thread.currentThread().getName() + ":" + num);
}
});
thread1.start();
try {
/**
*加入这个join方法主要是为了让线程thread1和thread2顺序执行
* 我们在main线程中调用了thread1的join方法,那么main线程就会被阻塞
* 等到thread1线程执行完后,main线程才会继续执行,所以在main线程中的
* thread2.start()才会执行,这样就保证了thread1和thread2顺序执行
*/
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
我们在ThreadLocalBDemo定义了一个全局变量 num,并且开启两个线程thread1和thread2,在两个线程中分别对num进行赋值处理,看一下输出结果:
Thread-0:null
Thread-0:1
Thread-1:1
Thread-1:2
通过输出结果我们可以看出,我们在thread1中给num赋值为1后,在thread2中的第一个输出语句中也拿到num=1的结果。在一些业务需求中我们不希望这个变量线程共享,而是每个线程都有属于它自己独立的一份,比如在实际业务中我们每个线程代表一个用户,这个num代表每个用户自己特有的订单数量等属性,那么像上面的程序就很明显不符合业务需求和逻辑。这个时候我们引入了ThrealLocal,ThreadLocal会为这个变量保存一个只属于它自己线程本身的变量副本,从而避免上面两个线程相互影响的情况。我们还是以上面的程序为案例,用ThreadLocal对其进行改造:
package com.zhangxudong;
public class ThreadLocalDemo {
static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocal.get());
threadLocal.set(1);
System.out.println(threadLocal.get());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocal.get());
threadLocal.set(2);
System.out.println(threadLocal.get());
}
});
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
输出结果:
null
1
null
2
通过上面的输出结果我们可以看到虽然我在先执行的thread1中通过ThreadLocal的set方法赋值为1,但在后执行的thread2中的第一行输出语句中,通过get方法拿到的值确实null,这就实现了变量的隔离。那ThreadLocal是怎么实现的这种各个线程变量隔离的呢?下面我们就进入ThreadLocal源码内部了解一下。
二、ThrealLocal实现原理
首先我们看一下ThreadLocal在使用set方法赋值的时候,是怎么实现为每个线程保存一个变量副本的。我们先从set()方法入手:
set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到,我们在thread1线程中利用ThreadLocal的set方法设置值的时候,在set方法内部首先是通过Thread t = Thread.currentThread()拿到当前的线程,这个线程当然就是thread1本身。紧接着调用了getMap(t)方法,这个方法传进去的参数就是当前线程thread1,那我们继续看一下getMap(t)这个方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到它返回值是一个ThreadLocalMap类型的threadLocals变量,这个threadLocals其实就是线程类Thread中的一个成员变量,当前这个时候也就是thread1实例对象中的threadLocals成员变量:
//Thread类中的成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
那么这个ThreadLocalMap又是一个什么东西呢?这个你就简单的先理解为它就是一个类似我们平常用的Map结构,里面存放的是(key,value)键值对形式的数据。上面介绍的一切,总结起来其实就是一句话:在线程类中有一个成员变量threadLocals,这个成员变量的类型是ThreadLocalMap类型的,我们可以往里面放一条条“键值对”形式的数据。所以我们每new一个线程,那么在这个线程对象了里就有一个只属于这个线程对象的ThreadLocalMap类型的threadLocals变量。
继续接着上面的set源码看,通过getMap(t)方法在拿到这个属于当前线程对象(thread1)的threadLocals变量后(map),判断其是否为空,如果为空的话createMap()方法创建一个,如果不为空的话,就往里面存一个(key,value)形式的键值对,其中的这个key就是this,表示我们最开始创建的ThreadLocal对象,value就是我们最开始要设置的值。所以不管我们创建多少个线程实例,在每个线程实例中都有一个属于它自己的threadLocals变量,通过set方法往里面放值的时候,也是放到了只属于这个线程的threadLocals中去(虽然各个线程中threadLocals的key是一样的,都是我们最开始创建的ThreadLocal对象,但threadLocals本身是不一样的,它属于不同的线程,这里可能有点绕,好好理解一下),所以我们在拿值得时候,也就会只从属于当前线程得threadLocals中去拿,这就实现了变量的隔离。到此,ThreadLocal的set方法就讲解完了。
get()方法
理解了set方法其实get方法就很好理解了,我们看一下get方法得源码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
我们可以看到,get方法中首先也是拿到当前线程,然后也是通过getMap()方法拿到属于它得threadLocals,如果这个threadLocals不为空,则通过getEntry()方法得到存在里面的键值对Entry类型的对象,这个键值对中的键就是ThreadLocal对象,值就是我们之前在set方法中设置的值,最后的e.value拿到的就是这个值。
三、ThreadLocal的内存泄漏问题
在上面讲解THreadLocal的set()方法的时候我们知道,set方法会把ThreadLocal对象作为键,具体的value作为值,以键值对的形式存放在ThreadLocalMap类型的threadLocals变量中,而这个键值对整体是以一个Entry类型存在的。我们接着上面set源码中的map.set(this, value)继续点进去看:
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[] 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();
}
上面的源码我们只看第8行和第28行就可以了,其他的先忽略,首先在这个set方法中的最开始有这么一行代码Entry[] tab = table,它维护了一个Entry类型的数组tab;接着看第28行:tab[i] = new Entry(key, value),可以发现是根据我们传过来的key和value值来new了一个Entry对象,并把它放到了Entry数组tab中。整体关系就是Thread中有一个ThreadLocalMap类型的成员变量threadLocals,而在ThreadLocalMap是ThreadLocal类的一个静态内部类,Entry又是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;
}
}
Entry 是ThreadLocalMap的一个静态内部类,并且它继承了弱引用(关于弱引用可以参考我前篇的文章java中四种引用类型),在Entry的构造方法中有这么一段:super(k),这说明它调用了其父类“弱应用WeakReference”的构造方法,并且将键值k传了进去(这个key值就是指向ThreadLocal对象的引用)。那么这个时候key就以一种弱引用的形式指向了ThreadLocal对象,我们通过一张图来说明一下这个:
首先我们在最开始通过ThreadLocal t1 = new ThreadLocal()这新建一个ThreadLocal对象时,这个t1是一个强引用并且执行ThreadLocal对象。之后通过一系列上面的操作,我们知道键值对Entry中的key会以弱引用的形式也指向Thread Local对象。
那为什么Entry要使用弱引用呢?因为若使用的是强引用,即使t1=null,但key依然以强引用的形式指向ThreadLocal对象,因此ThreadLocal对象不会被释放掉,所以会有内存泄漏的问题。但如果是弱引用,当t1=null后,因为key是以弱引用的形式指向ThreadLocal的对象,那么gc碰到弱引用它是不关心的,依然会把ThreadLocal对象回收掉。但是虽然这样,ThreadLocal还是会存在内存泄漏问题,因为当ThreadLocal对象被回收后,key值就变成了null,这就导致整个Entry中的value再也无法访问到,因此依然存在内存泄漏问题。综上,ThreadLocal是存在内存泄漏的 问题的。