JAVA多執行緒并發容易引發的問題及如何保證執行緒安全
之前的章節中我們介紹了在并發時,容易引發的問題及如何保證執行緒安全,本章節我們主講JAVA并發中的無同步方案:ThreadLocal
無同步方案:
1.可重入代碼:
可重入代碼:可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼,而在控制權回傳之后,原來的程式不會出現任何的錯誤,可重入代碼有一些公共的特征,例如不依賴存盤在堆上的資料和公用的系統資源、用到的狀態量都由引數傳入、不呼叫非可重入的方法等,簡而言之:如果一個方法,它的回傳結果是可以預測的,只要輸入了相同的資料,就能回傳相同的結果,那它就滿足可重入性的要求,當然也就是執行緒安全的,
2.執行緒本地存盤(ThreadLocal):
目錄執行緒本地存盤:如果一段代碼所需要的資料必須與其他代碼共享,那就看看這些共享資料的代碼是否能保證在同一個執行緒中執行?如果能保證,我們就可以把共享資料的可見范圍限制在同一個執行緒之內,這樣,即是無同步也能做到避免資料爭用,
- 1.ThreadLocal 介紹
- 2.ThreadLocal 應用
- 3.ThreadLocal 原始碼決議
- 3.1解決 Hash 沖突
- 4.ThreadLocal 特性
- 5.4.ThreadLocal 記憶體泄露問題
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 = https://www.cnblogs.com/java-bible/archive/2020/11/02/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 = https://www.cnblogs.com/java-bible/archive/2020/11/02/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 = https://www.cnblogs.com/java-bible/archive/2020/11/02/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 = https://www.cnblogs.com/java-bible/archive/2020/11/02/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寶典
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/199891.html
標籤:其他
下一篇:Java創建多執行緒的幾種方式


