摘要:如果釋放的次數多了,會得到一個異常反之則會導致當前線程一直持有該鎖,導致其他線程無法進入臨界區。,若沒有參數,當前線程會嘗試獲得鎖,如果申請鎖成功,則返回,否則立即返回。閉鎖閉鎖是一種同步工具類,可以延遲線程的進度直到其到達終止狀態。
為了更好地支持并發程序,“鎖”是較為常用的同步方法之一。在高并發環境下,激勵的鎖競爭會導致程序的性能下降。
所以我們將在這里討論一些有關于鎖使用和問題以及一些注意事項。
重入鎖可以完全替代Synchronized關鍵字,但其必須顯式的調用unlock。建議視為Synchronized的高級版,比起Synchronized關鍵字,其可定時、可輪詢并含有可中斷的鎖獲取操作,公平隊列以及非塊結構的鎖。
/** * 重入鎖演示 * */ public class ReeterLock implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); public static int i = 0; @Override public void run() { for (int j=0;j<10000;j++){ //手動上鎖,可以上N把,這里是為了演示 lock.lock(); lock.lock(); lock.lock(); try { i ++; } finally { //無論如何必須釋放鎖,上幾把 釋放幾把 lock.unlock(); lock.unlock(); lock.unlock(); } } } public static void main(String[] a) throws InterruptedException { ReeterLock rl = new ReeterLock(); Thread t1 = new Thread(rl); Thread t2 = new Thread(rl); t1.start(); t2.start(); t1.join(); t2.join(); System.out.print(i); } }
那么我們可以明顯的看到重入鎖保護著臨界區資源i,確保多線程對i操作的安全。在demo中我們也是加了3次鎖并釋放了3次鎖。
需要注意的是,如果同一線程多次獲得鎖,那么在釋放鎖的時候,也必須釋放相同次數。如果釋放的次數多了,會得到一個java.lang.IllegalMonitorStateException異常;反之則會導致當前線程一直持有該鎖,導致其他線程無法進入臨界區。
中斷響應:ReentrantLock.lockInterruptibly()對于synchronized來說,如果一個線程在等待鎖,那么結果只有兩種情況,要么它獲得這把鎖繼續執行,要么它就保持等待。而使用重入鎖,則提供另外一種可能,那就是線程可以被中斷。也就是在等待鎖,程序可以根據需要取消對鎖的請求。有些時候,這么做是非常有必要的。
lockInterruptibly()方法是一個可以對中斷進行響應的鎖申請動作,即在等待鎖的過程中,中斷響應。
public class IntLock implements Runnable{ public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; /** * 控制加鎖順序,制造死鎖 * @param lock */ public IntLock(int lock) { this.lock = lock; } @Override public void run() { try { /** * 1號線程,先占用 1號鎖,再申請 2號鎖 * 2號線程,先占用 2號鎖,再申請 1號鎖 * 這樣就很容易造成兩個線程相互等待. */ if (lock == 1){ //加入優先響應中斷的鎖 lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 進入..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /** * 這時候,1號線程 想要持有 2號鎖 ,但是2號線程已經先占用了2號鎖,所以1 號線程等待. * 2號線程也一樣,占用著2號鎖 不釋放,還想申請1號鎖,而1號鎖 被1號線程占用且不釋放. */ lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 完成..."); }else { lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 進入..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 完成..."); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + " 被中斷,報異常..."); e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName() + " 釋放..."); lock1.unlock(); } if (lock2.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName() + " 釋放..."); lock2.unlock(); } System.out.println(Thread.currentThread().getName() + " 線程退出..."); } } public static void main(String[] a) throws InterruptedException { IntLock re1 = new IntLock(1); IntLock re2 = new IntLock(2); Thread t1 = new Thread(re1," 1 號線程 "); Thread t2 = new Thread(re2," 2 號線程 "); t1.start(); t2.start(); //主線程sleep 2秒,讓兩個線程相互競爭資源.造成死鎖 Thread.sleep(2000); //中斷2號線程 t2.interrupt(); /* 執行結果: 1 號線程 進入... 2 號線程 進入... 2 號線程 被中斷,報異常... // 執行 t2.interrupt(); java.lang.InterruptedException 2 號線程 釋放... at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) 2 號線程 線程退出... at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) 1 號線程 完成... // 只有1號線程能執行完成 at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) 1 號線程 釋放... at com.iboray.javacore.Thread.T3.IntLock.run(IntLock.java:55) 1 號線程 釋放... at java.lang.Thread.run(Thread.java:745) 1 號線程 線程退出... */ } }鎖申請等待限時:ReentrantLock.tryLock
除了等待外部通之外,避免死鎖還有另外一種方法,就是限時等待,給定一個等待時間讓線程自動放棄。
tryLock(時長,計時單位),若超過設定時長還沒得到鎖就返回false,若成功獲得鎖就返回true。
tryLock(),若沒有參數,當前線程會嘗試獲得鎖,如果申請鎖成功,則返回true,否則立即返回false。這種模式不會引起線程等待,因此不會產生死鎖。
public class TimeLock implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); @Override public void run() { System.out.println(Thread.currentThread().getName() + " 申請資源..."); try { //申請3秒,如果獲取不到,返回false,退出. if (lock.tryLock(5, TimeUnit.SECONDS)) { System.out.println(Thread.currentThread().getName() + " 獲得資源,開始執行..."); //持有鎖6秒 Thread.sleep(6000); System.out.println(Thread.currentThread().getName() + " 執行完成..."); }else { System.out.println(Thread.currentThread().getName() + " 申請鎖失敗..."); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + " 中斷..."); e.printStackTrace(); }finally { if (lock.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName() + " 釋放鎖..."); lock.unlock(); } } } public static void main(String[] a) throws InterruptedException { TimeLock re = new TimeLock(); Thread t1 = new Thread(re," 1 號線程 "); Thread t2 = new Thread(re," 2 號線程 "); t1.start(); t2.start(); /* 執行結果: 1 號線程 申請資源... 2 號線程 申請資源... 1 號線程 獲得資源,開始執行... 2 號線程 釋放鎖... //等待了5秒后,依然申請不到鎖,就返回false 1 號線程 執行完成... 1 號線程 釋放鎖... */ } }
由于占用鎖的線程會持有鎖長達6秒,故另一個線程無法在5秒的等待時間內獲取鎖,因此,請求鎖會失敗。
公平鎖:ReentrantLock(true)在大多數情況下,鎖的申請都是非公平的。也就是說,線程1首先請求了鎖A,接著線程2也請求了鎖A。那么當鎖A可用時,是線程1還是線程2可以獲得鎖呢?顯然這是不一定的。系統只會從這個鎖的等待隊列中隨機挑選一個,因此不能保證其公平性。
公平鎖會按照實際的先后順序,保證先到先得,它不會產生饑餓,只要排隊,最終都可以等到資源。在創建重入鎖時,通過有參構造函數,傳入boolean類型的參數,true表示是公平鎖。實現公平所必然要維護一個有序隊列,所以公平鎖的實現成本高,性能相對也非常低,默認情況下,鎖是非公平的。
public class ReentrantLockExample3 implements Runnable{ //創建公平鎖 public static ReentrantLock lock = new ReentrantLock(true); static int i = 0; @Override public void run() { for (int j = 0;j<5;j++){ lock.lock(); try { i++; System.out.println(Thread.currentThread().getName() + " 獲得鎖 " + i); } finally { lock.unlock(); } } } public static void main(String[] a) throws InterruptedException { ReentrantLockExample3 re = new ReentrantLockExample3(); Thread t1 = new Thread(re," 1 號線程 "); Thread t2 = new Thread(re," 2 號線程 "); Thread t3 = new Thread(re," 3 號線程 "); Thread t4 = new Thread(re," 4 號線程 "); t1.start(); t2.start(); t3.start(); t4.start(); /* 執行結果: 1 號線程 獲得鎖 1 2 號線程 獲得鎖 2 3 號線程 獲得鎖 3 4 號線程 獲得鎖 4 1 號線程 獲得鎖 5 2 號線程 獲得鎖 6 3 號線程 獲得鎖 7 4 號線程 獲得鎖 8 ..... 4 號線程 獲得鎖 16 1 號線程 獲得鎖 17 2 號線程 獲得鎖 18 3 號線程 獲得鎖 19 4 號線程 獲得鎖 20 */ } }ReentrantLock的以上幾個重要的方法
lock() 獲取鎖,如果鎖被占用,則等待
lockInterruptibly() 獲取鎖,但優先響應中斷
tryLock() 嘗試獲取鎖,如果成功返回true,否則返回false。該方法不等待,立即返回。
tryLock(long time,TimeUnit unit) 在給定時間內獲取鎖。
unlock() 釋放鎖。
就重入鎖實現來看,它主要集中在Java 層面。在重入鎖實現中,主要包含三個要素:
原子狀態。原子狀態使用CAS操作來存儲當前鎖的狀態,判斷鎖是否已經被別的線程持有。
等待隊列。所有沒有請求成功的線程都進入等待隊列進行等待。當有線程釋放鎖后,系統就從當前等待隊列中喚醒一個線程繼續工作。
阻塞原語park()和unpack(),用來掛起和恢復線程。沒有得到鎖的線程將會被掛起。
重入鎖的好搭檔:Condition條件如果大家理解了Object.wait()和Object.notify()方法的話,就能很容易地理解Condition對象了。它和wait()和notify()方法的作用是大致相同的。但是wait()方法和notify()方法是和synchronized關鍵字合作使用的,而Condtion是與重入鎖相關聯的。通過Lock接口(重入鎖就實現了這個接口)的Condtion newCondition()方法可以生成一個與當前重入鎖綁定的Condition實例。利用Condition對象,我們就可以讓線程在合適的時間等待,或者在某一個特定的時刻得到通知,繼續執行。
Condition接口提供的基本方法如下:
void await() throws InterruptedException; void awaitUninterruptibly(); long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException; void signal(); void signalAll();
以上方法含義如下
await()方法會使當前線程等待,同時釋放當前鎖,當其他線程中使用signal()或者signalAll()方法時候,線程會重新獲得鎖并繼續執行。或者當線程被中斷時,也能跳出等待。這和Object.wait()方法很相似。
awaitUninterruptibly()和await()方法類似,但它不會再等待過程中響應中斷。
singal() 用于喚醒一個等待隊列中的線程。singalAll()是喚醒所有等待線程。
public class ConditionExample implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); public static Condition condition = lock.newCondition(); @Override public void run() { try { lock.lock(); System.out.println(Thread.currentThread().getName() + " 獲取到鎖..."); //等待 condition.await(); System.out.println(Thread.currentThread().getName() + " 執行完成"); } catch (InterruptedException e) { e.printStackTrace(); }finally { //釋放鎖 lock.unlock(); System.out.println(Thread.currentThread().getName() + " 釋放鎖"); } } public static void main(String[] a) throws InterruptedException { ConditionExample re = new ConditionExample(); Thread t1 = new Thread(re,"1 號線程 "); t1.start(); //主線程sleep,1號線程會一直等待.直到獲取到1號線程的鎖資源,并將其喚醒. Thread.sleep(2000); //獲得鎖 lock.lock(); //喚醒前必須獲得當前資源對象的鎖 condition.signal(); //釋放鎖 lock.unlock(); } }ReadWriteLock讀寫鎖
ReadWriteLock是JDK5中提供的讀寫分離鎖。讀寫分離鎖可以有效地幫助減少鎖競爭,以提升系統性能。用鎖分離的機制來提升性能非常容易理解,比如線程A1、A2、A3進行寫操作,B1、B2、B3進行讀操作,如果使用重入鎖或者內部鎖,則理論上說所有讀之間、讀與寫之間、寫和寫之間都是串行操作。當B1進行讀取時,B2、B3則需要等待鎖。由于讀操作并不對數據的完整性造成破壞,這種等待顯然是不合理的。因此,讀寫鎖就有了發揮功能的余地。
在這種情況下,讀寫鎖運行多個線程同時讀。但是考慮到數據完整性,寫寫操作和讀寫操作間依然是需要互相等待和持有鎖的。總的來說,讀寫鎖的訪問約束如下表。
讀 | 寫 | |
---|---|---|
讀 | 非阻塞 | 阻塞 |
寫 | 阻塞 | 阻塞 |
如果在系統中,讀的次數遠遠大于寫的操作,讀寫鎖就可以發揮最大的功效,提升系統的性能。
栗子:
public class ReadWriteLockExample { //創建普通重入鎖 private static Lock lock = new ReentrantLock(); //創建讀寫分離鎖 private static ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(); //創建讀鎖 private static Lock readLock = rwlock.readLock(); //創建寫鎖 private static Lock writeLock = rwlock.writeLock(); private int value; public Object HandleRead(Lock lock) throws InterruptedException { try { //上鎖 lock.lock(); //模擬處理業務 Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + " Read..."); return value; } finally { //釋放鎖 lock.unlock(); } } public void HandleWrite(Lock lock,int index) throws InterruptedException { try { lock.lock(); Thread.sleep(1000); value = index; System.out.println(Thread.currentThread().getName() + " Write..."); }finally { lock.unlock(); } } public static void main(String[] a ) throws InterruptedException { final ReadWriteLockExample rwle = new ReadWriteLockExample(); //創建讀方法 Runnable readR = new Runnable() { @Override public void run() { try { //rwle.HandleRead(lock); //普通鎖 rwle.HandleRead(readLock); } catch (InterruptedException e) { e.printStackTrace(); } } }; //創建寫方法 Runnable writeR = new Runnable() { @Override public void run() { try { //rwle.HandleWrite(lock,new Random().nextInt()); //普通鎖 rwle.HandleWrite(writeLock,new Random().nextInt()); } catch (InterruptedException e) { e.printStackTrace(); } } }; //18次讀 for (int i=0;i<18;i++){ Thread s = new Thread(readR); s.start(); } //2次寫 for (int i=18;i<20;i++){ Thread s = new Thread(writeR); s.start(); } /** * 結論: * * 用普通鎖運行,大約執行20秒左右 * * 用讀寫分離鎖,大約執行3秒左右 * */ } }
在讀鎖和寫鎖之間的交互可以采用多種實現方式。ReadWriteLock中的一些可選實現包括:
釋放優先:當一個寫入操作釋放寫入鎖時,并且隊列中同時存在讀線程和寫線程,那么應該優先選擇哪一個線程。
讀線程插隊:如果鎖是由讀線程持有,但是寫線程還在等待,是否允許新到的讀線程獲得訪問權,還是應在寫線程后面等待?若允許的話可以提高并發性但是可能造成寫線程的饑餓。
重入性:讀取鎖和寫入鎖是否可重入。
降級和升級:若一個線程持有寫鎖可否在繼續持有寫鎖的狀態下獲取讀鎖?這可能會使寫鎖“降級”為讀鎖。讀鎖是否優先于其它正在等待的讀線程和寫線程而升級為一個寫鎖?在大多數讀寫鎖實現中不支持“升級”,因為這樣容易死鎖(兩個讀線程試圖同時升級為寫鎖,那么二者都不會釋放寫鎖)。
閉鎖閉鎖是一種同步工具類,可以延遲線程的進度直到其到達終止狀態。閉鎖的作用相當于一扇門:在閉鎖到達結束狀態之前,這扇門一直是關閉的,并且沒有任何線程能通過,當到達結束狀態時,這扇門會打開并允許所有的線程通過。當閉鎖到達結束狀態后,將不會再改變狀態,因此這扇門將永遠保持打開狀態。閉鎖可以用來確保某些活動直到其他活動都完成才繼續執行,例如:
確保某個計算在其需要的所有資源都被初始化后才繼續執行。二元閉鎖(包括兩個狀態)可以用來表示“資源R已經被初始化”,而所有需要R的操作都必須先在這個比鎖上等待。
確保某個服務在其依賴的所有其他服務都已經啟動之后才啟動。每個服務都有一個相關的二元閉鎖。當啟動服務S時,將首先在S依賴的其他服務的閉鎖上等待,在所有依賴的服務都啟動后會釋放閉鎖S,這樣其他依賴S的服務才能繼續執行。
等待直到某個操作的所有參與者(例如,在多玩家游戲中的所有玩家)都就緒再繼續執行。在這種情況中,當所有的玩家都執行就緒時,閉鎖將到達結束狀態。
CountDownLatchCountDownLatch 就是一種靈活的閉鎖實現,可以在上述的各種情況中使用,它可以使一個或多個線程等待一組時間發生CountDown在英文中意為倒計時,Latch為門閂。閉鎖的狀態包括一個計數器,該計數器被初始化為一個正數,表示需要等待的事情數量。countDown方法遞減計數器,表示有一個事件已經發生了,而await方法等待計數器達到零,這表示所有需要等待的事情都已經發生。如果計數器的值是非零,那么await會一直阻塞直到計數器為零,或者等待中的線程中斷,或者等待超時。因此,這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計時結束,再開始執行。
CountDownLatch的構造函數接收一個整數作為參數,即當前這個計數器的技術個數。
public CountDownLatch(int count)
下面這個簡單的示例,演示了CountDownLatch的使用。
public class CountDownLatchExample implements Runnable{ static final CountDownLatch cdl = new CountDownLatch(10); static final CountDownLatchExample cdle = new CountDownLatchExample(); @Override public void run() { try { Thread.sleep(new Random().nextInt(10) * 1000); System.out.println(Thread.currentThread().getName() + " 部件檢查完畢..."); //一個線程完成工作,倒計時器減1 cdl.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] a) throws InterruptedException { ExecutorService exec = Executors.newFixedThreadPool(10); for (int i=0;i<10;i++){ exec.submit(cdle); } //等待所有線程完成,主線程才繼續執行 cdl.await(); System.out.println(Thread.currentThread().getName() + " 所有檢查完成,上跑道起飛..."); //關閉線程池 exec.shutdown(); } }FutureTask
FutureTask也可以用做閉鎖。FutureTask表示的計算是通過Callable來實現的,相當于一種可生成結果的Runnable,并且可以處于以下3種狀態:
等待運行(Waiting to run)
正在運行(Running)
運行完成(Completed)
Future.get的行為取決于任務的狀態。如果任務已經完成,那么get會立即返回結果,否則get將阻塞直到任務進入完成狀態,然后返回結果或者拋出異常。FutureTask將計算結果從執行的計算的線程傳遞到獲取這個結果的線程,而FutureTask的規劃確保了這種傳遞過程能夠實現結果的安全發布。
FutureTask在ExeCutor中表示異步任務,此外還可以用來表示一些時間較長的計算,這些計算可以在使用計算結果之前啟動。
信號量技術信號量(Counting Semaphore)用來控制同時訪問某個特定資源的操作數量,或者同時執行某個指定操作的數量。計數信號量還可以用來實現某種資源池,或者對容器施加邊界。
信號量為多線程協作提供了更為強大的控制方法。廣義上說,信號量是對鎖的擴展。無論是內部鎖synchronized還是重入鎖ReentrantLock,一次都只允許一個線程訪問一個資源,而信號量卻可以指定多個線程,同時訪問某一個資源。信號量主要提供了以下構造函數:
public Semaphore(int permits) public Semaphore(int permits,boolean fair) //第二個參數可以指定是否公平
在構造信號量對象時,必須要指定信號量的準入數,即同時能申請多少個許可。當每個線程每次只申請一個許可時,這就相當于指定了同時有多少個線程可以訪問某一個資源。信號量的主要邏輯方法有:
public void acquire() //嘗試獲得一個準入的許可。若無法獲得,則線程會等待,直到有線程釋放一個許可或者當前線程被中斷 public void acquireUninterruptibly()//和acquire()類似,但是不響應中斷 public boolean tryAcquire()//嘗試獲得一個許可,成功true失敗fasle,不會等待,立刻返回 public boolean tryAcquire(long timeout,TimeUnit unit) public void release()//線程訪問資源結束后,釋放一個許可,以使其他等待許可的線程進行資源訪問
栗子:
public class SemaphoreExample implements Runnable { //指定信號量,同時可以有5個線程訪問資源 public static final Semaphore s = new Semaphore(5); @Override public void run() { try { //申請信號量,也可以直接使用 s.acquire(); if (s.tryAcquire(1500, TimeUnit.SECONDS)) { //模擬耗時操作 Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + " 完成了任務.."); //離開時必須釋放信號量,不然會導致信號量泄露——申請了但沒有釋放 s.release(); } } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] a) throws InterruptedException { //申請20個線程 ExecutorService exec = Executors.newFixedThreadPool(20); final SemaphoreExample re = new SemaphoreExample(); for (int i=0;i<20;i++){ exec.submit(re); } exec.shutdown(); } }
Semaphore中管理者一組虛擬的許可(permit),許可的初始數量可通過構造函數來指定。在執行操作時可以先獲得許可(只要還有剩余的許可),并在使用以后釋放許可。如果沒有許可,那么acquire將阻塞直到有許可(或者被中斷或者操作超時)。release方法將返回一個許可給信號量。計算信號量的一種簡化形式是二值信號量,即初始值為1的Semaphore。二值信號量可以用做互斥體(mutex),并具備不可重入的加鎖語義:誰擁有這個唯一的許可,誰就擁有了互斥鎖。
同樣,我們也可以使用Semaphore將任何一種容器變成有界阻塞容器。
CyclicBarrier和之前的CountDownLatch類似,它(循環柵欄)也可以實現線程間的技術等待,但它的功能比CountDownLatch更加復雜強大。它能阻塞一組線程直到某個事件發生。因此,柵欄可以用于實現一些協議,例如“開會一定要在xx地方集合,等其他人到了再討論下一步要做的事情”。
CyclicBarrier的使用場景也很豐富。比如,司令下達命令,要求10個士兵一起去完成一項任務。這時,就會要求10個士兵先集合報道,接著,一起雄赳赳氣昂昂地執行任務。當10個士兵把自己手頭的任務都執行完成了,那么司令才能對外宣布,任務完成!
下面的栗子使用CyclicBarrier演示了上述司機命令士兵完成任務的場景。
public class CyclicBarrierExample { public static class Soldier implements Runnable{ private String name; private CyclicBarrier cyclicBarrier; public Soldier(String name, CyclicBarrier cyclicBarrier) { this.name = name; this.cyclicBarrier = cyclicBarrier; } @Override public void run() { try { System.out.println(name + " 來報道.."); //等待所有士兵到齊 cyclicBarrier.await(); doWork(); //等待所有士兵完成任務 cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } void doWork(){ try { Thread.sleep(new Random().nextInt(10) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + " 任務已完成.."); } } public static class doOrder implements Runnable{ boolean flag; int n; public doOrder(boolean flag, int n) { this.flag = flag; this.n = n; } @Override public void run() { if (flag){ System.out.println("司令 : 士兵 " + n +"個 任務完成"); }else { System.out.println("司令 : 士兵 " + n +"個 集合完畢"); //執行完后 改變完成標記.當下一次調用doOrder時,可以進入if flag = true; } } } public static void main(String[] a){ final int n = 10; //是否完成了任務 boolean flag = false; //創建10個士兵線程 Thread[] allSoldier = new Thread[n]; //創建CyclicBarrier實例 //這里的意思是,等待10個線程都執行完,就執行doOrder()方法 CyclicBarrier c = new CyclicBarrier(n, new doOrder(flag,n)); for (int i=0;i優化鎖 一般我們對于鎖的優化有以下幾個大致方向:
減少鎖的持有時間
減少鎖的請求頻率
使用帶有協調機制的獨占鎖,這些機制允許更高的并發性
鎖分段技術在某些情況下,可以將鎖分解技術進一步擴展為對一組獨立對象上的鎖進行分解,這種情況被稱為鎖分段。例如,在ConcurrentHashMap的實現中使用了一個包含16個鎖的數組,每個鎖保護所有散列桶的1/16,其中第N個散列桶由第(N mod 16)個鎖來保護。假設散列函數具有合理的分布性,并且關鍵字能夠均勻分布,那么這大約能把對于鎖的請求減少到原來的1/16,正是這項技術使得ConcurrentHashMap能夠支持多達16個并發的寫入器。(要使得擁有大量處理器的系統在高訪問量的情況下實現更高的并發性,還可以進一步增加鎖的數量,但僅當你能證明并發寫入線程的競爭足夠激烈并需要突破這個限制時,才能將鎖分段的數量超過默認的16個。)
另一個典型的案例就是LinkedBlockingQueue的實現。
take()和put()方法雖然都對隊列進行了修改操作,但由于是鏈表,因此,兩個操作分別作用于隊列的前端和末尾,理論上兩者并不沖突。使用獨占鎖,則要求在進行take和put操作時獲取當前隊列的獨占鎖,那么take和put就不可能真正的并發,他們會彼此等待對方釋放鎖。在JDK的實現中,取而代之的是兩把不同的鎖,分離了take和put操作.削弱了競爭的可能性.實現類取數據和寫數據的分離,實現了真正意義上成為并發操作。鎖分段的一個劣勢在于:與采用單個鎖來實現獨占訪問相比,要獲取多個鎖來實現獨占訪問將更加困難并且開銷更高。通常,在執行一個操作時最多只需獲取一個鎖,但在某些情況下需要加鎖整個容器,例如當ConcurrentHashMap需要擴展映射范圍,以及重新計算鍵值的散列值要分布到更大的桶集合中時,就需要獲取分段鎖集合中的所有鎖。
避免熱點域鎖分解和鎖分段技術都能提高可伸縮性,因為它們都能使不同的線程在不同的數據(或者同一個數據的不同部分)上操作,而不會相互干擾。如果程序采用鎖分段或分解技術,那么一定要表現出在鎖上的競爭頻率高于在鎖保護的數據上發生競爭的頻率。如果一個鎖保護兩個獨立變量X和Y,并且線程A想要訪問X,而線程B想要訪問Y(這類似于在ServerStatus中,一個線程調用addUser,而另一個線程調用addQuery),那么這兩個線程不會在任何數據上發生競爭,即使它們會在同一個鎖上發生競爭。
當每個操作都請求多個變量時,鎖的粒度將很難降低。這是在性能與可伸縮性之間相互制衡的另一個方面,一些常見的優化措施,例如將一些反復計算的結果緩存起來,都會引入一些”熱點域“,而這些熱點域往往會限制可伸縮性。
當實現HashMap時,你需要考慮如何在size方法中計算Map中的元素數量。最簡單的方法就是,在每次調用時都統計一次元素的數量。一種常見的優化措施是,在插入和移除元素時更新一個計數器,雖然這在put和remove等方法中略微增加了一些開銷,以確保計數器是最新的值,但這把size方法的開銷從O(n)降低到O(1)。
在單線程或者采用完全同步的實現中,使用一個獨立的計算器能很好地提高類似size和isEmpty這些方法的執行速度,但卻導致更難以提升實現的可伸縮性,因為每個修改map的操作都需要更新這個共享的計數器。即使使用鎖分段技術來實現散列鏈,那么在對計數器的訪問進行同步時,也會重新導致在使用獨占鎖時存在的可伸縮性問題。一個看似性能優化的措施——緩存size操作的結果,已經變成了一個可伸縮性問題。在這種情況下,計數器也被稱為熱點域,因為每個導致元素數量發生變化的操作都需要訪問它。
一些替代獨占鎖的方法
為了避免這個問題,ConcurrentHashMap中的size將對每個分段進行枚舉并將每個分段中的元素數量相加,而不是維護一個全局計數。為了避免枚舉每個元素,ConcurrentHashMap為每個分段都維護一個獨立的計數,并通過每個分段的鎖來維護這個值。第三種降低競爭鎖的影響的技術就是放棄使用獨占鎖,從而有助于使用一種友好并發的方式來管理共享狀態。例如,使用并發容器、讀-寫鎖、不可變對象以及原子變量。
ReadWriteLock實現了一種在多個讀取操作以及單個寫入操作情況下的加鎖規則:如果多個讀取操作都不會修改共享資源,那么這些讀取操作可以同時訪問該共享資源,但在執行寫入操作時必須以獨占方式來獲取鎖。對于讀取操作占多數的數據結構,ReadWriteLock能夠提供比獨占鎖更高的并發性。而對于只讀的數據結構,其中包含的不變性可以完全不需要加鎖操作。
原子變量提供了一種方式來降低更新“熱點域”時的開銷,例如競態計數器、序列發生器、或者對鏈表數據結構中頭節點的引用。原子變量類提供了在整數或者對象引用上的細粒度原子操作(因此可伸縮性更高),并使用了現代處理器中提供的底層并發原語(例如比較并交換)。如果在類中只包含少量的熱點域,并且這些域不會與其他變量參與到不變性條件中,那么用原子變量來替代他們能提高可伸縮性。(通過減少算法中的熱點域,可以提高可伸縮性——雖然原子變量能降低熱點域的更新開銷,但并不能完全消除。)
來自JVM的鎖優化 鎖粗化如果對一個鎖不停地進行請求,同步和釋放,其本身也會消耗系統寶貴的資源,反而不利于性能優化.
鎖偏向
虛擬機在遇到需要一連串對同一把鎖不斷進行請求和釋放操作的情況時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數,這就是鎖的粗化。偏向鎖是一種針對加鎖操作的優化手段,他的核心思想是:如果一個線程獲得了鎖,那么鎖就進行偏向模式.當這個線程再次請求鎖時,無需再做任何同步操作.這樣就節省了大量操作鎖的動作,從而提高程序性能.
因此,對于幾乎沒有鎖競爭的場合,偏向鎖有比較好的優化效果.因為極有可能連續多次是同一個線程請求相同的鎖.而對于鎖競爭激烈的程序,其效果不佳.
使用Java虛擬機參數:-XX:+UseBiasedLocking 可以開啟偏向鎖.
輕量級鎖如果偏向鎖失敗,虛擬機并不會立即掛起線程.它還會使用一種稱為輕量級的鎖的優化手段.輕量級鎖只是簡單的將對象頭部作為指針,指向持有鎖的線程堆棧內部,來判斷一個線程是否持有對象鎖.如果線程獲得輕量鎖成功,則可以順利進入臨界區.如果失敗,則表示其他線程爭搶到了鎖,那么當前線程的鎖請求就會膨脹為重量級鎖.
自旋鎖鎖膨脹后,虛擬機為了避免線程真實的在操作系統層面掛起,虛擬機還做了最后的努力就是自旋鎖.如果一個線程暫時無法獲得索,有可能在幾個CPU時鐘周期后就可以得到鎖,
鎖消除
那么簡單粗暴的掛起線程可能是得不償失的操作.虛擬機會假設在很短時間內線程是可以獲得鎖的,所以會讓線程自己空循環(這便是自旋的含義),如果嘗試若干次后,可以得到鎖,那么久可以順利進入臨界區,
如果還得不到,才會真實地講線程在操作系統層面掛起.鎖消除是一種更徹底的鎖優化,Java虛擬機在JIT編譯時,通過對運用上下文的掃描,去除不可能存在的共享資源競爭鎖,節省毫無意義的資源開銷.
我們可能會問:如果不可能存在競爭,為什么程序員還要加上鎖呢?
在Java軟件開發過程中,我們必然會用上一些JDK的內置API,比如StringBuffer、Vector等。你在使用這些類的時候,也許根本不會考慮這些對象到底內部是如何實現的。比如,你很有可能在一個不可能存在并發競爭的場合使用Vector。而周所眾知,Vector內部使用了synchronized請求鎖,如下代碼:
public String [] createString(){ Vectorv = new Vector (); for (int i =0;i<100;i++){ v.add(Integer.toString(i)); } return v.toArray(new String[]{}); } 上述代碼中的Vector,由于變量v只在createString()函數中使用,因此,它只是一個單純的局部變量。局部變量是在線程棧上分配的,屬于線程私有的數據,因此不可能被其他線程訪問。所以,在這種情況下,Vector內部所有加鎖同步都是沒有必要的。如果虛擬機檢測到這種情況,就會將這些無用的鎖操作去除。
鎖消除設計的一項關鍵技術是逃逸分析,就是觀察某個變量是否會跳出某個作用域(比如對Vector的一些操作).在本例中,變量v顯然沒有逃出createString()函數之外。以次為基礎,虛擬機才可以大膽將v內部逃逸出當前函數,也就是說v有可能被其他線程訪問。如果是這樣,虛擬機就不能消除v中的鎖操作。
逃逸分析必須在-server模式下進行,可以使用-XX:+DoEscapeAnalysis參數打開逃逸分析。使用-XX:+EliminateLocks參數可以打開鎖消除。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/66286.html
摘要:本文探討并發中的其它問題線程安全可見性活躍性等等。當閉鎖到達結束狀態時,門打開并允許所有線程通過。在從返回時被叫醒時,線程被放入鎖池,與其他線程競爭重新獲得鎖。 本文探討Java并發中的其它問題:線程安全、可見性、活躍性等等。 在行文之前,我想先推薦以下兩份資料,質量很高:極客學院-Java并發編程讀書筆記-《Java并發編程實戰》 線程安全 《Java并發編程實戰》中提到了太多的術語...
摘要:線程啟動規則對象的方法先行發生于此線程的每一個動作。所以局部變量是不被多個線程所共享的,也就不會出現并發問題。通過獲取到數據,放入當前線程處理完之后將當前線程中的信息移除。主線程必須在啟動其他線程后立即調用方法。 一、線程安全性 定義:當多個線程訪問某個類時,不管運行時環境采用何種調度方式,或者這些線程將如何交替執行,并且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行...
摘要:同時也會關注市場上同崗位薪資,以便對企業內部薪資結構做出相應調整。一般來說,相同崗位和職責的員工,薪資低于市場不超過,都屬于合理范疇,因為一個員工不會為了的薪酬而跳槽。同時,還能激勵員工自我提升,以獲得相應技能市場所給予的報酬。 各位職場人都聽說過薪資倒掛這詞兒吧,這個情況在行業內早就不是什...
摘要:是多線程包里的一個常見工具類,通過使用它可以借助線程能力極大提升處理響應速度,且實現方式非常優雅。主線程處于狀態,直到的值數減到,則主線程繼續執行。此時必須使用線程池,并限定最大可處理線程數量,否則服務器不穩定性會大福提升。 countdownlatch是java多線程包concurrent里的一個常見工具類,通過使用它可以借助線程能力極大提升處理響應速度,且實現方式非常優雅。今天我們...
摘要:創建線程的方式方式一將類聲明為的子類。將該線程標記為守護線程或用戶線程。其中方法隱含的線程為父線程。恢復線程,已過時。等待該線程銷毀終止。更多的使當前線程在鎖存器倒計數至零之前一直等待,除非線 知識體系圖: showImg(https://segmentfault.com/img/bVbef6v?w=1280&h=960); 1、線程是什么? 線程是進程中獨立運行的子任務。 2、創建線...
閱讀 845·2019-08-30 15:54
閱讀 3316·2019-08-29 15:33
閱讀 2701·2019-08-29 13:48
閱讀 1213·2019-08-26 18:26
閱讀 3333·2019-08-26 13:55
閱讀 1476·2019-08-26 10:45
閱讀 1164·2019-08-26 10:19
閱讀 305·2019-08-26 10:16