多線程_鎖
介紹鎖之前,先介紹一下JUC(java util concurrent)。它是java提供的一個工具包,裏面有我們常用的各種鎖,它分為3個包
- java.util.concurrent //如:volatile,CountDownLatch,CyclicBarrier,Semaphore
- java.util.concurrent.atomic //原子操作類對象:AtomicInteger…
- java.util.concurrent.locks //鎖:ReentrantLock,ReentrantReadWriteLock…
一:公平鎖/非公平鎖
- 公平鎖:加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得。
- 非公平鎖:加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待。
- 非公平鎖性能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個隊列
- ReentrantLock 默認的 lock()方法採用的是非公平鎖,可以通過new ReentrantLock(true)設置為公平鎖
- 非公平鎖性能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個隊列
二.可重入鎖(遞歸鎖)
- 指一個線程獲取外層函數鎖之後,內層遞歸函數也能仍然獲得該鎖的代碼 (就像進入自己的家的防盜門後,也同樣可以進卧室,衛生間…)
- ReentrantLock和synchronized 都是可重入鎖(遞歸鎖)
比如:
public synchronized void method1(){ ... method2(); } public synchronized void method2(){ ... }
作用:可重入鎖最大的作用就是避免死鎖。
三.自旋鎖
- 指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖。
- 這樣的好處是減少線程上下文切換的消耗(因為線程阻塞/喚醒代價很大),缺點是循環會消耗CPU。
- 應用:原子性操作類AtomicXXX就是採用自旋鎖+CAS使用。
手寫一個自旋鎖
public class SpinLockDemo { // 原子引用線程, 沒寫參數,引用類型默認為null AtomicReference<Thread> atomicReference = new AtomicReference<>(); //上鎖 public void myLock() { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "==>mylock"); // 自旋 while (!atomicReference.compareAndSet(null, thread)) { } //執行業務代碼 ... System.out.println(Thread.currentThread().getName() + "開始執行業務代碼"); } //解鎖 public void myUnlock() { //執行業務代碼 ... System.out.println(Thread.currentThread().getName() + "結束執行業務代碼"); Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName() + "==>myUnlock"); } // 測試 public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); }, "T1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); }, "T2").start(); } }
四.讀寫鎖
- 讀鎖(共享鎖):該鎖可被多個線程所持有。
- 寫鎖(獨佔鎖): 指該鎖一次只能被一個線程鎖持有
- 對於ReentranrLock和Synchronized而言都是獨佔鎖。
疑問:讀鎖和不加鎖有啥區別?
- 讀寫鎖是互斥的,共享的讀鎖是為了鎖住寫線程,也就是說在讀的時候不能寫 。
好處:
- 讀寫鎖保證:
- 讀-讀:能共存
- 讀-寫:不能共存
- 寫-寫:不能共存
- 提高並發的效率
使用
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //讀鎖 readWriteLock.readLock().lock(); readWriteLock.readLock().unlock(); //寫鎖 readWriteLock.writeLock().lock(); readWriteLock.writeLock().unlock();
五.悲觀鎖和樂觀鎖?
- 悲觀鎖:很悲觀,每次操作都加鎖。比如sync和lock.
- 樂觀鎖:很樂觀,每次都不加鎖。但是更新時會判斷一個條件。
- 一般採用version機制,和cas機制。
- update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
六.互斥鎖/同步鎖
- 互斥鎖:互斥是通過競爭對資源的獨佔使用,彼此沒有什麼關係,也沒有固定的執行順序。
- 就像加sync,lock鎖的時候就是互斥鎖。
- 同步鎖:同步是線程通過一定的邏輯順序佔有資源,有一定的合作關係去完成任務。
- 就像Barrier,Semphore這樣的機制。執行完某些線程才能執行下一個線程。
七.synchronized
synchronized 和 lock 區別: 最關鍵的就是 lock定製化比較高
- 首先synchronized是java內置關鍵字,在jvm層面,Lock是個java類;
- synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;
- synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放 鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;
- 用synchronized關鍵字的兩個線程1和線程2,如果當前線程1獲得鎖,線程2線程等待。如果線程1 阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結束了;
- synchronized的鎖可重入、不可中斷、非公平;而Lock鎖可重入、可判斷、可公平(兩者皆可)
- lock鎖可以喚醒指定線程
sync鎖升級
JDK1.6之後,對Synchronized進行了升級,將鎖分為幾個狀態: 無鎖->偏向鎖->輕量級鎖->重量級鎖 (不可逆)。
- 無鎖:沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源
- 偏向鎖:偏向第一個訪問Thread,非常的樂觀,從始至終只有一個線程請求某一把鎖
- 加鎖:
- 當線程第一次訪問時,在對象頭的相關位置(鎖狀態,持有的鎖,偏向鎖id記錄threadID)進行記錄。
- 後來這個線程再次進入時,比對ThreadID進行操作。
- 解鎖:
- 偏向鎖使用了一種等待競爭出現才釋放鎖的機制,只有別的線程也訪問該資源失敗時, 升級為輕量級鎖。
- 過程:在全局安全點,暫停擁有偏向鎖的線程,判斷偏向鎖線程是否存活,存活就升級為輕量級鎖,不存活就先變為無鎖狀態,再把搶佔的線程變為偏向鎖。
- 加鎖:
- 輕量級鎖:多個線程在不同的時間段請求同一把鎖,也就是說沒有鎖競爭
- 加鎖:
- 在訪問線程的棧中創建一個鎖記錄空間,將對象頭的信息放在這個空間,然後使用CAS將 對象頭轉變為一種指針,指向鎖記錄空間
- 解鎖:
- 使用CAS將鎖記錄空間的信息替換到對象頭中。
- 當存在多個線程競爭鎖的時候,就會進行自旋,自旋到一定數量,轉變為重量級鎖
- 加鎖:
- 重量級鎖:當一個線程拿到鎖時候,其他線程會進入阻塞狀態,當鎖被釋放的時候喚醒其他線程
sync的其他優化:
- 鎖粗化:將多次連接在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴展成為一個範圍更大的鎖。 比如stringBuffer.append方法.
- 鎖消除:根據逃逸技術,如果認為這段代碼是線程安全的,刪除沒有必要的加鎖操作
synchronized 的實現原理:
- 同步代碼塊是通過monitorEnter 和 monitorExit指令(位元組碼指令)獲取線程的執行權。
- 同步方法是通過acc_synchronized標誌實現線程的執行權的控制,一旦執行到同步方法, 就會先判斷是否有標誌位,才會去隱式的調用上面兩個指令。
- 具體過程:monitor對象存在於每個對象的對象頭,進入一個同步代碼塊,就會執行monitorEnter,就會獲取當前對象的一個持有權,這個時候monitor的計數器為1,當前線程就是這個monitor 的持有者,如果你已經是這個monitor的owner,再次進入,monitor就會 +1,同理當他執行 完monitorExit,對應的計數器就會減一,直到計數器為0,就會釋放持有權。
八.JUC的工具類(同步鎖)
- CountDownLatch:線程計數器(遞減)
//定義指定數量一個線程計數器 CountDownLatch count = new CountDownLatch(6); //計數器-1 count.countDown(); //線程阻塞,等待計數器歸零 count.await();
- CyclicBarrier:柵欄(七顆龍珠召喚神龍)(遞加)
//定義一個類似"召喚神龍"的對象,所有線程執行完再執行該線程 CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{}); //所有線程進入等待,等待線程全部執行完 cyclicBarrier.await();
小結:上述兩個方法雖然都是為了等待前面所有的線程執行完再執行後續的線程 ,但是CountDownLatc的後續線程只能是主線程,不能是分線程; 而CyclicBarrier的後續線程可以是分線程(自定義一個線程)
- Semaphore:信號量 (類似於阻塞隊列)
- 用於並發線程數的控制
(比如固定的幾個停車位,每個線程就是一個車,搶停車位) //定義一個停車位的對象 Semaphore semaphore = new Semaphore(8); //線程拿到停車位 semaphore.acquire(); //線程釋放停車位 semaphore.release();
寄語:做顆星星,有稜有角,還會發光