詳解java中各類鎖的機(jī)制
前言
總結(jié)java常見的鎖
區(qū)分各個鎖機(jī)制以及如何使用
| 使用方法 | 鎖名 |
|---|---|
| 考察線程是否要鎖住同步資源 | 樂觀鎖和悲觀鎖 |
| 鎖住同步資源后,要不要阻塞 | 不阻塞可以使用自旋鎖 |
| 一個線程多個流程獲取同一把鎖 | 可重入鎖 |
| 多個線程公用一把鎖 | 讀寫鎖(寫的共享鎖) |
| 多個線程競爭要不要排隊 | 公平鎖與非公平鎖 |
1. 樂觀鎖與悲觀鎖
悲觀鎖:不能同時進(jìn)行多人,執(zhí)行的時候先上鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖
樂觀鎖:通過版本號一致與否,即給數(shù)據(jù)加上版本,同步更新數(shù)據(jù)以及加上版本號。不會上鎖,判斷版本號,可以多人操作,類似生活中的搶票。每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機(jī)制實現(xiàn)事務(wù)的
(樂觀鎖可以使用版本號機(jī)制和CAS算法實現(xiàn))

通過具體案例演示悲觀鎖和樂觀鎖
在redis框架中
執(zhí)行multi之前,執(zhí)行命令watch
具體格式如下
watch key1 [key2]
具體代碼格式如下
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set add 100 OK 127.0.0.1:6379> watch add OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> incrby add 20 QUEUED 127.0.0.1:6379(TX)> exec 1) (integer) 120 127.0.0.1:6379>
flushdb是清空數(shù)據(jù)庫

但如果在另一個服務(wù)器上,輸入exec,會顯示出錯
因為用的是樂觀鎖,被修改了之后版本會發(fā)生改變
總的來說:
悲觀鎖:單獨每個人完成事情的時候,執(zhí)行上鎖解鎖。解決并發(fā)中的問題,不支持并發(fā)操作,只能一個一個操作,效率低
樂觀鎖:每執(zhí)行一件事情,都會比較數(shù)據(jù)版本號,誰先提交,誰先提交版本號
2. 公平鎖與非公平鎖
公平鎖:先來先到
非公平鎖:不是按照順序,可插隊
- 公平鎖:效率相對低
- 非公平鎖:效率高,但是線程容易餓死
通過這個函數(shù)Lock lock = new ReentrantLock(true);。創(chuàng)建一個可重入鎖,true 表示公平鎖,false 表示非公平鎖。默認(rèn)非公平鎖
通過查看源碼
帶有參數(shù)的ReentrantLock(true)為公平鎖
ReentrantLock(false)為非公平鎖
主要是調(diào)用NonfairSync()與FairSync()
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
具體其非公平鎖與公平鎖的源碼
查看公平鎖的源碼
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* Acquires only if reentrant or queue is empty.
*/
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}
通過代碼實例具體操作
//第一步 創(chuàng)建資源類,定義屬性和和操作方法
class LTicket {
//票數(shù)量
private int number = 30;
//創(chuàng)建可重入鎖
private final ReentrantLock lock = new ReentrantLock(true);
//賣票方法
public void sale() {
//上鎖
lock.lock();
try {
//判斷是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" :賣出"+(number--)+" 剩余:"+number);
}
} finally {
//解鎖
lock.unlock();
}
}
}
public class LSaleTicket {
//第二步 創(chuàng)建多個線程,調(diào)用資源類的操作方法
//創(chuàng)建三個線程
public static void main(String[] args) {
LTicket ticket = new LTicket();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
結(jié)果截圖如下

都是A線程執(zhí)行,而BC線程都沒執(zhí)行到,出現(xiàn)了非公平鎖
具體改變其設(shè)置可以通過可重入鎖中的一個有參構(gòu)造方法
修改代碼為private final ReentrantLock lock = new ReentrantLock(true);
代碼截圖為

3. 可重入鎖
可重入鎖也叫遞歸鎖
而且有了可重入鎖之后,破解第一把之后就可以一直進(jìn)入到內(nèi)層結(jié)構(gòu)
Object o = new Object();
new Thread(()->{
synchronized(o) {
System.out.println(Thread.currentThread().getName()+" 外層");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 中層");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 內(nèi)層");
}
}
}
},"t1").start();
synchronized (o)代表鎖住當(dāng)前{ }內(nèi)的代碼塊
以上都是synchronized鎖機(jī)制
下面講解lock鎖機(jī)制
public class SyncLockDemo {
public synchronized void add() {
add();
}
public static void main(String[] args) {
//Lock演示可重入鎖
Lock lock = new ReentrantLock();
//創(chuàng)建線程
new Thread(()->{
try {
//上鎖
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外層");
try {
//上鎖
lock.lock();
System.out.println(Thread.currentThread().getName()+" 內(nèi)層");
}finally {
//釋放鎖
lock.unlock();
}
}finally {
//釋放做
lock.unlock();
}
},"t1").start();
//創(chuàng)建新線程
new Thread(()->{
lock.lock();
System.out.println("aaaa");
lock.unlock();
},"aa").start();
}
}
在同一把鎖中的嵌套鎖,內(nèi)部嵌套鎖沒解鎖還是可以輸出,但是如果跳出該線程,執(zhí)行另外一個線程就會造成死鎖
要把握上鎖與解鎖的概念,都要寫上

