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