快速掌握并发编程---深入学习ThreadLocal
生活中的ThreadLocal
考试题只有一套,老师把考试题打印出多份,发给每位考生,然后考生各自写各自的试卷。考生之间不能相互交头接耳(会当做作弊)。各自写出来的答案不会影响他人的分数。
注意:考试题、考生、试卷。
用代码来实现:
public class ThreadLocalDemo {
//线程共享变量 localVar
public 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("全部写完");
String threadName = Thread.currentThread().getName();
//调用打印方法
print(threadName);
}
}, "张三");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程2中本地变量的值
localVar.set("写了一半");
String threadName = Thread.currentThread().getName();
//调用打印方法
print(threadName);
}
}, "李四");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程2中本地变量的值
localVar.set("完全没写");
String threadName = Thread.currentThread().getName();
//调用打印方法
print(threadName);
}
}, "王二");
t1.start();
t2.start();
t3.start();
}
}
输出
李四 :写了一半
王二 :完全没写
张三 :全部写完
背景
ThreadLocal
:字面意思为线程本地或者本地线程。但是其实真正含义并非如此,真正的含义是线程本地变量(副本)。
java.lang.ThreadLocal
是JDK1.2
版本的时候引入的,本文是基于JDK1.8
版本进行讲解的。
上面考试场景中的几个关键点我们这么可以这么理解:
考试题----共享变量,大家共享
试卷-----考试题的副本
考试----线程
ThreadLocal
可以理解为每个线程想绑定自己的东西,相互不受干扰。比如上面的考试场景,考试题大家都是一样的。但是考试题进行复印出来后,每人一份,各自写写各自的,相互不受影响,这就正是ThreadLocal
想要实现的功能。
当使用ThreadLocal
维护变量时,ThreadLocal
为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
可以想想生活中还有没有类似的例子。肯定非常多,只要我们用心去体会。
下面我们就来看看ThreadLocal
到底是如何实现的。
ThreadLocal设计原理
ThreadLocal
名字中第一个单词Thread表示线程,Local表示本地,我们就理解为线程本地变量了。想了解更多Thread,可看:快速掌握并发编程---Thread常用方法
先看看ThreadLocal
的整体
最关心的三个公有方法:set、get、remove
构造方法
public ThreadLocal() {
}
构造方法里没有任何逻辑处理,就是简单的创建一个实例。
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);
}
先看看ThreadLocalMap是个什么东东
ThreadLocalMap
是ThreadLocal
的静态内部类。
set方法整体为
ThreadLocalMap构造方法
//这个属性是ThreadLocal的,就是获取hashcode(这列很有学问,但是我们的目的不是他)
private final int threadLocalHashCode = nextHashCode();
private Entry[] table;
private static final int INITIAL_CAPACITY = 16;
//Entry是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//数组默认大小为16
table = new Entry[INITIAL_CAPACITY];
//len 为2的n次方,以ThreadLocal的计算的哈希值按照Entry[]取模(为了更好的散列)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置阈值(扩容阈值)
setThreshold(INITIAL_CAPACITY);
}
然后我们看看map.set()方法中是如何处理的
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//len 为2的n次方,以ThreadLocal的计算的哈希值按照Entry[]取模
int i = key.threadLocalHashCode & (len-1);
//找到ThreadLocal对应的存储的下标,如果当前槽内Entry不为空,
//即当前线程已经有ThreadLocal已经使用过Entry[i]
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 当前占据该槽的就是当前的ThreadLocal ,更新value结束
if (k == key) {
e.value = value;
return;
}
//当前卡槽的弱引用可能会回收了,key:null value:xxxObject ,
//需清理Entry原来的value ,便于垃圾回收value,且将新的value 放在该槽里,结束
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//在这之前没有ThreadLocal使用Entry[i],并进行值存储
tab[i] = new Entry(key, value);
//累计Entry所占的个数
int sz = ++size;
// 清理key 为null 的Entry ,可能需要扩容,扩容长度为原来的2倍,并需要进行重新hash
if (!cleanSomeSlots(i, sz) && sz >= threshold){
rehash();
}
}
从上面这个set方法,我们就大致可以把这三个进行一个关联了:
Thread
、ThreadLocal
、ThreadLocalMap
。
get方法
remove方法
expungeStaleEntry
方法代码里有点大,所以这里就贴了出来。
//删除陈旧entry的核心方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;//删除value
tab[staleSlot] = null;//删除entry
size--;//map的size自减
// 遍历指定删除节点,所有后续节点
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//key为null,执行删除操作
e.value = null;
tab[i] = null;
size--;
} else {//key不为null,重新计算下标
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {//如果不在同一个位置
tab[i] = null;//把老位置的entry置null(删除)
// 从h开始往后遍历,一直到找到空为止,插入
while (tab[h] != null){
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
return i;
}
对象引用
在Java里万事万物皆对象,这里有个对象,那么对象引用是什么呢?
User user=new User("老田");
关于上面这段代码的解释,很大部分人会说user是个对象。
一开始培训机构什么书籍里都说user是个对象,于是也就这么叫user是对象,这里的user指向了对象"老田"。这里的User user是定义了一个对象引用,可以指向任意的User对象,比如:
User user;
user = new User("张三");
user = new User("李四");
一个队对象被user引用了,这里user把他叫做对象引用 。
对象引用就好比男人,对象就是男人的老婆。根据目前我国法律规定,一个男人在任何时候最多只能有一个老婆,但是一辈子可以取多个老婆。哈哈哈!!!
另外如果是下面
int a;
a=1;
a=100;
这里的a,我们通常称之为变量。所以上面的user我们也可以理解为变量。
在Java里对象的引用也是分几种类型的,分以下四种类型:
强引用
软引用
弱引用
虚引用
强引用
强引用就是我们平时开发中用的最多的,比如说:
Person person = new Person("老田");
这个person就是强引用。
当一个对象被强引用时候,JVM
垃圾回收的时候是不会回收的,宁愿执行OOM(Out Of Memory)
异常也绝不回收,因为JVM
垃圾回收的时候会认为这个对象是被用户正在使用,若回收了很有可能造成无法想象的错误。
软引用
如果一个对象具有软引用,内存空间足够,JVM
垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。
使用软引用能防止内存泄露,增强程序的健壮性。
java.lang.ref.SoftReference
的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
也就是说,一旦SoftReference
保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference
类所提供的get()方法返回Java对象的强引用。
/**
* Returns this reference object's referent. If this reference object has
* been cleared, either by the program or by the garbage collector, then
* this method returns <code>null</code>.
*
* @return The object to which this reference refers, or
* <code>null</code> if this reference object has been cleared
*/
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
如果引用对象被清楚或者被GC
回收,这个get方法就返回null
。
弱引用
弱引用也是用来描述非必需对象的,当JVM
下一次进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java
中,用java.lang.ref.WeakReference
类来表示。
与软引用不同的是,不管是否内存不足,弱引用都会被回收。
弱引用可以结合 来使用,当由于系统触发gc
,导致软引用的对象被回收了,JVM
会把这个弱引用加入到与之相关联的ReferenceQueue
中,不过由于垃圾收集器线程的优先级很低,所以弱引用不一定会被很快回收。
虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
注意:虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
好了上面就大概说了一下对象的四大引用,主要本文后面需要用到弱引用。
ThreadLocal 内存泄漏
讲到内存泄漏,那我们还是把内存溢出和内存泄漏大致说一下。
内存溢出
在JVM如果发生内存溢出,说明内存不够实用,撑爆了,也就是我们说的OOM。大量内存得不到释放,又不断申请内存空间。
系统内存使用200M,已经使用了180M,可是你说你还想使用50M,于是系统就受不了。
就想气球一样,原本已经到极限了,你还是使劲打气,很容易就导致气球爆炸了。
就想你只能扛100斤的东西,现在给你200斤,肯定受不了。
内存泄漏
强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM
也不会去回收他指向的对象。前面说到强引用的时候,如果对象一直被引用,JVM是不会回收他的,直到最后系统OOM
。
看过《树先生》电影的人都知道,树先生家里的地被别人占用了,但是树先生不敢把人家怎么样。如果是很多人都去占用树先生家的地和财产,到最后树先生不就要饿死么。树先生这部电影确实好看,看完一遍基本上不知道在说什么,主要是树先生幻想的太多,很多人看了两遍也不是很懂。扯远了。。。
ThreadLocal内存泄漏
内存泄漏案例
模拟了一个线程数为THREAD_LOOP_SIZE
的线程池,所有线程共享一个ThreadLocal
变量,每一个线程执行的时候插入一个大的 List 集合,这里由于执行了500
次循环,也就是产生了500个线程,每一个线程都会依附一个 ThreadLocal
变量:
public class ThreadLocalOOMDemo {
private static final int THREAD_LOOP_SIZE = 500;
private static final int MOCK_BIG_DATA_LOOP_SIZE = 10000;
private static ThreadLocal<List<User>> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE);
for (int i = 0; i < THREAD_LOOP_SIZE; i++) {
executorService.execute(() -> {
threadLocal.set(new ThreadLocalOOMDemo().addBigList());
Thread t = Thread.currentThread();
System.out.println(Thread.currentThread().getName());
//threadLocal.remove(); //不取消注释的话就可能出现OOM
});
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//executorService.shutdown();
}
private List<User> addBigList() {
List<User> params = new ArrayList<>(MOCK_BIG_DATA_LOOP_SIZE);
for (int i = 0; i < MOCK_BIG_DATA_LOOP_SIZE; i++) {
params.add(new User("Java后端技术全栈", "123456" + i, "man", i));
}
return params;
}
class User {
private String userName;
private String password;
private String sex;
private int age;
public User(String userName, String password, String sex, int age) {
this.userName = userName;
this.password = password;
this.sex = sex;
this.age = age;
}
}
}
在设置IDEA或者eclipse中,设置 JVM 参数设置最大内存为 -Xmx64m,以便模拟出 OOM:
然后,运行上面的案例
从上面的案例中我们看到:线程池中的每一个线程使用完 ThreadLocal 对象之后再也不用,由于线程池中的线程不会退出,线程池中的线程的存在,同时 ThreadLocal 变量也会存在,占用内存!造成 OOM 溢出!
前面我们分析了Thread、ThreadLocal、ThreadLocalMap三者的关系
一个 Thread 中只有一个 ThreadLocalMap,一个 ThreadLocalMap 中可以有多个 ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个 ThreadLocalMap 中一个的 Entry(也就是说:一个 Thread 可以依附有多个 ThreadLocal 对象)。
总结
每个 Thread 维护一个 ThreadLocalMap
映射表,这个映射表的 key 是 ThreadLocal
实例本身,value 是真正需要存储的 Object。
ThreadLocal
本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap
获取 value。
值得注意的是图中的虚线,表示 ThreadLocalMap
是使用 ThreadLocal
的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
ThreadLocalMap
使用 ThreadLocal
的弱引用作为 key,如果一个 ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。
如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
注意:其实在T
hreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get(),set(),remove()的时候都会清除线程 ThreadLocalMap
里所有 key 为 null 的 value。
但是如果上述代码中的这行代码
threadLocal.remove();
把注释放开,这不会抛出OOM
。
另外,网上很多文章都说这是由于弱引用导致的,个人认为不能把锅扔给弱引用,这和使用者有直接关系。如果使用得当是不会出现OOM
的。
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
那为什么使用弱引用而不是强引用??
key 使用强引用
当ThreadLocalMap
的key为强引用回收ThreadLocal
时,因为ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry内存泄漏。
key 使用弱引用
当ThreadLocalMap
的key为弱引用回收ThreadLocal
时,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。当key为null,在下一次ThreadLocalMap
调用set(),get(),remove()方法的时候会被清除value值。
下面是 福利
如果觉得有帮助,点个在看呗,(*^▽^*)