詳解java中各類鎖的機制
前言
總結java常見的鎖
區分各個鎖機制以及如何使用
使用方法 | 鎖名 |
---|---|
考察線程是否要鎖住同步資源 | 樂觀鎖和悲觀鎖 |
鎖住同步資源後,要不要阻塞 | 不阻塞可以使用自旋鎖 |
一個線程多個流程獲取同一把鎖 | 可重入鎖 |
多個線程公用一把鎖 | 讀寫鎖(寫的共享鎖) |
多個線程競爭要不要排隊 | 公平鎖與非公平鎖 |
1. 樂觀鎖與悲觀鎖
悲觀鎖:不能同時進行多人,執行的時候先上鎖。傳統的關系型數據庫裡邊就用到瞭很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖
樂觀鎖:通過版本號一致與否,即給數據加上版本,同步更新數據以及加上版本號。不會上鎖,判斷版本號,可以多人操作,類似生活中的搶票。每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機制實現事務的
(樂觀鎖可以使用版本號機制和CAS算法實現)
通過具體案例演示悲觀鎖和樂觀鎖
在redis框架中
執行multi之前,執行命令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是清空數據庫
但如果在另一個服務器上,輸入exec,會顯示出錯
因為用的是樂觀鎖,被修改瞭之後版本會發生改變
總的來說:
悲觀鎖:單獨每個人完成事情的時候,執行上鎖解鎖。解決並發中的問題,不支持並發操作,隻能一個一個操作,效率低
樂觀鎖:每執行一件事情,都會比較數據版本號,誰先提交,誰先提交版本號
2. 公平鎖與非公平鎖
公平鎖:先來先到
非公平鎖:不是按照順序,可插隊
- 公平鎖:效率相對低
- 非公平鎖:效率高,但是線程容易餓死
通過這個函數Lock lock = new ReentrantLock(true);。創建一個可重入鎖,true 表示公平鎖,false 表示非公平鎖。默認非公平鎖
通過查看源碼
帶有參數的ReentrantLock(true)為公平鎖
ReentrantLock(false)為非公平鎖
主要是調用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; }
通過代碼實例具體操作
//第一步 創建資源類,定義屬性和和操作方法 class LTicket { //票數量 private int number = 30; //創建可重入鎖 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 { //第二步 創建多個線程,調用資源類的操作方法 //創建三個線程 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(); } }
結果截圖如下
都是A線程執行,而BC線程都沒執行到,出現瞭非公平鎖
具體改變其設置可以通過可重入鎖中的一個有參構造方法
修改代碼為private final ReentrantLock lock = new ReentrantLock(true);
代碼截圖為
3. 可重入鎖
可重入鎖也叫遞歸鎖
而且有瞭可重入鎖之後,破解第一把之後就可以一直進入到內層結構
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()+" 內層"); } } } },"t1").start();
synchronized (o)代表鎖住當前{ }內的代碼塊
以上都是synchronized鎖機制
下面講解lock鎖機制
public class SyncLockDemo { public synchronized void add() { add(); } public static void main(String[] args) { //Lock演示可重入鎖 Lock lock = new ReentrantLock(); //創建線程 new Thread(()->{ try { //上鎖 lock.lock(); System.out.println(Thread.currentThread().getName()+" 外層"); try { //上鎖 lock.lock(); System.out.println(Thread.currentThread().getName()+" 內層"); }finally { //釋放鎖 lock.unlock(); } }finally { //釋放做 lock.unlock(); } },"t1").start(); //創建新線程 new Thread(()->{ lock.lock(); System.out.println("aaaa"); lock.unlock(); },"aa").start(); } }
在同一把鎖中的嵌套鎖,內部嵌套鎖沒解鎖還是可以輸出,但是如果跳出該線程,執行另外一個線程就會造成死鎖
要把握上鎖與解鎖的概念,都要寫上
4. 讀寫鎖(共享鎖與獨占鎖)
讀鎖是共享鎖,寫鎖是獨占鎖
- 共享鎖的一種具體實現
- 讀寫鎖管理一組鎖,一個是隻讀的鎖,一個是寫鎖。
讀寫鎖:一個資源可以被多個讀線程訪問,也可以被一個寫線程訪問,但不能同時存在讀寫線程,讀寫互斥,讀讀共享(寫鎖獨占,讀鎖共享,寫鎖優先級高於讀鎖)
讀寫鎖ReentrantReadWriteLock
讀鎖為ReentrantReadWriteLock.ReadLock,readLock()方法
寫鎖為ReentrantReadWriteLock.WriteLock,writeLock()方法
創建讀寫鎖對象private ReadWriteLock rwLock = new ReentrantReadWriteLock();
寫鎖 加鎖 rwLock.writeLock().lock();,解鎖為rwLock.writeLock().unlock();
讀鎖 加鎖rwLock.readLock().lock();,解鎖為rwLock.readLock().unlock();
案例分析:
模擬多線程在map中取數據和讀數據
完整代碼如下
//資源類 class MyCache { //創建map集合 private volatile Map<String,Object> map = new HashMap<>(); //創建讀寫鎖對象 private ReadWriteLock rwLock = new ReentrantReadWriteLock(); //放數據 public void put(String key,Object value) { //添加寫鎖 rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+" 正在寫操作"+key); //暫停一會 TimeUnit.MICROSECONDS.sleep(300); //放數據 map.put(key,value); System.out.println(Thread.currentThread().getName()+" 寫完瞭"+key); } catch (InterruptedException e) { e.printStackTrace(); } finally { //釋放寫鎖 rwLock.writeLock().unlock(); } } //取數據 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(); //創建線程放數據 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); //創建線程取數據 for (int i = 1; i <=5; i++) { final int num = i; new Thread(()->{ myCache.get(num+""); },String.valueOf(i)).start(); } } }
5. 互斥鎖
互斥鎖是獨占鎖的一種常規實現,是指某一資源同時隻允許一個訪問者對其進行訪問,具有唯一性和排它性
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//創建互斥鎖並初始化 pthread_mutex_lock(&mutex);//對線程上鎖,此時其他線程阻塞等待該線程釋放鎖 //要執行的代碼段 pthread_mutex_unlock(&mutex);//執行完後釋放鎖
6. 自旋鎖
查看百度百科的解釋,具體如下 :
它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為瞭解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在調度機制上略有不同。對於互斥鎖,如果資源已經被占用,資源申請者隻能進入睡眠狀態。但是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裡看是否該自旋鎖的保持者已經釋放瞭鎖,”自旋”一詞就是因此而得名
通俗的來說就是一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環。獲取鎖的線程一直處於活躍狀態,但是並沒有執行任何有效的任務。
其特點:
- 持有鎖時間等待過長,消耗CPU
- 無法滿足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在“線程饑餓”問題
- 自旋鎖不會使線程狀態發生切換,處於用戶態(不會到內核態進行線程的狀態轉換),一直都是活躍,不會使線程進入阻塞狀態,減少瞭不必要的上下文切換,執行速度快。
其模擬算法如下
do{ b=1; while(b){ lock(bus); b = test_and_set(&lock); unlock(bus); } //臨界區 //lock = 0; //其餘部分 }while(1)
7. 無鎖 / 偏向鎖 / 輕量級鎖 / 重量級鎖
- 無鎖:沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時隻有一個線程能修改成功
- 偏向鎖:是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖,降低獲取鎖的代價
- 輕量級鎖:鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能
- 重量級鎖:線程並發加劇,線程的自旋超過瞭一定次數,或者一個線程持有鎖,一個線程在自旋,還有線程要訪問
以上就是詳解java中各類鎖的機制的詳細內容,更多關於java鎖的機制的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- java並發編程中ReentrantLock可重入讀寫鎖
- Java中ReentrantLock4種常見的坑
- 新手瞭解java 多線程基礎知識
- Java並發編程之ReentrantLock實現原理及源碼剖析
- 一文瞭解Java讀寫鎖ReentrantReadWriteLock的使用