高併發和ThreadLocal以及記憶體洩漏
併發程式設計
首先感謝https://blog.csdn.net/iter_zc/article/details/39546405這個系列的作者。之前接觸過高併發,但是都是在斷斷續續的接觸和學習,沒有一個結構化的學習。我的博文就是在看了他的講解之後自己理解的。如果我寫的不夠好,大家可以去看一下大神寫的,希望大家都可以提升自己的技術。
1. 併發程式設計的場景
java裡面涉及到併發問題大多是多執行緒的問題,如果資料是無狀態的(不會發生變化的)或者說執行緒封閉的,那麼就不需要考慮多執行緒下的安全問題。如果資料必須是多個執行緒共享的,那麼就必須要要考慮必要的解決方式。通常我們在專案裡面遇見的多執行緒問題不是特別多,這是因為大多的資料可以滿足前面兩點,這也是正確的思路。我認為我們在專案裡面應該儘量把變數滿足無狀態和執行緒封閉的要求,在保證效率的情況下,儘量避免執行緒安全問題。
2.如何避免併發問題
1.資料無狀態
主要是一些只讀不寫的資料
2.執行緒封閉
執行緒封閉指的是資料是每個執行緒單獨持有的,而不是共享。比如我們寫web工程,其實理論上來說肯定是有執行緒安全的問題。但是tomcat會為每個請求建立一個執行緒直到請求結束。所以我們寫的內部邏輯都是執行緒封閉的。但是在邏輯內部,如果還是存在多執行緒那麼就可以考慮是否可以執行緒封閉。
#解決方式 ThreadLocal類
原始碼解讀:ThreadLocal類最主要的三個方法就是set get remove
1.set()
public void set(T value) {<br>
//呼叫JNI 獲取當前執行緒<br/>
Thread t = Thread.currentThread();<br>
//下面介紹
ThreadLocalMap map = getMap(t);<br>
if (map != null)<br>
map.set(this, value);<br>
else
createMap(t, value);
}
//這個值是當前執行緒持有的一個變數
ThreadLocalMap getMap (Thread t) {
return t.threadLocals;
}
可以看出來ThreadLocalMap是當前執行緒持有的一個變數 繼續點進去
ThreadLocal.ThreadLocalMap threadLocals = null;<br/>
//這個類是ThreadLocal類的內部類
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
上面的程式碼可以看出這個ThreadLocalMap這個類維護了一個Entry的內部類,Entry這個類繼承了 WeakReference<> 這個類,WeakReference在java裡面就是代表弱引用,每次JVM在進行GC的時候都會清除這個ThreadLocal<?>類的物件(為什麼使用這種引用的原因後面講)。這個類維護了一個key value型別的對映,key就是ThreadLocal物件。value就是實際的值。而ThreadLocalMap這個類內部有一個Entry陣列來儲存多個Entry物件。
接下來看map.set方法 在當前已經存在threadLocal的情況下
private void set(ThreadLocal<?> key, Object value) {
//獲取陣列
Entry[] tab = table;
//獲取陣列長度
int len = tab.length;
//hreadLocalHashCode 是ThreadLocal類裡面通過ActomicInteger來原子性記錄生成的下一個物件的雜湊值
//下面這段程式碼就是通過雜湊值來解決map的衝突問題,並且定位這個entry在陣列的位置
//https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/81124944這篇文章對雜湊演算法和ThrreadLocal裡面哈 希//演算法的介紹
int i = key.threadLocalHashCode & (len-1);
//迴圈定位 檢視是否已經存在衝突,如果已經衝突,那麼找下一個為空的位置
for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果這個值已經存在 那麼採取替換的方式
if (k == key) {
e.value = value;
return;
}
//如果這個位置還沒有使用,那麼把key,value放在相應位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果已經存在衝突,那麼找到可以存放的位置 i 並新建entry並把長度加一
tab[i] = new Entry(key, value);
int sz = ++size;
//如果沒有被回收的無用的key,並且長度大於臨界值的時候,那麼採取擴容
//cleanSomeSlots 這個方法會把key值為null的Entry刪除
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
</code>
createMap() 第一次set的時候
<code>
//建立map 並把它賦值給當前執行緒
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
</code>
2.get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//找到對應的Entry類 首先按照 key.threadLocalHashCode & (len-1(和set採用相同的演算法))快速定位到位置,
//如果不對,那麼再呼叫 getEntryAfterMiss(key, i, e) 方法迴圈遍歷尋找
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果不存在 那麼呼叫下面的方法
return setInitialValue();
}
private T setInitialValue() {
//返回值為null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//再次驗證 是否存在map
if (map != null)
map.set(this, value);
else
//這個方法上面介紹過 就是建立新的map,並且以當前threadlocal為key value為null建立entry,作為陣列的第一個元素
createMap(t, value);
return value;
}
</code>
3.remove()
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//定位 運用hash雜湊演算法
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//清除引用 this.referent = null;
e.clear();
//清除數組裡面其他的被回收的key值
expungeStaleEntry(i);
return;
}
}
}
3.Threadlocal的記憶體洩露問題
記憶體洩漏和記憶體溢位的區別,前者是已經沒有作用的物件沒有得到回收。後者是JVM需要的記憶體過大。可以見記憶體洩漏是誘導記憶體溢位的一個原因。
首先看一張引用圖
Key是弱引用物件,每次GC的時候都會回收這個物件。(JAVA裡面四種引用大家可以網上查一下),這樣一旦發生GC,那麼key值將會被回收,此時value因為還有強引用,所以沒辦法回收。但是因為key的回收,此時這個entry就沒有任何存在的意義,但是Thread那條引用鏈導致這個entry沒辦法被回收。這樣就會導致記憶體洩露的問題。上面我介紹的remove方法就可以手動清除當前entry,他的做法就是把當前的entry設定為null。所以我們再使用這個方法的時候一定要記得remove().防止記憶體洩露。
既然使用弱引用存在記憶體洩露的問題,那麼為什麼沒有使用強引用呢。我認為首先使用強引用的話,那麼即使threadLocal物件被回收,那麼他的靜態內部類(預設修飾符,只能包內呼叫)map還是沒辦法去清理entry物件,同樣會導致記憶體洩漏的發生。並且很難去修復。
如果使用弱引用,即使key值被回收,呼叫remove方法就可以去掉entry物件。並且在呼叫set時候都會在某種情況下觸發清除entry數組裡面entry的key為空(已經被回收)的物件的清除工作。注:set會在發生衝突的情況下觸發。
4.使用的程式碼以及簡單驗證
public class ThreadLocalTest {
/**
* 初始值設定為0
*/
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
/**
* 對比上面 number不是執行緒獨佔的
*/ private static int number = 0;
private static class TestLocal implements Runnable{
public void run() {
try {
int newValue = threadLocal.get()+1;
threadLocal.set(newValue);
number++;
System.out.println("number的值"+number);
System.out.println("threadlocal的值為"+threadLocal.get());
} catch (Exception e) {
e.printStackTrace();
}finally {
threadLocal.remove();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new TestLocal()).start();
}
}
}
列印結果
number的值2
number的值2
threadlocal的值為1
threadlocal的值為1
number的值3
threadlocal的值為1
number的值4
threadlocal的值為1
number的值6
number的值5
threadlocal的值為1
number的值7
threadlocal的值為1
threadlocal的值為1
number的值8
threadlocal的值為1
number的值9
threadlocal的值為1
number的值10
threadlocal的值為1
由此可以證明threadLocal是執行緒私有的。
5.應用
實際業務我還沒有使用過,因為我的業務暫時還沒有用到。但是我瞭解到spring裡面的事務就是通過threadLocal實現的。大家可以看一下https://blog.csdn.net/zdp072/article/details/39214867 裡面講的很清晰,程式碼邏輯也非常好。
感言
上面有總結不對的地方的話,歡迎大家批評指正。
原始碼有很多我們可以借鑑的地方,比如數組裡面如果想快速定位到某個值所在的下表怎麼辦?除了使用java提供的原生方法外,可以借鑑這裡
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();
//https://www.cnblogs.com/ilellen/p/4135266.html 介紹了這個神奇的值
final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//定位的方法
int i = key.threadLocalHashCode & (len-1);
還有就是適當的使用弱引用防止記憶體溢位,同時也要考慮記憶體洩漏的可能性。