JAVA多线程并发容易引发的问题及如何保障线程平安
之前的章节中咱们介绍了在并发时,容易引发的问题及如何保障线程平安,本章节咱们主讲JAVA并发中的无同步计划:ThreadLocal
无同步计划:
1.可重入代码:
可重入代码:能够在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回之后,原来的程序不会呈现任何的谬误。可重入代码有一些公共的特色,例如不依赖存储在堆上的数据和专用的系统资源、用到的状态量都由参数传入、不调用非可重入的办法等。简而言之:如果一个办法,它的返回后果是能够预测的,只有输出了雷同的数据,就能返回雷同的后果,那它就满足可重入性的要求,当然也就是线程平安的。
2.线程本地存储(ThreadLocal):
线程本地存储:如果一段代码所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保障在同一个线程中执行?如果能保障,咱们就能够把共享数据的可见范畴限度在同一个线程之内,这样,即是无同步也能做到防止数据争用。
[TOC]
1.ThreadLocal 介绍
一句话总结:
ThreadLocal
是一个存储在线程本地正本的工具类,要保障线程平安,不肯定非要进行同步。同步只是保障共享数据争用时的正确性,如果一个办法原本就不波及共享数据,那么天然毋庸同步。既然是本地存储的,那么就只有以后线程能够拜访,天然是线程平安的
2.ThreadLocal 利用
ThreadLocal
的罕用办法:
public class ThreadLocal<T> { public T get() {} public void set(T value) {} public void remove() {} public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}}
阐明:
get
- 用于获取ThreadLocal
在以后线程中保留的变量正本。set
- 用于设置以后线程中变量的正本。remove
- 用于删除以后线程中变量的正本。如果此线程局部变量随后被以后线程读取,则其值将通过调用其initialValue
办法从新初始化,除非其值由两头线程中的以后线程设置。 这可能会导致以后线程中屡次调用initialValue
办法。initialValue
- 为 ThreadLocal 设置默认的get
初始值,须要重写initialValue
办法 。
用法:
ThreadLocal<String> threadLocal = new ThreadLocal<>();//ThreadLocal对象threadLocal.set("java宝典");//存储内容String str = threadLocal.get();//获取内容
实例:
public class ThreadLocalTest { public static void main(String[] args) { ThreadLocal<Boolean> threadLocal = new ThreadLocal<>(); threadLocal.set(false);//存数据 printCurrentThread(threadLocal.get()); new Thread(new Runnable() { @Override public void run() { threadLocal.set(true);//存数据 printCurrentThread(threadLocal.get()); } }, "test1").start();//线程test1 new Thread(new Runnable() { @Override public void run() { threadLocal.set(true);//存数据 printCurrentThread(threadLocal.get()); } }, "test2").start();//线程test2 } private static void printCurrentThread(boolean b) { System.out.println(Thread.currentThread().getName() + ":\t" + b);//打印出线程名与传的boolean值 }}//result://main: false//test1: true//test2: true
3.ThreadLocal 源码解析
ThreadLocal做为数据存储类,那么关键点在于set
与get
办法。上面代码比拟多,解说次要在正文内.
public void set(T value) { Thread t = Thread.currentThread();//获取以后线程 ThreadLocalMap map = getMap(t);//获取线程的ThreadLocalMap if (map != null) map.set(this, value);//给map设置值,键为以后的ThreadLocal,值为传入的value else createMap(t, value);}/*** getMap办法*/ThreadLocalMap getMap(Thread t) { return t.threadLocals;//返回传入的线程的ThreadLocalMap}/*** createMap办法*/void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);//创立新的ThreadLocalMap并将值传入将线程的threadLocals设置为这个ThreadLocalMap}
public T get() { Thread t = Thread.currentThread();//获取以后线程 ThreadLocalMap map = getMap(t);//获取以后线程的map if (map != null) {//如果map不为空 ThreadLocalMap.Entry e = map.getEntry(this);//获取Entry if (e != null) {//Entry不为空 @SuppressWarnings("unchecked") T result = (T)e.value;//获取Entry的值 return result;//返回获取的值 } } return setInitialValue();}/*** setInitialValue办法*/private T setInitialValue() { T value = initialValue();//初始值 Thread t = Thread.currentThread();//获取以后线程 ThreadLocalMap map = getMap(t);//获取以后线程的map if (map != null)//map不为空设置默认的值,也就是null map.set(this, value); else createMap(t, value);//map为空新建一个map在存储默认的值 return value;//返回默认值}/*** initialValue办法*/protected T initialValue() { return null;}
剖析:在调用set办法时获取以后线程,通过获取以后线程的ThreadLocalMap,在map不为空的时候将值存储进去,如果map为空那么新建一个ThreadLocalMap并设置给Thread后存储传入的数据。
通过这部分源码能够看出为什么ThreadLocal只能操作本人线程里的数据,因为这里跟它线程的ThreadLocalMap有关系,再来剖析ThreadLocalMap
//存储数据的构造,并且是弱援用static class Entry extends WeakReference<ThreadLocal<?>> { /** 与ThreadLocal关联的值 */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}//table的初始容量,必须是2的幂private static final int INITIAL_CAPACITY = 16;//table用于存储数据,长度必须是2的幂private Entry[] table;//table中存在的数据的条目数private int size = 0;//阀值,用于扩容private int threshold; // Default to 0//阀值设置为以后传入的值的2/3倍private void setThreshold(int len) { threshold = len * 2 / 3;}//下一个值private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0);}//上一个值private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1);}//构造函数ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY];//初始化数组 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue);//数据存储进去 size = 1;//数组外面的数据条目数量设置为1 setThreshold(INITIAL_CAPACITY);//这是阀值}
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; //依据threadLocalHashCode进行一个位运算(取模)失去索引i int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get();//获取以后下标Entry的值 //如果获取的ThreadLocal雷同间接替换e的值 *1* if (k == key) { e.value = value; return; } //如果Entry key对应的k为为null那么清空所有key为null的数据 *2* if (k == null) { replaceStaleEntry(key, value, i); return; } } //如果上述都不满足,间接增加 *3* tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}//哈希值private final int threadLocalHashCode = nextHashCode();private static final int HASH_INCREMENT = 0x61c88647;//返回下一个哈希值private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
3.1解决 Hash 抵触
ThreadLocalMap
尽管是相似 Map
构造的数据结构,但它并没有实现 Map
接口。它不反对 Map
接口中的 next
办法,这意味着 ThreadLocalMap
中解决 Hash 抵触的形式并非 拉链表 形式。
实际上,ThreadLocalMap
采纳线性探测的形式来解决 Hash 抵触。所谓线性探测,就是依据初始 key 的 hashcode 值确定元素在 table 数组中的地位,如果发现这个地位上曾经被其余的 key 值占用,则利用固定的算法寻找肯定步长的下个地位,顺次判断,直至找到可能寄存的地位。
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null;}
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread());//获取以后线程的ThreadLocalMap if (m != null)//如果ThreadLocalMap不为空 m.remove(this);//调用ThreadLocalMap的remove办法}/*** ThreadLocalMap#remove*/private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length;//table的长度 int i = key.threadLocalHashCode & (len-1);//应用哈希值取模(求余操作) for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } }}/*** ThreadLocalMap.Entry#clear*/public void clear() { this.referent = null;//值设置为null}/*** ThreadLocal#expungeStaleEntry*/private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i;}
4.ThreadLocal 个性
ThreadLocal和Synchronized都是为了解决多线程中雷同变量的拜访抵触问题,不同的点是
- Synchronized是通过线程期待,就义工夫来解决拜访抵触
- ThreadLocal是通过每个线程独自一份存储空间,就义空间来解决抵触,并且相比于Synchronized,ThreadLocal具备线程隔离的成果,只有在线程内能力获取到对应的值,线程外则不能拜访到想要的值。
正因为ThreadLocal的线程隔离个性,使他的利用场景相对来说更为非凡一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具备不同的数据正本的时候,就能够思考采纳ThreadLocal。
5.4.ThreadLocal 内存泄露问题
ThreadLocalMap
的 Entry
继承了 WeakReference
,所以它的 key (ThreadLocal
对象)是弱援用,而 value (变量正本)是强援用。
- 如果
ThreadLocal
对象没有内部强援用来援用它,那么ThreadLocal
对象会在下次 GC 时被回收。 - 此时,
Entry
中的 key 曾经被回收,然而 value 因为是强援用不会被垃圾收集器回收。如果创立ThreadLocal
的线程始终继续运行,那么 value 就会始终得不到回收,产生内存泄露。
那么如何防止内存透露呢?
办法就是:应用 ThreadLocal
的 set
办法后,显示的调用 remove
办法 。
ThreadLocal<String> threadLocal = new ThreadLocal();try { threadLocal.set("xxx"); // ...} finally { threadLocal.remove();}
关注公众号:java宝典