Java中鎖的分類與使用方法
Lock和synchronized
- 鎖是一種工具,用于控制對(duì)共享資源的訪問(wèn)
- Lock和synchronized,這兩個(gè)是最創(chuàng)建的鎖,他們都可以達(dá)到線程安全的目的,但是使用和功能上有較大不同
- Lock不是完全替代synchronized的,而是當(dāng)使用synchronized不合適或不足以滿足要求的時(shí)候,提供高級(jí)功能
- Lock 最常見(jiàn)的是ReentrantLock實(shí)現(xiàn)
為啥需要Lock
- syn效率低:鎖的釋放情況少,試圖獲得鎖時(shí)不能設(shè)定超時(shí),不能中斷一個(gè)正在試圖獲得鎖的線程
- 不夠靈活,加鎖和釋放的時(shí)機(jī)單一,每個(gè)鎖僅有一個(gè)單一的條件(某個(gè)對(duì)象),可能是不夠的
- 無(wú)法知道是否成功獲取到鎖
主要方法
Lock();
最普通的獲取鎖,最佳實(shí)踐是finally中釋放鎖,保證發(fā)生異常的時(shí)候鎖一定被釋放
/** * 描述:Lock不會(huì)像syn一樣,異常的時(shí)候自動(dòng)釋放鎖 * 所以最佳實(shí)踐是finally中釋放鎖,保證發(fā)生異常的時(shí)候鎖一定被釋放 */ private static Lock lock = new ReentrantLock(); public static void main(String[] args) { lock.lock(); try { //獲取本鎖保護(hù)的資源 System.out.println(Thread.currentThread().getName() + "開(kāi)始執(zhí)行任務(wù)"); } finally { lock.unlock(); } }
tryLock(long time,TimeUnit unit);超時(shí)就放棄
用來(lái)獲取鎖,如果當(dāng)前鎖沒(méi)有被其它線程占用,則獲取成功,則返回true,否則返回false,代表獲取鎖失敗
/** * 描述:用TryLock避免死鎖 */ static class TryLockDeadlock implements Runnable { int flag = 1; static Lock lock1 = new ReentrantLock(); static Lock lock2 = new ReentrantLock(); @Override public void run() { for (int i = 0; i < 100; i++) { if (flag == 1) { try { if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) { try { System.out.println("線程1獲取到了鎖1"); Thread.sleep(new Random().nextInt(1000)); if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){ try { System.out.println("線程1獲取到了鎖2"); System.out.println("線程1成功獲取到了2把鎖"); break; }finally { lock2.unlock(); } }else{ System.out.println("線程1獲取鎖2失敗,已重試"); } } finally { lock1.unlock(); Thread.sleep(new Random().nextInt(1000)); } } else { System.out.println("線程1獲取鎖1失敗,已重試"); } } catch (InterruptedException e) { e.printStackTrace(); } } if (flag == 0) { try { if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) { try { System.out.println("線程2獲取到了鎖2"); Thread.sleep(new Random().nextInt(1000)); if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){ try { System.out.println("線程2獲取到了鎖1"); System.out.println("線程2成功獲取到了2把鎖"); break; }finally { lock1.unlock(); } }else{ System.out.println("線程2獲取鎖1失敗,已重試"); } } finally { lock2.unlock(); Thread.sleep(new Random().nextInt(1000)); } } else { System.out.println("線程2獲取鎖2失敗,已經(jīng)重試"); } } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { TryLockDeadlock r1 = new TryLockDeadlock(); TryLockDeadlock r2 = new TryLockDeadlock(); r1.flag = 1; r2.flag = 0; new Thread(r1).start(); new Thread(r2).start(); } } 執(zhí)行結(jié)果: 線程1獲取到了鎖1 線程2獲取到了鎖2 線程1獲取鎖2失敗,已重試 線程2獲取到了鎖1 線程2成功獲取到了2把鎖 線程1獲取到了鎖1 線程1獲取到了鎖2 線程1成功獲取到了2把鎖
lockInterruptibly(); 中斷
相當(dāng)于tryLock(long time,TimeUnit unit) 把超時(shí)時(shí)間設(shè)置為無(wú)限,在等待鎖的過(guò)程中,線程可以被中斷
/** * 描述:獲取鎖的過(guò)程中,中斷了 */ static class LockInterruptibly implements Runnable { private Lock lock = new ReentrantLock(); @Override public void run() { System.out.println(Thread.currentThread().getName() + "嘗試獲取鎖"); try { lock.lockInterruptibly(); try { System.out.println(Thread.currentThread().getName() + "獲取到了鎖"); Thread.sleep(5000); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "睡眠中被中斷了"); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + "釋放了鎖"); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "等鎖期間被中斷了"); } } public static void main(String[] args) { LockInterruptibly lockInterruptibly = new LockInterruptibly(); Thread thread0 = new Thread(lockInterruptibly); Thread thread1 = new Thread(lockInterruptibly); thread0.start(); thread1.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } thread0.interrupt(); } } 執(zhí)行結(jié)果: Thread-0嘗試獲取鎖 Thread-1嘗試獲取鎖 Thread-0獲取到了鎖 Thread-0睡眠中被中斷了 Thread-0釋放了鎖 Thread-1獲取到了鎖 Thread-1釋放了鎖
Java鎖分類:
樂(lè)觀鎖和悲觀鎖:
樂(lè)觀鎖:
比較樂(lè)觀,認(rèn)為自己在處理操作的時(shí)候,不會(huì)有其它線程來(lái)干擾,所以并不會(huì)鎖住操作對(duì)象
- 在更新的時(shí)候,去對(duì)比我修改期間的數(shù)據(jù)有沒(méi)有被改變過(guò),如沒(méi)有,就正常的修改數(shù)據(jù)
- 如果數(shù)據(jù)和我一開(kāi)始拿到的不一樣了,說(shuō)明其他人在這段時(shí)間內(nèi)改過(guò),會(huì)選擇放棄,報(bào)錯(cuò),重試等策略
- 樂(lè)觀鎖的實(shí)現(xiàn)一般都是利用CAS算法來(lái)實(shí)現(xiàn)的
劣勢(shì):
可能造成ABA問(wèn)題,就是不知道是不是修改過(guò)
使用場(chǎng)景:
適合并發(fā)寫入少的情況,大部分是讀取的場(chǎng)景,不加鎖的能讓讀取的性能大幅提高
悲觀鎖:
比較悲觀,認(rèn)為如果我不鎖住這個(gè)資源,別人就會(huì)來(lái)爭(zhēng)搶,就會(huì)造成數(shù)據(jù)結(jié)果錯(cuò)誤,所以它會(huì)鎖住操作對(duì)象,Java中悲觀鎖的實(shí)現(xiàn)就是syn和Lock相關(guān)類
劣勢(shì):
- 阻塞和喚醒帶來(lái)的性能劣勢(shì)
- 如果持有鎖的線程被永久阻塞,比如遇到了無(wú)限循環(huán),死鎖等活躍性問(wèn)題,那么等待該線程釋放鎖的那幾個(gè)線程,永遠(yuǎn)也得不到執(zhí)行
- 優(yōu)先級(jí)反轉(zhuǎn),優(yōu)先級(jí)低的線程拿到鎖不釋放或釋放的比較慢,就會(huì)造成這個(gè)問(wèn)題
使用場(chǎng)景:
適合并發(fā)寫入多的情況,適用于臨界區(qū)持鎖時(shí)間比較長(zhǎng)的情況:
- 臨界區(qū)有IO操作
- 臨界區(qū)代碼復(fù)雜或者循環(huán)量大
- 臨界區(qū)競(jìng)爭(zhēng)非常激烈
可重入鎖:
可重入就是說(shuō)某個(gè)線程已經(jīng)獲得某個(gè)鎖,可以再次獲取鎖而不會(huì)出現(xiàn)死鎖
ReentrantLock 和 synchronized 都是可重入鎖
// 遞歸調(diào)用演示可重入鎖 static class RecursionDemo{ public static ReentrantLock lock = new ReentrantLock(); private static void accessResource(){ lock.lock(); try { System.out.println("已經(jīng)對(duì)資源處理了"); if (lock.getHoldCount() < 5){ System.out.println("已經(jīng)處理了"+lock.getHoldCount()+"次"); accessResource(); } }finally { lock.unlock(); } } public static void main(String[] args) { new RecursionDemo().accessResource(); } } 執(zhí)行結(jié)果: 已經(jīng)對(duì)資源處理了 已經(jīng)處理了1次 已經(jīng)對(duì)資源處理了 已經(jīng)處理了2次 已經(jīng)對(duì)資源處理了 已經(jīng)處理了3次 已經(jīng)對(duì)資源處理了 已經(jīng)處理了4次 已經(jīng)對(duì)資源處理了
ReentrantLock的其它方法
- isHeldByCurrentThread 可以看出鎖是否被當(dāng)前線程持有
- getQueueLength()可以返回當(dāng)前正在等待這把鎖的隊(duì)列有多長(zhǎng),一般這兩個(gè)方法是開(kāi)發(fā)和調(diào)試時(shí)候使用,上線后用到的不多
公平鎖和非公平鎖
- 公平指的是按照線程請(qǐng)求的順序,來(lái)分配鎖;
- 非公平指的是,不完全按照請(qǐng)求的順序,在一定情況下,可以插隊(duì)
- 非公平鎖可以避免喚醒帶來(lái)的空檔期
/** * 描述:演示公平鎖和非公平鎖 */ class FairLock{ public static void main(String[] args) { PrintQueue printQueue = new PrintQueue(); Thread[] thread = new Thread[10]; for (int i = 0; i < 10; i++) { thread[i] = new Thread(new Job(printQueue)); } for (int i = 0; i < 5; i++) { thread[i].start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Job implements Runnable{ PrintQueue printQueue; public Job(PrintQueue printQueue) { this.printQueue = printQueue; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"開(kāi)始打印"); printQueue.printJob(new Object()); System.out.println(Thread.currentThread().getName()+"打印完成"); } } class PrintQueue{ // true 公平,false是非公平 private Lock queueLock = new ReentrantLock(true); public void printJob(Object document){ queueLock.lock(); try { int duration = new Random().nextInt(10)+1; System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒"); Thread.sleep(duration * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { queueLock.unlock(); } queueLock.lock(); try { int duration = new Random().nextInt(10)+1; System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒"); Thread.sleep(duration * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { queueLock.unlock(); } } } 執(zhí)行結(jié)果: Thread-0開(kāi)始打印 Thread-0正在打印,需要10秒 Thread-1開(kāi)始打印 Thread-2開(kāi)始打印 Thread-3開(kāi)始打印 Thread-4開(kāi)始打印 Thread-1正在打印,需要2秒 Thread-2正在打印,需要2秒 Thread-3正在打印,需要2秒 Thread-4正在打印,需要4秒 Thread-0正在打印,需要2秒 Thread-0打印完成 Thread-1正在打印,需要7秒 Thread-1打印完成 Thread-2正在打印,需要8秒 Thread-2打印完成 Thread-3正在打印,需要3秒 Thread-3打印完成 Thread-4正在打印,需要8秒 Thread-4打印完成 true改為false演示非公平鎖: Lock queueLock = new ReentrantLock(false); 執(zhí)行結(jié)果: Thread-0正在打印,需要7秒 Thread-1開(kāi)始打印 Thread-2開(kāi)始打印 Thread-3開(kāi)始打印 Thread-4開(kāi)始打印 Thread-0正在打印,需要9秒 Thread-0打印完成 Thread-1正在打印,需要3秒 Thread-1正在打印,需要2秒 Thread-1打印完成 Thread-2正在打印,需要4秒 Thread-2正在打印,需要7秒 Thread-2打印完成 Thread-3正在打印,需要10秒 Thread-3正在打印,需要2秒 Thread-3打印完成 Thread-4正在打印,需要7秒 Thread-4正在打印,需要8秒 Thread-4打印完成
共享鎖和排它鎖:
- 排它鎖,又稱為獨(dú)占鎖,獨(dú)享鎖
- 共享鎖,又稱為讀鎖,獲得共享鎖之后,可以查看但無(wú)法修改和刪除數(shù)據(jù),其他線程此時(shí)也可以獲取到共享鎖,也可以查看但無(wú)法修改和刪除數(shù)據(jù)
- 共享鎖和排它鎖的典型是讀寫鎖 ReentrantReadWriteLock,其中讀鎖是共享鎖,寫鎖是獨(dú)享鎖
讀寫鎖的作用:
- 在沒(méi)有讀寫鎖之前,我們假設(shè)使用ReentrantLock,那么雖然我們保證了線程安全,但是也浪費(fèi)了一定的資源:多個(gè)讀操作同時(shí)進(jìn)行,并沒(méi)有線程安全問(wèn)題
- 在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒(méi)有寫鎖的情況下,讀是無(wú)阻塞的,提高了程序的執(zhí)行效率
讀寫鎖的規(guī)則:
- 多個(gè)線程值申請(qǐng)讀鎖,都可以申請(qǐng)到
- 要么一個(gè)或多個(gè)一起讀,要么一個(gè)寫,兩者不會(huì)同時(shí)申請(qǐng)到,只能存在一個(gè)寫鎖
/** * 描述:演示可以多個(gè)一起讀,只能一個(gè)寫 */ class CinemaReadWrite{ private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); private static void read(){ readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "釋放了讀鎖"); readLock.unlock(); } } private static void write(){ writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "釋放了寫鎖"); writeLock.unlock(); } } public static void main(String[] args) { new Thread(()-> read(),"Thrad1").start(); new Thread(()-> read(),"Thrad2").start(); new Thread(()-> write(),"Thrad3").start(); new Thread(()-> write(),"Thrad4").start(); } } 執(zhí)行結(jié)果: Thrad1得到了讀鎖,正在讀取 Thrad2得到了讀鎖,正在讀取 Thrad2釋放了讀鎖 Thrad1釋放了讀鎖 Thrad3得到了寫鎖,正在寫入 Thrad3釋放了寫鎖 Thrad4得到了寫鎖,正在寫入 Thrad4釋放了寫鎖
讀鎖和寫鎖的交互方式:
讀鎖插隊(duì)策略:
- 公平鎖:不允許插隊(duì)
- 非公平鎖:寫鎖可以隨時(shí)插隊(duì),讀鎖僅在等待隊(duì)列頭節(jié)點(diǎn)不是想獲取寫鎖線程的時(shí)候可以插隊(duì)
自旋鎖和阻塞鎖
- 讓當(dāng)前線程進(jìn)行自旋,如果自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開(kāi)銷。這就是自旋鎖。
- 阻塞鎖和自旋鎖相反,阻塞鎖如果遇到?jīng)]拿到鎖的情況,會(huì)直接把線程阻塞,知道被喚醒
自旋缺點(diǎn):
- 如果鎖被占用的時(shí)間很長(zhǎng),那么自旋的線程只會(huì)白浪費(fèi)處理器資源
- 在自旋的過(guò)程中,一直消耗cpu,所以雖然自旋鎖的起始開(kāi)銷低于悲觀鎖,但是隨著自旋的時(shí)間增長(zhǎng),開(kāi)銷也是線性增長(zhǎng)的
原理:
- 在Java1.5版本及以上的并發(fā)框架java.util.concurrent 的atmoic包下的類基本都是自旋鎖的實(shí)現(xiàn)
- AtomicInteger的實(shí)現(xiàn):自旋鎖的實(shí)現(xiàn)原理是CAS,AtomicInteger中調(diào)用unsafe 進(jìn)行自增操作的源碼中的do-while循環(huán)就是一個(gè)自旋操作,如果修改過(guò)程中遇到其他線程競(jìng)爭(zhēng)導(dǎo)致沒(méi)修改成功,就在while里死循環(huán)直至修改成功
/** * 描述:自旋鎖演示 */ class SpinLock{ private AtomicReference<Thread> sign = new AtomicReference<>(); public void lock(){ Thread currentThread = Thread.currentThread(); while (!sign.compareAndSet(null,currentThread)){ System.out.println("自旋獲取失敗,再次嘗試"); } } public void unLock(){ Thread currentThread = Thread.currentThread(); sign.compareAndSet(currentThread,null); } public static void main(String[] args) { SpinLock spinLock = new SpinLock(); Runnable runnable = new Runnable(){ @Override public void run(){ System.out.println(Thread.currentThread().getName()+"開(kāi)始嘗試自旋鎖"); spinLock.lock(); System.out.println(Thread.currentThread().getName()+"獲取到了自旋鎖"); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }finally { spinLock.unLock(); System.out.println(Thread.currentThread().getName()+"釋放了自旋鎖"); } } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } } 執(zhí)行結(jié)果: Thread-0開(kāi)始嘗試自旋鎖 Thread-0獲取到了自旋鎖 Thread-1開(kāi)始嘗試自旋鎖 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 自旋獲取失敗,再次嘗試 Thread-0釋放了自旋鎖 Thread-1獲取到了自旋鎖 Thread-1釋放了自旋鎖
使用場(chǎng)景:
- 自旋鎖一般用于多核服務(wù)器,在并發(fā)度不是特別高的情況下,比阻塞鎖的效率要高
- 另外,自旋鎖適用于臨界區(qū)比較短小的情況,否則如果臨界區(qū)很大(線程一旦拿到鎖,很久之后才會(huì)釋放),那也是不合適的
總結(jié)
到此這篇關(guān)于Java中鎖的分類與使用方法的文章就介紹到這了,更多相關(guān)Java中鎖使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Spring Security中AuthenticationEntryPoint不生效相關(guān)問(wèn)題
這篇文章主要介紹了解決Spring Security中AuthenticationEntryPoint不生效相關(guān)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12關(guān)于idea更新到2020.2.3無(wú)法創(chuàng)建web項(xiàng)目原因 library is not specified
這篇文章主要介紹了關(guān)于idea更新到2020.2.3無(wú)法創(chuàng)建web項(xiàng)目原因 library is not specified,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10Spring Data環(huán)境搭建實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了Spring Data環(huán)境搭建實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08為什么說(shuō)要慎用SpringBoot @ComponentScan
本文主要介紹了為什么說(shuō)要慎用SpringBoot @ComponentScan,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07Java 添加、修改、讀取、復(fù)制、刪除Excel批注的實(shí)現(xiàn)
這篇文章主要介紹了Java 添加、修改、讀取、復(fù)制、刪除Excel批注的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02