前言:
本文不會深入不會深入!科普文,就是歸納一下平時我們遇到的各種鎖,這樣聽到也不會太懵逼。真正深入的還是要看書的~
在Java開發(fā)中,特別是并發(fā)編程的時候我們會和很多的鎖打交道。寫下一篇筆記記錄一下各種鎖的概念。
1.死鎖
在學(xué)習(xí)操作系統(tǒng),或者并發(fā)編程中的時候我們經(jīng)常會遇到死鎖的概念。什么是死鎖?
死鎖是指兩個或兩個以上的進(jìn)程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程。
舉個簡單的例子:
一個線程需要搶占A鎖去執(zhí)行某段代碼,搶占到了A鎖后,要搶占B鎖去繼續(xù)執(zhí)行代碼。另外一個線程則需要搶占B鎖,然后搶占A鎖去執(zhí)行代碼。這樣就會造成彼此之間阻塞。兩個線程一起卡死。
如何解決?
- 避免寫嵌套鎖(廢話);
- 規(guī)范嵌套鎖的順序,多個線程用戶同一個順序并且放在同一個方法里;
- 引入超時機(jī)制(需要用到顯示鎖Lock)
2.顯示鎖,公平鎖,可重入鎖
標(biāo)題中的說的三種鎖說的就是Lock這個接口下的鎖,Lock具有這三種概念。
顯示鎖:非Java給我們提供的關(guān)鍵字去操作。而是我們自己定義鎖,然后顯示的獲取,顯示的釋放。
公平鎖:一定程度上保證線程獲得這個鎖的機(jī)會是公平的。但是這樣會大大消耗性能。
可重入鎖:看下面的代碼。這兩個是同步代碼塊。他們獲取的是同一把鎖。因此獲取鎖的是線程,不是對象。因此在線程沒有結(jié)束之前,這把鎖可以一直使用。
PS:Lock下的ReentrantLock也具有可重入性。
public class Widget {
// 獲得了鎖
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
// 獲得了鎖
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
3.Lock的介紹
其實synchronize從jdk一直發(fā)展到現(xiàn)在,性能已經(jīng)非常好。如果不是很有必要還是建議synchronize;因為簡單易用。不過這里還是要說一下Lock。下面是這個接口的方法。

lock:獲得鎖
lockInterruptibly:可打斷鎖
tryLock:嘗試獲取鎖。
tryLock帶時間:引入超時機(jī)制。很大程度上可以避免死鎖
unlock():釋放鎖
看上去是非常爽的。也非常的優(yōu)雅,畢竟啥時候加鎖上鎖的控制權(quán)在我們這里了。但是如果我們忽略了釋放鎖,或者說程序出了點(diǎn)問題,這個鎖沒有釋放。那就麻煩大了~因此還是要酌情使用!
怎么用呢?這里給一個demo
class LockDemo {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
4.ReentrantReadWriteLock
ReentrantReadWriteLock是一個讀寫鎖:
- 在讀取數(shù)據(jù)的時候,可以多個線程同時進(jìn)入到到臨界區(qū)(被鎖定的區(qū)域)
- 在寫數(shù)據(jù)的時候,無論是讀線程還是寫線程都是互斥的
當(dāng)我們讀的操作比較多。寫的操作比較少的時候可以用ReentrantReadWriteLock。
ReentrantReadWriteLock就兩個方法: - readLock():得到一個可以被多個讀操作共用的毒鎖。但是用會排斥所有寫操作
- writeLock():得到一個寫鎖。排斥其他所有讀操作和寫操作
舉個例子
讀鎖:
package com.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockTest2 {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
final LockTest2 lockTest2 = new LockTest2();
new Thread(new Runnable() {
@Override
public void run() {
lockTest2.get(Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lockTest2.get(Thread.currentThread());
}
}).start();
}
public void get(Thread thread) {
try {
lock.readLock().lock();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在進(jìn)行讀操作");
}
System.out.println(thread.getName() + "操作完畢");
}catch (Exception e) {
}
finally {
lock.readLock().unlock();
}
}
}

我們可以看到是交替進(jìn)行的~
寫鎖:
寫鎖只有將readLock改成writeLock就好了。這里不貼代碼了。直接觀察結(jié)果可以看出是互斥的。必須一個個來

5.悲觀鎖,樂觀鎖
悲觀鎖,樂觀鎖也是我們并發(fā)編程中常遇到的問題。在數(shù)據(jù)庫層面有為體現(xiàn)。
下面舉例子說明:

假設(shè)現(xiàn)在有兩個線程:
A線程來了,讀到了這個數(shù)據(jù),status為0;B線程改為了1了。由于數(shù)據(jù)庫的事務(wù)隔離機(jī)制。并發(fā)修改了這個數(shù)據(jù)。那數(shù)據(jù)就混亂了。
這個時候就要保持互斥性,同一個時間只能有一個線程來操作這行數(shù)據(jù)。
解決方案:
樂觀鎖:
加一個字段版本字段:
A線程讀出來的時候版本號位1,然后更新的時候加個條件update xxx where version =1;但是B線程已經(jīng)改為了2,所以會更新失敗;這樣就可以控制并發(fā)修改。一般是給用戶提示,有人已經(jīng)操作了數(shù)據(jù)。但是還是可以讀取這一行數(shù)據(jù)
悲觀鎖
SQL關(guān)鍵字FOR_UPDATE
select * from XXXX FOR_UPDATE
把查詢出來的數(shù)據(jù)鎖住了,其他人讀這條數(shù)據(jù)會阻塞。在用戶界面上會轉(zhuǎn)圈會等待。
舉例:
比如外賣。用戶下了一個單,這個時候訂單的數(shù)據(jù)異步到配送端了。此時,用戶突然申請退單了。此時數(shù)據(jù)庫訂單狀態(tài)應(yīng)該是退單狀態(tài)。然而由于時差,訂單的數(shù)據(jù)才到配送端。此時配送人員又搶單成功將訂單狀態(tài)重新改為了配送中(實際上這張訂單應(yīng)該是退單狀態(tài)的。)
6.分布式鎖
我們知道樂觀鎖,悲觀鎖是數(shù)據(jù)庫層面(單個)。如果我們分庫分表的話我們怎么處理呢?這個時候我們就要用分布式鎖。
我們可以用zookeeper,redis實現(xiàn)~具體實現(xiàn)大家可以baidu一下!