可重入鎖(遞歸鎖)
本文里面講的是廣義上的可重入鎖,而不是單指 JAVA 下的 ReentrantLock。可重入鎖,也叫做遞歸鎖,指的是
同一線程 外層函數獲得鎖之后 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在 JAVA 環境下 ReentrantLock 和 synchronized 都是 可重入鎖。當然有可重入鎖就有不可重入鎖,不可重入鎖就是
同一線程 外層函數獲得鎖之后 ,只能當前函數使用
package com.yjc.juc; import java.util.concurrent.locks.ReentrantLock; class ZiYuan { ReentrantLock reentrantLock = new ReentrantLock(); public void method1() { reentrantLock.lock(); try { System.out.println("進入方法1"); System.out.println("准備調用方法2"); method2(); } catch (Exception e) { e.printStackTrace(); } finally { reentrantLock.unlock(); } } public void method2() { reentrantLock.lock(); try { System.out.println("進入方法2"); } catch (Exception e) { e.printStackTrace(); } finally { reentrantLock.unlock(); } } } public class MyLock { public static void main(String[] args) { ZiYuan ziYuan=new ZiYuan(); ziYuan.method1(); } }
公平鎖(Fair)
公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的線程會先被分配到鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式來定義公平鎖
加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得
非公平鎖(Nonfair)
JVM 按隨機、就近原則分配鎖的機制則稱為不公平鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式,默認為非公平鎖。非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機制。
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待
1. 非公平鎖性能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個隊列
2. Java 中的 synchronized 是非公平鎖,ReentrantLock 默認的 lock()方法采用的是非公平鎖。可以通過構造方法更改為公平鎖
ReadWriteLock 讀寫鎖
為了提高性能,Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程序的執行效率。讀寫鎖分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由 jvm 自己控制的,你只要上好相應的鎖即可。
讀鎖
如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖
寫鎖
如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
Java 中 讀 寫 鎖 有 個 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 體 的 實 現ReentrantReadWriteLock
package com.yjc.juc; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class MyReadWriteLock{ public static void main(String[] args){ ReadWriteLockDemo rwd = new ReadWriteLockDemo(); //啟動100個讀線程 for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { rwd.get(); } }).start(); } //寫線程 new Thread(new Runnable() { @Override public void run() { rwd.set((int)(Math.random()*101)); } },"Write").start(); } } class ReadWriteLockDemo{ //模擬共享資源--Number private int number = 0; // 實際實現類--ReentrantReadWriteLock,默認非公平模式 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //讀 public void get(){ //使用讀鎖 readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+" : "+number); }finally { readWriteLock.readLock().unlock(); } } //寫 public void set(int number){ readWriteLock.writeLock().lock(); try { this.number = number; System.out.println(Thread.currentThread().getName()+" : "+number); }finally { readWriteLock.writeLock().unlock(); } } }
樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到並發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,采取在寫先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復讀-比較-寫的操作。
java 中的樂觀鎖基本都是通過 CAS 操作實現的,CAS 是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。
悲觀鎖
悲觀鎖是就是悲觀思想,即認為寫多,遇到並發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會 block 直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如 RetreenLock。
自旋鎖
自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。線程自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那線程也不能一直占用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。
自旋鎖的優缺點
自旋鎖盡可能的減少線程的阻塞,這對於鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小於線程阻塞掛起再喚醒的操作的消耗,這些操作會導致線程發生兩次上下文切換!但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間占用鎖執行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用 cpu 做無用功,占着 XX 不 XX,同時有大量線程在競爭一個鎖,會導致獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操作的消耗,其它需要 cup 的線程又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關閉自旋鎖;
自旋鎖時間閾值(1.6 引入了適應性自旋鎖)
自旋鎖的目的是為了占着 CPU 的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的線程處於自旋狀態占用 CPU 資源,進而會影響整體系統的性能。因此自旋的周期選的額外重要!
JVM 對於自旋周期的選擇,jdk1.5 這個限度是一定的寫死的,在 1.6 引入了適應性自旋鎖,適應性自旋鎖意味着自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認為一個線程上下文切換的時間是最佳的一個時間,同時 JVM 還針對當前 CPU 的負荷情況做了較多的優化,如果平均負載小於 CPUs 則一直自旋,如果有超過(CPUs/2)個線程正在自旋,則后來線程直接阻塞,如果正在自旋的線程發現 Owner 發生了變化則延遲自旋時間(自旋計數)或進入阻塞,如果 CPU 處於節電模式則停止自旋,自旋時間的最壞情況是 CPU的存儲延遲(CPU A 存儲了一個數據,到 CPU B 得知這個數據直接的時間差),自旋時會適當放棄線程優先級之間的差異。
自旋鎖的開啟
JDK1.6 中-XX:+UseSpinning 開啟;
-XX:PreBlockSpin=10 為自旋次數;
JDK1.7 后,去掉此參數,由 jvm 控制;