序言
由於最近項目上遇到了高併發問題,而自己對高併發,多線程這裏的知識點相對薄弱,尤其是基礎,所以想系統的學習一下,以後可能會出一系列的JUC文章及總結 ,同時也爲企業級的高併發項目做好準備。
本文是JUC文章的第四篇,如想看以往關於JUC文章,請點擊JUC系列總結
此係列文章的總結思路大致分爲三部分:
- 理論(概念);
- 實踐(代碼證明);
- 總結(心得及適用場景)。
在這裏提前說也是爲了防止大家看着看着就迷路了。
備註:此文在擁有相關線程基礎閱讀爲最佳,比如cas,synchronized,reentrantLock等。
java鎖大綱
- 從鎖的公平性來區分,可以分爲
公平鎖
和非公平鎖
; - 從鎖是否可重複獲取可分爲
可重入鎖
和不可重入鎖
; - 從資源已被鎖定,線程是否阻塞可以分爲
自旋鎖
; - 從線程是否對資源加鎖可以分爲
悲觀鎖
和樂觀鎖
; - 從那個多個線程能否獲取同一把鎖分爲
共享鎖
和排他鎖
。 - 多Jvm環境下多線程操作多個資源類分爲
分佈式鎖
。
公平鎖與非公平鎖
公平鎖
什麼是公平鎖呢?
公平鎖就是多個線程按照申請鎖的順序去獲得鎖,線程會直接進入隊列去排隊,永遠都是隊列的第一位才能得到鎖。
其實白話來講,就是多個線程排成一隊,先來後到,先到先得。
優缺點
優點:
- 很公平,所有的線程都能得到資源,不會造成線程飢餓。
缺點:
- 併發度低,cpu喚醒阻塞線程的開銷大。
非公平鎖
什麼是非公平鎖呢?
非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。
白話來講,就是多個線程搶奪,插隊現象。
優缺點
優點:
- 併發度高,減少CPU喚醒線程的開銷。
缺點:
- 導致優先級反轉或飢餓現象
在這裏說明一個細節問題:
synchronized
和juc.ReentrantLock
默認都是非公平鎖。ReentrantLock
在構造的時候傳入true
則是公平鎖。
代碼演示
class MyResource{
//非公平鎖
// private final ReentrantLock reentrantLock = new ReentrantLock();
//公平鎖
private final ReentrantLock reentrantLock = new ReentrantLock(true);
public void ifFair(){
System.out.println(Thread.currentThread().getName() +"線程 \t已經進入方法");
try{
reentrantLock.lock(); System.err.println(Thread.currentThread().getName()+"線程====== \t線程獲得了鎖");
}catch(Exception e){
e.getStackTrace();
}finally {
//釋放鎖
reentrantLock.unlock();
}
}
}
public class FairLockTest {
public static void main(String[] args) {
MyResource myResource = new MyResource();
for (int i = 0; i < 8; i++) {
new Thread(() ->{
myResource.ifFair();
},String.valueOf(i)).start();
}
}
}
公平鎖輸出如下:
0線程 已經進入方法
6線程 已經進入方法
4線程 已經進入方法
3線程 已經進入方法
1線程 已經進入方法
2線程 已經進入方法
7線程 已經進入方法
5線程 已經進入方法
0線程====== 線程獲得了鎖
6線程====== 線程獲得了鎖
4線程====== 線程獲得了鎖
3線程====== 線程獲得了鎖
1線程====== 線程獲得了鎖
2線程====== 線程獲得了鎖
7線程====== 線程獲得了鎖
5線程====== 線程獲得了鎖
可以看出是按申請鎖的順序來進行獲得鎖的。
非公平鎖輸出如下:
0線程 已經進入方法
5線程 已經進入方法
4線程 已經進入方法
3線程 已經進入方法
1線程 已經進入方法
2線程 已經進入方法
7線程 已經進入方法
6線程 已經進入方法
0線程====== 線程獲得了鎖
4線程====== 線程獲得了鎖
1線程====== 線程獲得了鎖
2線程====== 線程獲得了鎖
7線程====== 線程獲得了鎖
6線程====== 線程獲得了鎖
5線程====== 線程獲得了鎖
3線程====== 線程獲得了鎖
獲取鎖的順序已被打亂。
可重入鎖與不可重入鎖
可重入鎖
什麼是可重入鎖呢?
可重入鎖,又名遞歸鎖,指的是同一個線程在外層方法獲得鎖時,進入內層方法會自動獲取鎖。
白話來講,可以這麼形容,就像你有了家門的鎖,廁所、書房、廚房就爲你敞開了一樣(即便他們每個都有鎖)。可重入鎖可以避免死鎖的問題。
我們常用的ReenTrantLock
和synchronized
都是可重入鎖的體現。
不可重入鎖
與可重入鎖相反,即便它有大門鑰匙,廁所、書房、廚房你照樣進不去。
Lock
鎖爲不可重入鎖的體現。
鎖的配對
鎖之間要配對,加了幾把鎖,最後就得解開幾把鎖,下面的代碼編譯和運行都沒有任何問題。但鎖的數量不匹配會導致死循環。
如下就會導致死循環問題:
lock.lock();
lock.lock();
try{
someAction();
}finally{
lock.unlock();
}
代碼演示
可重入鎖演示
class MyData{
public synchronized void get(){
System.out.println(Thread.currentThread().getName() +"線程 \t"+ "獲取了值");
set();
}
private synchronized void set() {
System.out.println(Thread.currentThread().getName()+"線程\t"+"插入了值");
}
final ReentrantLock reenTrantLock = new ReentrantLock();
public void push(){
try{
reenTrantLock.lock();
System.out.println(Thread.currentThread().getName() +"線程\t"+"生產了產品");
poll();
}catch(Exception e){
e.getStackTrace();
}finally {
reenTrantLock.unlock();
}
}
private void poll() {
try{
reenTrantLock.lock();
System.out.println(Thread.currentThread().getName() +"線程\t"+"消費了產品");
}catch(Exception e){
e.getStackTrace();
}finally {
reenTrantLock.unlock();
}
}
}
public class ReentrancyTest {
public static void main(String[] args) {
MyData myData = new MyData();
//證明synchronized的可重入性
new Thread(() ->{
myData.get();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("====以下爲證明ReentrantLock的可重入性====");
//證明ReentrantLock的可重入性
new Thread(() ->{
myData.push();
}).start();
}
}
結果如下:
Thread-0線程 獲取了值
Thread-0線程 插入了值
===============以下爲證明ReentrantLock的可重入性===================
Thread-1線程 生產了產品
Thread-1線程 消費了產品
不可重入鎖演示
//構造一個不可重入鎖
class NoReenTrantLock{
private boolean isLock = false;
public synchronized void lock() throws InterruptedException {
while (isLock){
wait();
}
isLock = true;
}
public synchronized void unLock(){
isLock = false;
notify();
}
}
//構造資源
class NoReenTrantResource{
private NoReenTrantLock noReenTrantLock = new NoReenTrantLock();
public void send() throws InterruptedException {
noReenTrantLock.lock();
System.out.println(Thread.currentThread().getName()+"線程\t"+"發送成功");
receive();
noReenTrantLock.unLock();
}
public void receive() throws InterruptedException {
noReenTrantLock.lock();
System.out.println(Thread.currentThread().getName()+"線程\t"+"接收成功");
noReenTrantLock.unLock();
}
}
public class NoReenTrantcyTest {
public static void main(String[] args) throws InterruptedException {
//測試
new NoReenTrantResource().send();
//結果
//main線程 發送成功(並且程序一直出於阻塞狀態)
//其實另一種理解可以爲 可重入鎖有效的避免的死鎖問題。
}
}
結果如下:
自旋鎖
什麼是自旋鎖呢?
自旋鎖,就是一個線程嘗試去獲取某一把鎖的時候不會立即阻塞,而是採用循環的方式去嘗試獲取。自己在那兒一直循環獲取,就像“自旋”一樣。
優缺點
優點:
- 減少線程切換的上下文開銷,避免了用戶進程和內核切換的消耗;
缺點:
-
如果長時間上鎖的話,自旋鎖會非常耗費性能(消耗CPU,它阻止了其他線程的運行和調度)。
可以通過設置自旋時間。自旋時間可由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態來決定,基本認爲一個線程上下文切換的時間是最佳的一個時間。
CAS底層的getAndAddInt
就是自旋鎖思想。
//跟CAS類似,一直循環比較。
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
使用場景
-
鎖的競爭不激烈,且佔用鎖時間非常短的代碼塊,因爲自旋的消耗會小於線程阻塞掛起再喚醒的操作的消耗,這些操作會導致線程發生兩次上下文切換!
-
如果鎖的競爭激烈,或者持有鎖的線程需要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了;
因爲自旋鎖在獲取鎖前一直都是佔用 cpu 做無用功,佔着 XX 不 XX,同時有大量線程在競爭一個鎖,會導致獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操作的消耗,其它需要 cpu 的線程又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關閉自旋鎖。
代碼演示
//構造自旋鎖
class spinLock{
private AtomicBoolean islock = new AtomicBoolean(false);
public void lock(){
while(!tryLock()){
System.err.println(Thread.currentThread().getName() +"線程 \t"+"嘗試自旋獲取鎖");
}
System.out.println(Thread.currentThread().getName() +"線程 \t"+"獲得了鎖");
}
private boolean tryLock() {
return islock.compareAndSet(false,true);
}
public void unLock(){
while(!islock.compareAndSet(true,false)){
System.out.println(Thread.currentThread().getName() +"線程 \t"+"正在自旋解鎖");
}
System.out.println(Thread.currentThread().getName() +"線程 \t"+"已解鎖");
}
public void contrLock(AtomicBoolean islock){
this.islock = islock;
}
}
//構建資源類
class spinLockResource{
private spinLock spinLock = new spinLock();
//演示自旋鎖的獲取過程
public void send(){
spinLock.lock();
System.out.println("發送成功");
spinLock.unLock();
}
//控制自旋狀態
public void contrLock(AtomicBoolean islock){
spinLock.contrLock(islock);
}
}
//測試
public class SpinLockTest {
public static void main(String[] args) {
spinLockResource resource = new spinLockResource();
//資源操作線程
new Thread(() ->{
try{TimeUnit.MILLISECONDS.sleep(10);}catch(Exception e){e.getStackTrace();};
//演示自旋操作
resource.send();
},"oper").start();
//控制線程
new Thread(() ->{
resource.contrLock(new AtomicBoolean(true));
try{TimeUnit.MILLISECONDS.sleep(30);}catch(Exception e){e.getStackTrace();};
resource.contrLock(new AtomicBoolean(false));
},"control").start();
}
}
結果顯示:
oper線程 嘗試自旋獲取鎖
...
oper線程 嘗試自旋獲取鎖
oper線程 嘗試自旋獲取鎖
oper線程 嘗試自旋獲取鎖
oper線程 嘗試自旋獲取鎖
oper線程 嘗試自旋獲取鎖
oper線程 獲得了鎖
發送成功
oper線程 已解鎖
悲觀鎖與樂觀鎖
悲觀鎖
什麼是悲觀鎖呢?
悲觀鎖,其實他是一種悲觀思想,它總認爲別人在操作數據/資源的時候,會對數據/資源發生更改,從而在其持有數據/資源的時候,將其鎖住,到這另一個線程過來時發生阻塞,知道悲觀鎖將數據/資源釋放爲止。
其中,在傳統的數據庫中就運用了這種思想,比如行鎖,表鎖,寫鎖等…
而在Java層面上講,Synchronized
和 ReentrantLock
等獨佔鎖(排他鎖都是其思想的實現
因爲 Synchronzied 和 ReetrantLock 不管是否持有資源,它都會嘗試去加鎖,生怕自己心愛的寶貝被別人拿走。
樂觀鎖
什麼時樂觀鎖呢?
樂觀鎖,顧名思義,是一種樂觀的思想,它總認爲數據/資源不會被別人修改,所以在讀取上不會上鎖,但是在寫入操作的時候,他會進行判斷當前數據是否被修改過。具體實現方案有兩種:
-
版本號機制;
一般會在在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會+1。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
核心代碼:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
-
CAS實現。
即compare and swap 或者 compare and set,涉及到三個操作數,數據所在的內存值V,預期值A,新值B。當需要更新時,判斷當前V與A是否相等,若相等,則用B更新,若失敗則重試,一般情況下是一個自旋操作,即不斷的重試。
樂觀鎖的缺點
- 以上兩種機制均存在ABA問題,在CAS詳解及ABA問題的解決我們也層複述過,在這裏我們就不做過多解釋;
- 在併發度高,寫入頻繁的場景下,導致自旋循壞開銷大。
使用場景
讀取頻繁使用樂觀鎖,寫入頻繁使用悲觀鎖。
共享鎖與排他鎖
共享鎖
共享鎖是什麼呢?
共享鎖,又稱讀鎖,指的是允許多個線程同時獲取一個鎖,一個鎖可以同時被多個線程擁有。
如果某個線程對資源加上共享鎖後,則其他線程只能對資源再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數據,不能修改數據。
排他鎖
排他鎖是什麼呢?
排他鎖,又稱獨佔鎖,指的是一個鎖在某一時刻只能被一個線程佔有,其它線程必須等待鎖被釋放之後纔可能獲取到鎖。
synchronized
、ReentrantLock
和Lock
都是獨佔鎖的體現。
讀寫鎖
另外,我們在這裏說一個比較特殊的機制,ReentrantReadWriteLock
。
它的內部含有兩把鎖,ReadLock
和 WriteLock
,也就是一個讀鎖一個寫鎖,合在一起叫做讀寫鎖。
讀鎖是共享鎖,寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因爲讀鎖和寫鎖是分離的。所以ReentrantReadWriteLock的併發性相比一般的互斥鎖有了很大提升。
比如緩存,就需要讀寫鎖來控制。緩存就是一個鍵值對,以下Demo模擬了緩存的讀寫操作,讀的get
方法使用了ReentrantReadWriteLock.ReadLock()
,寫的put
方法使用了ReentrantReadWriteLock.WriteLock()
。這樣避免了寫被打斷,實現了多個線程同時讀。
代碼演示
class MyCache{
private volatile HashMap<String,String> map = new HashMap<>();
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//寫
public void addEle(String key,String value){
reentrantReadWriteLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在寫入: " + key);
//爲了增強顯示效果
try{ TimeUnit.SECONDS.sleep(1);}catch(Exception e){e.getStackTrace();};
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "\t" + "寫入完成······");
}catch(Exception e){
e.getStackTrace();
}finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
//讀
public void selectEle(String key){
reentrantReadWriteLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在讀取: " + key);
String result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t" + "讀取完成========== " + result);
}catch(Exception e){
e.getStackTrace();
reentrantReadWriteLock.readLock().unlock();
}
}
}
//測試類
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//寫
for (int i = 0; i < 5; i++) {
final String temp = i+"";
new Thread(() ->{
myCache.addEle(temp,temp);
},"Write"+String.valueOf(i)).start();
}
//讀
for (int i = 0; i < 5; i++) {
final String temp = i+"";
new Thread(() ->{
myCache.selectEle(temp);
},"Read"+String.valueOf(i)).start();
}
}
}
在未引入ReenTrantReadWriteLock時的輸出效果:
Write0 正在寫入: 0
Write2 正在寫入: 2
Write2 寫入完成``````````````````
Write1 正在寫入: 1
Write3 正在寫入: 3
Write3 寫入完成``````````````````
Write0 寫入完成``````````````````
Write4 正在寫入: 4
Write4 寫入完成``````````````````
Write1 寫入完成``````````````````
Read1 正在讀取: 1
Read2 正在讀取: 2
Read1 讀取完成========== 1
Read4 正在讀取: 4
Read4 讀取完成========== 4
Read2 讀取完成========== 2
Read0 正在讀取: 0
Read0 讀取完成========== 0
Read3 正在讀取: 3
Read3 讀取完成========== 3
在引入ReenTrantReadWriteLock時的輸出效果:
Write0 正在寫入: 0
Write0 寫入完成``````````````````0
Write1 正在寫入: 1
Write1 寫入完成``````````````````1
Write2 正在寫入: 2
Write2 寫入完成``````````````````2
Write3 正在寫入: 3
Write3 寫入完成``````````````````3
Write4 正在寫入: 4
Write4 寫入完成``````````````````4
Read0 正在讀取: 0
Read0 讀取完成========== 0
Read1 正在讀取: 1
Read1 讀取完成========== 1
Read2 正在讀取: 2
Read3 正在讀取: 3
Read4 正在讀取: 4
Read4 讀取完成========== 4
Read2 讀取完成========== 2
Read3 讀取完成========== 3
分佈式鎖
zookeeper分佈式鎖
未完待續…
Redis分佈式鎖
未完待續…
總結
以上爲Java常用的鎖,其實在我們知道的基礎上,一定要知道是什麼,怎麼用,爲什麼用以及使用場景,可能以上文章僅僅只是我的看法,如您有新的看法,請留言,多多溝通…