Java的Lock接口與讀寫鎖詳解
一、Lock接口與synchronized關(guān)鍵字
鎖是用來控制多個(gè)線程訪問共享資源的方式,一般來說,一個(gè)鎖能夠防止多個(gè)線程同時(shí)訪問共享資源(但是有些鎖可以允許多個(gè)線程并發(fā)的訪問共享資源,比如讀寫鎖)。
在Lock接口出現(xiàn)之前,Java程序是靠synchronized關(guān)鍵字實(shí)現(xiàn)鎖功能的,而Java SE 5之后,并發(fā)包中新增了Lock接口(以及相關(guān)實(shí)現(xiàn)類)用來實(shí)現(xiàn)鎖功能,它提供了與synchronized關(guān)鍵字類似的同步功能,只是在使用時(shí)需要顯式地獲取和釋放鎖。
雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時(shí)獲取鎖等多種synchronized關(guān)鍵字所不具備的同步特性。
使用synchronized關(guān)鍵字將會(huì)隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。當(dāng)然,這種方式簡化了同步的管理,可是擴(kuò)展性沒有顯式的鎖獲取和釋放來的好。
synchronized關(guān)鍵字在使用的過程中會(huì)有如下幾個(gè)問題:
1. 不可控性,無法做到隨心的加鎖和釋放鎖;
2. 效率比較低下,比如我們現(xiàn)在并發(fā)的讀兩個(gè)文件,讀與讀之間是互不影響的,但如果給這個(gè)讀的對(duì)象使用synchronized來實(shí)現(xiàn)同步的話,那么只要有一個(gè)線程進(jìn)入了,那么其他的線程都要等待;
3. 無法知道線程是否獲取到了鎖;
Lock是一個(gè)上層的接口,其原型如下,總共提供了6個(gè)方法:
public interface Lock { // 用來獲取鎖,如果鎖已經(jīng)被其他線程獲取,則一直等待,直到獲取到鎖 void lock(); // 該方法獲取鎖時(shí),可以響應(yīng)中斷,比如現(xiàn)在有兩個(gè)線程,一個(gè)已經(jīng)獲取到了鎖,另一個(gè)線程調(diào)用這個(gè)方法正在等待鎖 //但是此刻又不想讓這個(gè)線程一直在這死等,可以通過調(diào)用線程的Thread.interrupted()方法,來中斷線程的等待過程 void lockInterruptibly() throws InterruptedException; // tryLock方法會(huì)返回bool值,該方法會(huì)嘗試著獲取鎖,如果獲取到鎖,就返回true,如果沒有獲取到鎖,就返回false, //但是該方法會(huì)立刻返回,而不會(huì)一直等待 boolean tryLock(); // 這個(gè)方法和上面的tryLock差不多是一樣的,只是會(huì)嘗試指定的時(shí)間,如果在指定的時(shí)間內(nèi)拿到了鎖,則會(huì)返回true, //如果在指定的時(shí)間內(nèi)沒有拿到鎖,則會(huì)返回false boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 釋放鎖 void unlock(); // 實(shí)現(xiàn)線程通信,相當(dāng)于wait和notify,后面會(huì)單獨(dú)講解 Condition newCondition(); }
使用Lock是需要手動(dòng)釋放鎖的,但是如果程序中拋出了異常,那么就無法做到釋放鎖,有可能引起死鎖,所以我們在使用Lock的時(shí)候,有一種固定的格式,如下:
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally {// 必須使用try,最后在finally里面釋放鎖 l.unlock(); }
在finally塊中釋放鎖,目的是保證在獲取到鎖之后,最終能夠被釋放。 不要將獲取鎖的過程寫在try塊中,因?yàn)槿绻讷@取鎖(自定義鎖的實(shí)現(xiàn))時(shí)發(fā)生了異常,異常拋出的同時(shí),也會(huì)導(dǎo)致鎖無故釋放。
Lock接口提供的synchronized關(guān)鍵字所不具備的主要特性有:
- 嘗試非阻塞地獲取鎖,當(dāng)前線程獲取鎖時(shí),如果鎖沒有被其他線程獲取到,則成功獲取并持有鎖;
- 被中斷地獲取鎖,與syncronized不同,獲取到鎖的線程能響應(yīng)中斷,當(dāng)獲取到鎖的線程被中斷時(shí),會(huì)拋出中斷異常,并釋放鎖;
- 超時(shí)獲取鎖,在指定的截止時(shí)間前獲取鎖,如果時(shí)間到了仍未獲取到鎖,則返回;
以多線程讀取文件來示例:
public class LockDemo { // new一個(gè)鎖對(duì)象,注意此處必須聲明成類對(duì)象,保持只有一把鎖,ReentrantLock是Lock的唯一實(shí)現(xiàn)類 Lock lock = new ReentrantLock(); public void readFile(String fileMessage){ lock.lock();// 上鎖 try { System.out.println(Thread.currentThread().getName()+"得到了鎖,正在讀取文件……"); for (int i = 0; i< fileMessage.length(); i++){ System.out.print(fileMessage.charAt(i)); } System.out.println(); System.out.println("文件讀取完畢!"); } finally { System.out.println(Thread.currentThread().getName()+"釋放了鎖!"); lock.unlock(); } } public static void main(String[] args) { LockDemo demo = new LockDemo(); String fileName = "H:/Java_Workspace_Console/test.txt"; // 創(chuàng)建若干個(gè)線程 new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); } }
如果先把鎖的那兩行代碼注釋掉,看下效果如何:
多個(gè)線程讀取到的內(nèi)容錯(cuò)亂。 然后我們把鎖的代碼加上,看下效果如何:
如果我們把上面的readFile方法前面加上synchronized關(guān)鍵字,然后把鎖去掉,效果是一樣的。 tryLock方法的使用和Lock方法的使用類似,不做過多的說明了,代碼如下:
public class TryLockDemo { // new一個(gè)鎖對(duì)象,注意此處必須聲明成類對(duì)象,保持只有一把鎖,ReentrantLock是Lock的唯一實(shí)現(xiàn)類 Lock lock = new ReentrantLock(); public void readFile(String fileMessage){ // 上鎖 if (lock.tryLock()) { try { System.out.println(Thread.currentThread().getName()+"得到了鎖,正在讀取文件……"); for (int i = 0; i< fileMessage.length(); i++){ System.out.print(fileMessage.charAt(i)); } System.out.println(); System.out.println("文件讀取完畢!"); } finally { System.out.println(Thread.currentThread().getName()+"釋放了鎖!"); lock.unlock(); } } } public static void main(String[] args) { TryLockDemo demo = new TryLockDemo(); String fileName = "H:/Java_Workspace_Console/test.txt"; // 創(chuàng)建若干個(gè)線程 new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); } }
二、讀寫鎖
ReentrantLock是排他鎖,這些鎖在同一時(shí)刻只允許一個(gè)線程進(jìn)行訪問,而讀寫鎖在同一時(shí)刻可以允許多個(gè)讀線程訪問,但是在寫線程訪問時(shí),所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護(hù)了一對(duì)鎖,一個(gè)讀鎖和一個(gè)寫鎖,通過分離讀鎖和寫鎖,使得并發(fā)性相比一般的排他鎖有了很大提升。
除了保證寫操作對(duì)讀操作的可見性以及并發(fā)性的提升之外,讀寫鎖能夠簡化讀寫交互場景的編程方式。假設(shè)在程序中定義一個(gè)共享的用作緩存數(shù)據(jù)結(jié)構(gòu),它大部分時(shí)間提供讀服務(wù)(例如查詢和搜索),而寫操作占有的時(shí)間很少,但是寫操作完成之后的更新需要對(duì)后續(xù)的讀服務(wù)可見。
一般情況下,讀寫鎖的性能都會(huì)比排它鎖好,因?yàn)榇蠖鄶?shù)場景讀是多于寫的。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量。Java并發(fā)包提供讀寫鎖的接口是ReadWriteLock:
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
該接口也有一個(gè)實(shí)現(xiàn)類ReentrantReadWriteLock,下面我們先看一下,多線程同時(shí)讀取文件時(shí),用synchronized實(shí)現(xiàn)的效果,代碼如下:
public class SyncReadDemo { public synchronized void get(Thread thread) { System.out.println("start time:"+System.currentTimeMillis()); for(int i=0; i<5; i++){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName() + ":正在進(jìn)行讀操作……"); } System.out.println(thread.getName() + ":讀操作完畢!"); System.out.println("end time:"+System.currentTimeMillis()); } public static void main(String[] args) { final SyncReadDemo lock = new SyncReadDemo(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); } }
測試結(jié)果如下:
整個(gè)過程耗時(shí)200ms
在加了synchronized關(guān)鍵字之后,讀與讀之間,也是互斥的,也就是說,必須等待Thread-0讀完之后,才會(huì)輪到Thread-1線程讀,而無法做到同時(shí)讀文件,這種情況在大量線程同時(shí)都需要讀文件的時(shí)候,效率低下。 下面我們來測試ReadAndWriteLock的性能,代碼如下:
public class ReadAndWriteLockDemo { ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void get(Thread thread) { lock.readLock().lock(); try{ System.out.println("start time:"+System.currentTimeMillis()); for(int i=0; i<5; i++){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName() + ":正在進(jìn)行讀操作……"); } System.out.println(thread.getName() + ":讀操作完畢!"); System.out.println("end time:"+System.currentTimeMillis()); }finally{ lock.readLock().unlock(); } } public static void main(String[] args) { final ReadAndWriteLockDemo lock = new ReadAndWriteLockDemo(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); } }
整個(gè)過程耗時(shí):100ms Thread-0和Thread-1是在同時(shí)讀取文件。不過要注意的是,如果有一個(gè)線程已經(jīng)占用了讀鎖,則此時(shí)其他線程如果要申請寫鎖,則申請寫鎖的線程會(huì)一直等待釋放讀鎖。如果有一個(gè)線程已經(jīng)占用了寫鎖,則此時(shí)其他線程如果申請寫鎖或者讀鎖,則申請的線程會(huì)一直等待釋放寫鎖。讀鎖和寫鎖是互斥的。
可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和 ReentrantLock都是可重入鎖,可重入性在我看來實(shí)際上表明了鎖的分配機(jī)制:基于線程的分配,而不是基于方法調(diào)用的分配。
舉個(gè)簡單的例子,當(dāng)一 個(gè)線程執(zhí)行到某個(gè)synchronized方法時(shí),比如說method1,而在method1中會(huì)調(diào)用另外一個(gè)synchronized方法 method2,此時(shí)線程不必重新去申請鎖,而是可以直接執(zhí)行方法method2。
可中斷鎖
可中斷鎖:顧名思義,就是可以相應(yīng)中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時(shí)間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同時(shí)有多個(gè)線程在等待一個(gè)鎖,當(dāng)這個(gè)鎖被釋放時(shí),等待時(shí)間最久的線程(最先請求的線程)會(huì)獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進(jìn)行的。這樣就可能導(dǎo)致某個(gè)或者一些線程永遠(yuǎn)獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。而對(duì)于ReentrantLock和ReentrantReadWriteLock,它默認(rèn)情況下是非公平鎖,但是可以在構(gòu)造函數(shù)中指定其為公平鎖。
公平性與否是針對(duì)獲取鎖而言的,如果一個(gè)鎖是公平的,那么鎖的獲取順序就應(yīng)該符合請求的絕對(duì)時(shí)間順序,也就是FIFO。非公平性鎖可能使線程“饑餓”,為什么它又被設(shè)定成默認(rèn)的實(shí)現(xiàn)呢?
如果把每次不同線程獲取到鎖定義為1次切換,公平性鎖為了保證鎖的獲取按照FIFO原則,代價(jià)是進(jìn)行大量的線程切換。非公平性鎖雖然可能造成線程“饑餓”,但會(huì)有更少的線程切換,保證了其更大的吞吐量。
到此這篇關(guān)于Java的Lock接口與讀寫鎖詳解的文章就介紹到這了,更多相關(guān)Lock接口與讀寫鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java多線程中ReentrantLock與Condition詳解
這篇文章主要介紹了Java多線程中ReentrantLock與Condition詳解,需要的朋友可以參考下2017-11-11關(guān)于springboot 配置文件中屬性變量引用方式@@解析
這篇文章主要介紹了關(guān)于springboot 配置文件中屬性變量引用方式@@解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-04-04JPA如何設(shè)置表名和實(shí)體名,表字段與實(shí)體字段的對(duì)應(yīng)
這篇文章主要介紹了JPA如何設(shè)置表名和實(shí)體名,表字段與實(shí)體字段的對(duì)應(yīng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11利用ClasserLoader實(shí)現(xiàn)jar包加載并調(diào)用里面的方法
classloader即是類加載,虛擬機(jī)把描述類的數(shù)據(jù)從class字節(jié)碼文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行檢驗(yàn)、轉(zhuǎn)換解析和初始化,了解java的類加載機(jī)制,可以快速解決運(yùn)行時(shí)的各種加載問題并快速定位其背后的本質(zhì)原因,本文介紹了如何利用ClasserLoader來實(shí)現(xiàn)jar包加載并調(diào)用里面的方法2024-09-09詳解java8在Collection中新增加的方法removeIf
這篇文章主要介紹了詳解java8在Collection中新增加的方法removeIf的相關(guān)資料,需要的朋友可以參考下2018-01-01Spring中@ConfigurationProperties的用法解析
這篇文章主要介紹了Spring中@ConfigurationProperties的用法解析,傳統(tǒng)的Spring一般都是基本xml配置的,后來spring3.0新增了許多java config的注解,特別是spring boot,基本都是清一色的java config,需要的朋友可以參考下2023-11-11使用Eclipse創(chuàng)建Maven的Java WEB項(xiàng)目的兩種方式
本文詳細(xì)介紹了如何在JDK 1.8、Maven 3.6.3和Eclipse 2017版本下創(chuàng)建Java Web項(xiàng)目,包括選擇archetype方式、配置Tomcat、設(shè)置為Web3.1、配置Maven編譯級(jí)別、修復(fù)Eclipse提示的錯(cuò)誤、設(shè)置Maven源文件夾等步驟,需要的朋友可以參考下2024-11-11Android開發(fā)簡單計(jì)算器實(shí)現(xiàn)代碼
這篇文章主要介紹了Android開發(fā)簡單計(jì)算器實(shí)現(xiàn),本文放置了完整的Android開發(fā)電腦,通過部署項(xiàng)目可以直接按到效果,希望本篇文章可以對(duì)你有所幫助2021-06-06Java 實(shí)戰(zhàn)項(xiàng)目之精美物流管理系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SpringBoot+Vue+maven+Mysql實(shí)現(xiàn)一個(gè)精美的物流管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11