摘要:可重入意味著鎖被綁定到當(dāng)前線程,線程可以安全地多次獲取相同的鎖,而不會(huì)發(fā)生死鎖例如同步方法在同一對(duì)象上調(diào)用另一個(gè)同步方法。寫(xiě)入鎖釋放后,兩個(gè)任務(wù)并行執(zhí)行,它們不必等待對(duì)方是否完成,因?yàn)橹灰獩](méi)有線程持有寫(xiě)入鎖,它們就可以同時(shí)持有讀取鎖。
原文地址: Java 8 Concurrency Tutorial: Synchronization and Locks
為了簡(jiǎn)單起見(jiàn),本教程的示例代碼使用了在這里定義的兩個(gè)輔助方法,sleep(seconds) 和 stop(executor)
Synchronized當(dāng)我們編寫(xiě)多線程代碼訪問(wèn)可共享的變量時(shí)需要特別注意,下面是一個(gè)多線程去改變一個(gè)整數(shù)的例子。
定義一個(gè)變量 count,定義一個(gè)方法 increment() 使 count 增加 1.
int count = 0; void increment() { count = count + 1; }
當(dāng)多個(gè)線程同時(shí)調(diào)用 increment() 時(shí)就會(huì)出現(xiàn)問(wèn)題:
ExecutorService executor = Executors.newFixedThreadPool(2); IntStream.range(0, 10000) .forEach(i -> executor.submit(this::increment)); stop(executor); System.out.println(count); // 9965
上面的代碼執(zhí)行結(jié)果并不是10000,原因是我們?cè)诓煌木€程上共享一個(gè)變量,而沒(méi)有給這個(gè)變量的訪問(wèn)設(shè)置競(jìng)爭(zhēng)條件。
為了增加數(shù)字,必須執(zhí)行三個(gè)步驟:(i) 讀取當(dāng)前值;(ii) 將該值增加1;(iii) 將新值寫(xiě)入變量;如果兩個(gè)線程并行執(zhí)行這些步驟,則兩個(gè)線程可能同時(shí)執(zhí)行步驟1,從而讀取相同的當(dāng)前值。 這導(dǎo)致寫(xiě)入丟失,所以實(shí)際結(jié)果較低。 在上面的示例中,35個(gè)增量由于并發(fā)非同步訪問(wèn)計(jì)數(shù)而丟失,但是當(dāng)你自己執(zhí)行代碼時(shí)可能會(huì)看到不同的結(jié)果。
幸運(yùn)的是,Java 早期通過(guò) synchronized 關(guān)鍵字支持線程同步。增加計(jì)數(shù)時(shí),我們可以利用同步來(lái)解決上述競(jìng)爭(zhēng)條件:
synchronized void incrementSync() { count = count + 1; }
當(dāng)我們使用 incrementSync() 方法時(shí),我們得到了希望的結(jié)果,而且每次執(zhí)行的結(jié)果都是這樣的。
ExecutorService executor = Executors.newFixedThreadPool(2); IntStream.range(0, 10000) .forEach(i -> executor.submit(this::incrementSync)); stop(executor); System.out.println(count); // 10000
synchronized 關(guān)鍵值也可以用在一個(gè)語(yǔ)句塊中
void incrementSync() { synchronized (this) { count = count + 1; } }
在 JVM 的內(nèi)部使用了一個(gè)監(jiān)視器,也可以稱為監(jiān)視器鎖和內(nèi)部鎖來(lái)管理同步。這個(gè)監(jiān)視器被綁定到一個(gè)對(duì)象上,當(dāng)使用同步方法時(shí),每個(gè)方法共享相應(yīng)對(duì)象的監(jiān)視器。
所有隱式監(jiān)視器都實(shí)現(xiàn)了可重入特性。 可重入意味著鎖被綁定到當(dāng)前線程,線程可以安全地多次獲取相同的鎖,而不會(huì)發(fā)生死鎖(例如同步方法在同一對(duì)象上調(diào)用另一個(gè)同步方法)。
Locks除了使用關(guān)鍵字 synchronized 支持的隱式鎖(對(duì)象的內(nèi)置鎖)外,Concurrency API 支持由 Lock 接口指定的各種顯示鎖。顯示鎖能控制更細(xì)的粒度,因此也有更好的性能,在邏輯上也比較清晰。
標(biāo)準(zhǔn) JDK中提供了多種顯示鎖的實(shí)現(xiàn),將在下面的章節(jié)中進(jìn)行介紹。
ReentrantLockReentrantLock 類是一個(gè)互斥鎖,它和 synchronized 關(guān)鍵字訪問(wèn)的隱式鎖具有相同的功能,但它具有擴(kuò)展功能。它也實(shí)現(xiàn)了可重入的功能。
下面來(lái)看看如何使用 ReentrantLock
ReentrantLock lock = new ReentrantLock(); int count = 0; void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } }
鎖通過(guò) lock() 獲取,通過(guò) unlock() 釋放,將代碼封裝到 try/finally 塊中是非常重要的,以確保在出現(xiàn)異常的時(shí)候也能釋放鎖。這個(gè)方法和使用關(guān)鍵字 synchronized 修飾的方法是一樣是線程安全的。如果一個(gè)線程已經(jīng)獲得了鎖,后續(xù)線程調(diào)用 lock() 會(huì)暫停線程,直到鎖被釋放,永遠(yuǎn)只有一個(gè)線程能獲取鎖。
lock 支持更細(xì)粒度的去控制一個(gè)方法的同步,如下面的代碼:
ExecutorService executor = Executors.newFixedThreadPool(2); ReentrantLock lock = new ReentrantLock(); executor.submit(() -> { lock.lock(); try { sleep(1000); } finally { lock.unlock(); } }); executor.submit(() -> { System.out.println("Locked: " + lock.isLocked()); System.out.println("Held by me: " + lock.isHeldByCurrentThread()); boolean locked = lock.tryLock(); System.out.println("Lock acquired: " + locked); }); stop(executor);
當(dāng)?shù)谝粋€(gè)任務(wù)獲取鎖時(shí),第二個(gè)任務(wù)獲取鎖的狀態(tài)信息:
Locked: true Held by me: false Lock acquired: false
作為 lock() 方法的替代方法 tryLock() 嘗試去獲取鎖而不暫停當(dāng)前線程,必須使用 bool 結(jié)果去判斷是否真的獲取到了鎖。
ReadWriteLockReadWriteLock 指定了另一種類型的鎖,即讀寫(xiě)鎖。讀寫(xiě)鎖實(shí)現(xiàn)的邏輯是,當(dāng)沒(méi)有線程在寫(xiě)這個(gè)變量時(shí),其他的線程可以讀取這個(gè)變量,所以就是當(dāng)沒(méi)有線程持有寫(xiě)鎖時(shí),讀鎖就可以被所有的線程持有。如果讀取比寫(xiě)更頻繁,這將增加系統(tǒng)的性能和吞吐量。
ExecutorService executor = Executors.newFixedThreadPool(2); Mapmap = new HashMap<>(); ReadWriteLock lock = new ReentrantReadWriteLock(); executor.submit(() -> { lock.writeLock().lock(); try { sleep(1000); map.put("foo", "bar"); } finally { lock.writeLock().unlock(); } });
上面的例子首先獲取一個(gè)寫(xiě)入鎖,在 sleep 1秒后在 map 中寫(xiě)入值,在這個(gè)任務(wù)完成之前,還有兩個(gè)任務(wù)正在提交,試圖從 map 讀取值:
Runnable readTask = () -> { lock.readLock().lock(); try { System.out.println(map.get("foo")); sleep(1000); } finally { lock.readLock().unlock(); } }; executor.submit(readTask); executor.submit(readTask); stop(executor);
當(dāng)執(zhí)行上面的代碼時(shí),你會(huì)注意到兩人讀取的任務(wù)必須等待直到寫(xiě)入完成(當(dāng)在讀取的時(shí)候,寫(xiě)是不能獲取鎖的)。寫(xiě)入鎖釋放后,兩個(gè)任務(wù)并行執(zhí)行,它們不必等待對(duì)方是否完成,因?yàn)橹灰獩](méi)有線程持有寫(xiě)入鎖,它們就可以同時(shí)持有讀取鎖。
StampedLockJava 8 提供了一種新類型的鎖 StampedLock,像上面的例子一樣它也支持讀寫(xiě)鎖,與 ReadWriteLock 不同的是,StampedLock 的鎖定方法返回一個(gè) long 值,可以利用這個(gè)值檢查是否釋放鎖和鎖仍然有效。另外 StampedLock 支持另外一種稱為樂(lè)觀鎖的模式。
下面使用 StampedLock 來(lái)替換 ReadWriteLock
ExecutorService executor = Executors.newFixedThreadPool(2); Mapmap = new HashMap<>(); StampedLock lock = new StampedLock(); executor.submit(() -> { long stamp = lock.writeLock(); try { sleep(1000); map.put("foo", "bar"); } finally { lock.unlockWrite(stamp); } }); Runnable readTask = () -> { long stamp = lock.readLock(); try { System.out.println(map.get("foo")); sleep(1000); } finally { lock.unlockRead(stamp); } }; executor.submit(readTask); executor.submit(readTask); stop(executor);
通過(guò) readLock() 和 writeLock() 方法來(lái)獲取讀寫(xiě)鎖會(huì)返回一個(gè)稍后用于在 finally 塊中釋放鎖的值。注意,這里的鎖不是可重入的。每次鎖定都會(huì)返回一個(gè)新的值,并在沒(méi)有鎖的情況下阻塞,在使用的時(shí)候要注意不要死鎖。
就像前面 ReadWriteLock 中的示例一樣,兩個(gè)讀取任務(wù)必須等待寫(xiě)入任務(wù)釋放鎖。然后同時(shí)并行執(zhí)行打印結(jié)果到控制臺(tái)。
下面的例子演示了樂(lè)觀鎖
ExecutorService executor = Executors.newFixedThreadPool(2); StampedLock lock = new StampedLock(); executor.submit(() -> { long stamp = lock.tryOptimisticRead(); try { System.out.println("Optimistic Lock Valid: " + lock.validate(stamp)); sleep(1000); System.out.println("Optimistic Lock Valid: " + lock.validate(stamp)); sleep(2000); System.out.println("Optimistic Lock Valid: " + lock.validate(stamp)); } finally { lock.unlock(stamp); } }); executor.submit(() -> { long stamp = lock.writeLock(); try { System.out.println("Write Lock acquired"); sleep(2000); } finally { lock.unlock(stamp); System.out.println("Write done"); } }); stop(executor);
通過(guò)調(diào)用 tryOptimisticRead() 來(lái)獲取樂(lè)觀讀寫(xiě)鎖,tryOptimisticRead()總是返回一個(gè)值,而不會(huì)阻塞當(dāng)前線程,也不關(guān)鎖是否可用。如果有一個(gè)寫(xiě)鎖激活則返回0。可以通過(guò) lock.validate(stamp) 來(lái)檢查返回的標(biāo)記(long 值)是否有效。
執(zhí)行上面的代碼輸出:
Optimistic Lock Valid: true Write Lock acquired Optimistic Lock Valid: false Write done Optimistic Lock Valid: false
樂(lè)觀鎖在獲得鎖后立即生效。與普通讀鎖相反,樂(lè)觀鎖不會(huì)阻止其他線程立即獲得寫(xiě)鎖。在第一個(gè)線程休眠一秒之后,第二個(gè)線程獲得一個(gè)寫(xiě)鎖,而不用等待樂(lè)觀讀鎖解除。樂(lè)觀的讀鎖不再有效,即使寫(xiě)入鎖定被釋放,樂(lè)觀的讀取鎖仍然無(wú)效。
因此,在使用樂(lè)觀鎖時(shí),必須在每次訪問(wèn)任何共享的變量后驗(yàn)證鎖,以確保讀取仍然有效。
有時(shí)將讀鎖轉(zhuǎn)換為寫(xiě)鎖并不需要再次解鎖和鎖定是有用的。StampedLock 為此提供了tryConvertToWriteLock() 方法,如下面的示例所示:
ExecutorService executor = Executors.newFixedThreadPool(2); StampedLock lock = new StampedLock(); executor.submit(() -> { long stamp = lock.readLock(); try { if (count == 0) { stamp = lock.tryConvertToWriteLock(stamp); if (stamp == 0L) { System.out.println("Could not convert to write lock"); stamp = lock.writeLock(); } count = 23; } System.out.println(count); } finally { lock.unlock(stamp); } }); stop(executor);
該任務(wù)首先獲得一個(gè)讀鎖,并將當(dāng)前的變量計(jì)數(shù)值打印到控制臺(tái)。 但是,如果當(dāng)前值為 0,我們要分配一個(gè)新的值23。我們首先必須將讀鎖轉(zhuǎn)換為寫(xiě)鎖,以不打破其他線程的潛在并發(fā)訪問(wèn)。 調(diào)用 tryConvertToWriteLock() 不會(huì)阻塞,但可能會(huì)返回 0,指示當(dāng)前沒(méi)有寫(xiě)鎖定可用。 在這種情況下,我們調(diào)用writeLock()來(lái)阻塞當(dāng)前線程,直到寫(xiě)鎖可用。
Semaphores除了鎖之外,并發(fā)API還支持計(jì)數(shù)信號(hào)量。 鎖通常授予對(duì)變量或資源的獨(dú)占訪問(wèn)權(quán),而信號(hào)量則能夠維護(hù)整套許可證。 在不同的情況下,必須限制對(duì)應(yīng)用程序某些部分的并發(fā)訪問(wèn)量。
下面是一個(gè)如何限制對(duì)長(zhǎng)時(shí)間任務(wù)的訪問(wèn)的例子:
ExecutorService executor = Executors.newFixedThreadPool(10); Semaphore semaphore = new Semaphore(5); Runnable longRunningTask = () -> { boolean permit = false; try { permit = semaphore.tryAcquire(1, TimeUnit.SECONDS); if (permit) { System.out.println("Semaphore acquired"); sleep(5000); } else { System.out.println("Could not acquire semaphore"); } } catch (InterruptedException e) { throw new IllegalStateException(e); } finally { if (permit) { semaphore.release(); } } }; IntStream.range(0, 10) .forEach(i -> executor.submit(longRunningTask)); stop(executor);
執(zhí)行程序可以同時(shí)運(yùn)行10個(gè)任務(wù),但是我們使用5信號(hào)量,因此限制并發(fā)訪問(wèn)為5個(gè)。使用try/finally塊,即使在異常的情況下正確釋放信號(hào)量也是非常重要的。
運(yùn)行上面的代碼輸出:
Semaphore acquired Semaphore acquired Semaphore acquired Semaphore acquired Semaphore acquired Could not acquire semaphore Could not acquire semaphore Could not acquire semaphore Could not acquire semaphore Could not acquire semaphore
當(dāng)有 5 個(gè)任務(wù)獲取型號(hào)量后,隨后的任務(wù)便不能獲取信號(hào)量了。但是如果前面 5 的任務(wù)執(zhí)行完成,finally 塊釋放了型號(hào)量,隨后的線程就可以獲取星號(hào)量了,總數(shù)不會(huì)超過(guò)5個(gè)。這里調(diào)用 tryAcquire() 獲取型號(hào)量設(shè)置了超時(shí)時(shí)間1秒,意味著當(dāng)線程獲取信號(hào)量失敗后可以阻塞等待1秒再獲取。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70892.html
摘要:在接下來(lái)的分鐘,你將會(huì)學(xué)會(huì)如何通過(guò)同步關(guān)鍵字,鎖和信號(hào)量來(lái)同步訪問(wèn)共享可變變量。所以在使用樂(lè)觀鎖時(shí),你需要每次在訪問(wèn)任何共享可變變量之后都要檢查鎖,來(lái)確保讀鎖仍然有效。 原文:Java 8 Concurrency Tutorial: Synchronization and Locks譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 歡迎閱讀我的Java8并發(fā)教程的第二部分。這份指南將...
摘要:并發(fā)教程原子變量和原文譯者飛龍協(xié)議歡迎閱讀我的多線程編程系列教程的第三部分。如果你能夠在多線程中同時(shí)且安全地執(zhí)行某個(gè)操作,而不需要關(guān)鍵字或上一章中的鎖,那么這個(gè)操作就是原子的。當(dāng)多線程的更新比讀取更頻繁時(shí),這個(gè)類通常比原子數(shù)值類性能更好。 Java 8 并發(fā)教程:原子變量和 ConcurrentMap 原文:Java 8 Concurrency Tutorial: Synchroni...
摘要:在這個(gè)示例中我們使用了一個(gè)單線程線程池的。在延遲消逝后,任務(wù)將會(huì)并發(fā)執(zhí)行。這是并發(fā)系列教程的第一部分。第一部分線程和執(zhí)行器第二部分同步和鎖第三部分原子操作和 Java 8 并發(fā)教程:線程和執(zhí)行器 原文:Java 8 Concurrency Tutorial: Threads and Executors 譯者:BlankKelly 來(lái)源:Java8并發(fā)教程:Threads和Execut...
摘要:本文旨在對(duì)鎖相關(guān)源碼本文中的源碼來(lái)自使用場(chǎng)景進(jìn)行舉例,為讀者介紹主流鎖的知識(shí)點(diǎn),以及不同的鎖的適用場(chǎng)景。中,關(guān)鍵字和的實(shí)現(xiàn)類都是悲觀鎖。自適應(yīng)意味著自旋的時(shí)間次數(shù)不再固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。 前言 Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當(dāng)?shù)膱?chǎng)景下能夠展現(xiàn)出非常高的效率。本文旨在對(duì)鎖相關(guān)源碼(本文中的源碼來(lái)自JDK 8)、使用場(chǎng)景...
摘要:如何在線程池中提交線程內(nèi)存模型相關(guān)問(wèn)題什么是的內(nèi)存模型,中各個(gè)線程是怎么彼此看到對(duì)方的變量的請(qǐng)談?wù)動(dòng)惺裁刺攸c(diǎn),為什么它能保證變量對(duì)所有線程的可見(jiàn)性既然能夠保證線程間的變量可見(jiàn)性,是不是就意味著基于變量的運(yùn)算就是并發(fā)安全的請(qǐng)對(duì)比下對(duì)比的異同。 并發(fā)編程高級(jí)面試面試題 showImg(https://upload-images.jianshu.io/upload_images/133416...
閱讀 1216·2021-09-03 10:44
閱讀 617·2019-08-30 13:13
閱讀 2808·2019-08-30 13:11
閱讀 1976·2019-08-30 12:59
閱讀 1043·2019-08-29 15:32
閱讀 1607·2019-08-29 15:25
閱讀 1003·2019-08-29 12:24
閱讀 1290·2019-08-27 10:58