java线程池 锁_java多线程——锁
這是多線程系列第四篇,其他請關注以下:
如果你看過前面幾篇關于線程的文字,會對線程的實現原理了然于胸,有了理論的支持會對實踐有更好的指導,那么本篇會偏重于線程的實踐,對線程的幾種應用做個簡要的介紹。
本篇主要內容:
線程安全的分類
線程同步的實現方式
鎖優化
線程安全分類
線程安全并非是一個非真既假的二元世界,如果按照線程安全的“安全程度”來排序的話,java中可以分為以下幾類
不可變。對數據類型修飾為final類型的,就可以保證其實不可變的(reference 對象除外,final對象屬性不保證,只保證內存地址)。不可變的對象一定是線程安全,比如String類,它是一個典型的不可變對象,當調用它的subString()、replace()和concat()的方法都不會影響它原來的值,只會返回一個新構造的字符串對象。
絕對線程安全。這個定義是極其嚴格的,一個類要達到,不管運行時環境如何,調用者都不需要任何額外的同步措施。
相對線程安全。這個就是我們通常所講的線程安全,它保證對對象的單獨操作是線程安全的,不需要做額外保證工作,但對于特定順序的連續調用,可能需要調用端采用額外的同步手段來保證調用的正確性。
線程兼容。這是指對象本身并非線程安全,可以通過調用端正確地使用同步手段來保證對象在并發環境中可以安全地使用。我們常用的非線程安全類,都屬于這個范疇。
線程對立。這個是指無論調用端是否采用同步措施,都無法在多線程環境中并發使用代碼。這種我們應該避免。
上述中可能對絕對安全和相對安全,并不是很好區分,我們采用一個示例來區分:
public class VectorTest {
private Vector vector = new Vector();
public void remove() {
new Thread() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}.start();
}
public void print() {
new Thread() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
}.start();
}
public void add(int data) {
vector.add(data);
}
public static void main(String[] args) {
VectorTest test = new VectorTest();
for (int j=0;j<100;j++){
for (int i = 0; i < 10; i++) {
test.add(i);
}
test.remove();
test.print();
}
}
}
上述代碼中運行會報錯:ArrayIndexOutOfBoundsException的異常,這個異常是在print方法里面出現的,當remove線程刪除 一個元素之后,print方法正好執行到vector.get()方法,此時就會出現這個異常。
我們知道vector是線程安全的,它的get()、remove()、size()、add()方法都采用synchronize 進行同步了,但是多線程的情況下,如果不對方法調用端做額外同步的情況下,仍然不是線程安全的。這就是我們說的相對線程安全,它能不保證何時,調用者都不需要任何額外的同步措施。
線程安全同步的實現方式
互斥同步是常見的一種并發正確性保障手段。同步是指多個線程并發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用。java中常見的互斥同步手段就是synchronize和ReentrantLock。
想必這兩種加鎖方式對多線程有了解的人都知道。具體用法我們不再探討。我們聊一聊這兩者的不同和具體場景應用。
synchronize在前面的文字中我們也講過了,它屬于重度鎖,由于jvm線程是映射于操作系統原生線程,在阻塞或者喚醒線程時候,需要從用戶態轉換到內核上,這個耗費有時候會耗費時間超過代碼執行時間,所以jvm會對一些代碼執行短的同步代碼采用自旋鎖等方式,避免頻繁的切入到核心態之中。
synchronize是jvm提供的一種內置鎖,被jvm推薦使用,它寫出的代碼相對比較簡單緊湊,只有當內置鎖滿足不了需求的時候,再來用ReentrantLock。
ReentrantLock
那么ReentrantLock能提供哪些高級功能?我們看個示例;
public void synA() {
synchronized (lockA) {
synchronized (lockB) {
//doSomeThing....
}
}
}
public void synB() {
synchronized (lockB) {
synchronized (lockA) {
//doSomeThing....
}
}
}
上述通過synchronized的代碼,在多個線程分別調用synA和synB的時候容易發生死鎖的問題。要想避免,只能在編寫的時候強制要求所有的調用順序一致。而在ReentrantLock可以采用輪詢鎖的方式來避免此種問題。
public void tryLockA() {
long stopTime = System.currentTimeMillis() + 10000l
while (true) {
if (lockA.tryLock()) {
try {
if (lockB.tryLock()) {
try {
// doSomeThing.....
return;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
if (System.currentTimeMillis() > stopTime) {
return;
}
}
}
上述trylock的方式,如果不能獲取到所需要的鎖,那么可以采用輪詢的方式來獲取,從而讓程序從新獲取控制權,而且會釋放已經獲得的鎖。另外trylock還提供有定時重載方法,方便你在一定時間內獲得鎖,如果指定時間內不能給出結果,會零程序結束。
ReentrantLock除了提供可輪詢,定時鎖以外,還可以提供可中斷的鎖獲取操作,以便獲取可取消的操作中使用枷鎖。另外還提供了鎖獲取操作,公平隊列以及非塊結構的鎖。這些功能都極大的豐富了對鎖操作的可定制性。
當然了,你如果ReentrantLock的這些高級功能你并用不上,還是推薦采用synchronized。在性能上synchronized在java6以后已經可以和ReentrantLock相平衡了,而且據官方據說,這一方面的性能未來還會加強,因為它屬于jvm的內置屬性,能執行一些優化,例如對線程封閉鎖對象的鎖消除優化,以及增加鎖的顆粒度來消除鎖同步等。這些在ReentrantLock是很難得到實現的。
鎖優化
上述我們也了解過,多線程對資源競爭的時候會令其他沒有競爭到的線性進行阻塞等待,而阻塞以及喚醒又要需要內核的調度,這對有限的cpu來說代價過于龐大,于是jvm就在鎖優化上花費了大量的精力,以提高執行效率。
我們看看常見的鎖優化方式。
自旋鎖
在共享數據鎖定的狀態下,有很多方法都是只會持有很短的一段時間,為了這么一小段時間而讓線程掛起和恢復很不值得。那么jvm就讓等待鎖的線程稍等一下,但不放棄相應的執行時間。以此看等待的線程是否很快釋放,如此就減少了線程調度的壓力。如果鎖被占用時間很短,這個效果就很好,如果時間過長,就白白浪費了循環的資源,而且會帶來資源浪費。
自適應自旋鎖
自旋鎖無法依據鎖被占用時間長短來處理,后續就引入了,自適應的自旋鎖,自選的時間不再固定了,而是由前一次在同一個鎖的自選時間以及擁有的狀態來決定的。如此就會變的智能起來。
鎖消除
鎖消除是指jvm即時編譯器在運行時候,對一些代碼上要求同步,但被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除檢測主要依據是來源于逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去,就把他們當做棧上數據對待,認為是線程私有的,同步也就無需進行了。
鎖粗化
編寫代碼的時候,總是推薦將同步塊作用范圍越小越好,如果一系列的操作都是對一個對象反復枷鎖和解鎖,甚至出現在循環體中,及時沒有線程競爭,也會導致不必要的性能損耗。那么對于此種代碼,jvm會擴大其鎖的顆粒度,對這一部分代碼只采用一個同步操作來進行。
輕量級鎖
輕量級鎖是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥產生的性能消耗。jvm中對象header分為兩部分信息,第一部分用于存儲對象自身的運行數據,稱為”Mark Word”,它是實現輕量級鎖的關鍵。另外一部分用于存儲執行方法區對象類型數據的指針。當代碼進入同步塊的時候,如果此同步對象沒有被鎖定,則把“Mark world”中指向鎖記錄的指針,標記為“01”。
如果Mark Word更新成功,線程擁有了該對象的鎖,則把執行鎖標位的指針標記為”00”,如果更新失敗,并且當前對象的Mark word 沒有指向當前線程的棧幀,則說明鎖對象已經被其他線程搶占了。如果有兩條以上 線程爭用同一個鎖,那輕量級鎖就不再奏效了,鎖標記為”10“,膨脹為重量級鎖。
輕量級鎖是基于,絕大部分的鎖定,在同步周期內是不存在競爭的,所以以此來減輕互斥產生的性能消耗。當然如果存在鎖競爭,除了互斥還會避免使用互斥量的開銷,還有額外產生同步修改標記位的操作。
偏向鎖
偏向鎖是在無競爭情況下把整個同步都消除了,連CAS更新操作也不做了,它會偏向第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要同步,當另外一個線程嘗試獲取這個鎖的時候,則宣告偏向模式結束。
-----------------------------------------------------------------------------
想看更多有趣原創的技術文章,掃描關注公眾號。
關注個人成長和游戲研發,推動國內游戲社區的成長與進步。
總結
以上是生活随笔為你收集整理的java线程池 锁_java多线程——锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第十六章:Java内存模型——Java并
- 下一篇: 构建之法08