生存中 的 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 值。
上面是 福利