4. 讀寫鎖(共享鎖與獨占鎖)
讀鎖是共享鎖,寫鎖是獨占鎖
- 共享鎖的一種具體實現(xiàn)
- 讀寫鎖管理一組鎖,一個是只讀的鎖,一個是寫鎖。
讀寫鎖:一個資源可以被多個讀線程訪問,也可以被一個寫線程訪問,但不能同時存在讀寫線程,讀寫互斥,讀讀共享(寫鎖獨占,讀鎖共享,寫鎖優(yōu)先級高于讀鎖)
讀寫鎖ReentrantReadWriteLock
讀鎖為ReentrantReadWriteLock.ReadLock,readLock()方法
寫鎖為ReentrantReadWriteLock.WriteLock,writeLock()方法
創(chuàng)建讀寫鎖對象private ReadWriteLock rwLock = new ReentrantReadWriteLock();
寫鎖 加鎖 rwLock.writeLock().lock();,解鎖為rwLock.writeLock().unlock();
讀鎖 加鎖rwLock.readLock().lock();,解鎖為rwLock.readLock().unlock();
案例分析:
模擬多線程在map中取數(shù)據(jù)和讀數(shù)據(jù)
完整代碼如下
//資源類
class MyCache {
//創(chuàng)建map集合
private volatile Map<String,Object> map = new HashMap<>();
//創(chuàng)建讀寫鎖對象
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//放數(shù)據(jù)
public void put(String key,Object value) {
//添加寫鎖
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在寫操作"+key);
//暫停一會
TimeUnit.MICROSECONDS.sleep(300);
//放數(shù)據(jù)
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 寫完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放寫鎖
rwLock.writeLock().unlock();
}
}
//取數(shù)據(jù)
public Object get(String key) {
//添加讀鎖
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName()+" 正在讀取操作"+key);
//暫停一會
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+" 取完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放讀鎖
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
//創(chuàng)建線程放數(shù)據(jù)
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
TimeUnit.MICROSECONDS.sleep(300);
//創(chuàng)建線程取數(shù)據(jù)
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
5. 互斥鎖
互斥鎖是獨占鎖的一種常規(guī)實現(xiàn),是指某一資源同時只允許一個訪問者對其進(jìn)行訪問,具有唯一性和排它性
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//創(chuàng)建互斥鎖并初始化 pthread_mutex_lock(&mutex);//對線程上鎖,此時其他線程阻塞等待該線程釋放鎖 //要執(zhí)行的代碼段 pthread_mutex_unlock(&mutex);//執(zhí)行完后釋放鎖
6. 自旋鎖
查看百度百科的解釋,具體如下 :
它是為實現(xiàn)保護(hù)共享資源而提出一種鎖機(jī)制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執(zhí)行單元獲得鎖。但是兩者在調(diào)度機(jī)制上略有不同。對于互斥鎖,如果資源已經(jīng)被占用,資源申請者只能進(jìn)入睡眠狀態(tài)。但是自旋鎖不會引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名
通俗的來說就是一個線程在獲取鎖的時候,如果鎖已經(jīng)被其它線程獲取,那么該線程將循環(huán)等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環(huán)。獲取鎖的線程一直處于活躍狀態(tài),但是并沒有執(zhí)行任何有效的任務(wù)。
其特點:
- 持有鎖時間等待過長,消耗CPU
- 無法滿足等待時間最長的線程優(yōu)先獲取鎖。不公平的鎖就會存在“線程饑餓”問題
- 自旋鎖不會使線程狀態(tài)發(fā)生切換,處于用戶態(tài)(不會到內(nèi)核態(tài)進(jìn)行線程的狀態(tài)轉(zhuǎn)換),一直都是活躍,不會使線程進(jìn)入阻塞狀態(tài),減少了不必要的上下文切換,執(zhí)行速度快。
其模擬算法如下
do{
b=1;
while(b){
lock(bus);
b = test_and_set(&lock);
unlock(bus);
}
//臨界區(qū)
//lock = 0;
//其余部分
}while(1)
7. 無鎖 / 偏向鎖 / 輕量級鎖 / 重量級鎖
- 無鎖:沒有對資源進(jìn)行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功
- 偏向鎖:是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖,降低獲取鎖的代價
- 輕量級鎖:鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能
- 重量級鎖:線程并發(fā)加劇,線程的自旋超過了一定次數(shù),或者一個線程持有鎖,一個線程在自旋,還有線程要訪問
以上就是詳解java中各類鎖的機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于java鎖的機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java List接口與Iterator接口及foreach循環(huán)使用解析
這篇文章主要介紹了Java List接口與Iterator接口及foreach循環(huán),主要包括List接口與Iterator接口及foreach循環(huán)具體的使用方法和代碼,需要的朋友可以參考下2022-04-04
Mybatis -如何處理clob類型數(shù)據(jù)
這篇文章主要介紹了Mybatis 如何處理clob類型數(shù)據(jù)的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
Spring?Boot?Admin?添加報警提醒和登錄驗證功能的具體實現(xiàn)
報警提醒功能是基于郵箱實現(xiàn)的,當(dāng)然也可以使用其他的提醒功能,如釘釘或飛書機(jī)器人提醒也是可以的,但郵箱報警功能的實現(xiàn)成本最低,所以本文我們就來看郵箱的報警提醒功能的具體實現(xiàn)2022-01-01
詳解Spring mvc DispatchServlet 實現(xiàn)機(jī)制
本篇文章主要介紹了詳解Spring mvc DispatchServlet 實現(xiàn)機(jī)制,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09
使用@JsonFormat和@DateTimeFormat對Date格式化操作
這篇文章主要介紹了使用@JsonFormat和@DateTimeFormat對Date格式化操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
基于Zookeeper實現(xiàn)服務(wù)注冊和服務(wù)發(fā)現(xiàn)功能
無論是采用SOA還是微服務(wù)架構(gòu),都需要使用服務(wù)注冊和服務(wù)發(fā)現(xiàn)組件,本文將基于 Zookeeper 實現(xiàn)服務(wù)注冊和服務(wù)發(fā)現(xiàn)功能,如果跟我一樣有同樣的困惑,希望可以通過本文了解其他組件如何使用 Zookeeper 作為注冊中心的工作原理2023-09-09

