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