Lock
Lock
- Lock和synchronized,這兩個是最常見的鎖,它們都可以達到線程安全的目的,但是在使用上和功能上又有較大的不同。
- Lock并不是用來代替synchronized的,而是當使用synchronized不合適或不足以滿足要求的時候,來提供高級功能的。
為什么synchronized不夠用
- 效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的線程
- 不夠靈活(讀寫鎖更靈活):加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個對象),可能是不夠的
- 無法知道是否成功獲取到鎖
Lock接口
- 通常情況下,Lock只允許一個線程來訪問這個共享資源。不過有的時候,一些特殊的實現也可允許并發訪問,比如ReadWriteLock里面的ReadLock。
![]()
ReentrantLock實現類
既是互斥鎖,又是可重入鎖。
獲取鎖方法
lock()
- lock()就是最普通的獲取鎖。如果鎖已被其他線程獲取,則進行等待
- Lock不會像synchronized一樣在異常時自動釋放鎖,在finallly中釋放鎖,以保證發生異常時鎖一定被釋放
coding
/**
* 描述: Lock不會像synchronized一樣,異常的時候自動釋放鎖,所以最佳實踐是,finally中釋放鎖,以便保證發生異常的時候鎖一定被釋放
*/
public class MustUnlock {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try{
//獲取本鎖保護的資源
System.out.println(Thread.currentThread().getName()+"開始執行任務");
}finally {
lock.unlock();
}
}
}
- lock()方法不能被中斷,這會帶來很大的隱患:一旦陷入死鎖,lock()就會陷入永久等待
tryLock()
- tryLock()用來嘗試獲取鎖,如果當前鎖沒有被其他線程占用則獲取成功,則返回true,否則返回false,代表獲取鎖失敗
- 相比于lock,這樣的方法顯然功能更強大了,我們可以根據是否能獲取到鎖來決定后續程序的行為
- 該方法會立即返回,即便在拿不到鎖時不會一直在那等
tryLock(long time,TimeUnit unit)
超時就放棄,避免死鎖
coding
**
* 描述: 用tryLock來避免死鎖
*/
public class TryLockDeadlock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockDeadlock r1 = new TryLockDeadlock();
TryLockDeadlock r2 = new TryLockDeadlock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程1獲取到了鎖1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程1獲取到了鎖2");
System.out.println("線程1成功獲取到了兩把鎖");
break;
} finally {
lock2.unlock();
}
} else {
System.out.println("線程1獲取鎖2失敗,已重試");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程1獲取鎖1失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程2獲取到了鎖2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程2獲取到了鎖1");
System.out.println("線程2成功獲取到了兩把鎖");
break;
} finally {
lock1.unlock();
}
} else {
System.out.println("線程2獲取鎖1失敗,已重試");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程2獲取鎖2失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
==============運行結果=============
線程1獲取到了鎖1
線程2獲取到了鎖2
線程1獲取鎖2失敗,已重試
線程2獲取到了鎖1
線程2成功獲取到了兩把鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了兩把鎖
lockInterruptibly()
相當于tryLock(long time,TimeUnit unit)把超時時間設置為無限。在等待鎖的過程中,線程可以被中斷。
coding
public class LockInterruptibly implements Runnable {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibly lockInterruptibly = new LockInterruptibly();
Thread thread0 = new Thread(lockInterruptibly);
Thread thread1 = new Thread(lockInterruptibly);
thread0.start();
thread1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "嘗試獲取鎖");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "獲取到了鎖");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠期間被中斷了");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "釋放了鎖");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "獲得鎖期間被中斷了");
}
}
}
=============運行結果=============
Thread-0嘗試獲取鎖
Thread-1嘗試獲取鎖
Thread-0獲取到了鎖
Thread-1獲得鎖期間被中斷了
Thread-0釋放了鎖
isHeldByCurrentThread
鎖是否被當前線程持有。
一般調試開發使用
getQueueLength
返回當前正在等待這把鎖的隊列有多長。
一般調試開發使用
可見性
Lock的加解鎖和synchronized有同樣的內存語義,也就是說,下一個線程加鎖后可以看到所有前一個線程解鎖前發生的所有操作
synchronized可見性

lock可見性

鎖分類

樂觀鎖和悲觀鎖
樂觀鎖(非互斥同步鎖)
- 認為自己在處理操作的時候不會有其他線程來干擾,所以并不會鎖住被操作對象
- 在更新的時候,去對比在我修改的期間數據有沒有被其他人改變過:如果沒被改變過,就說明真的是只有我自己在操作,那我就正常去修改數據
- 如果數據和我一開始拿到的不一樣了,說明其他人在這段時間內改過數據,那我就不能繼續剛才的更新數據過程了,我會選擇放棄、報錯、重試等策略
- 樂觀鎖的實現一般都是利用CAS算法來實現的
- 原子類、并發容器屬于樂觀鎖
操作流程





悲觀鎖(互斥同步鎖)
- 如果我不鎖住這個資源,別人就會來爭搶,就會造成數據結果錯誤,所以每次悲觀鎖為了確保結果的正確性,會在每次獲取并修改數據時,把數據鎖住,讓別人無法訪問該數據,這樣就可以確保數據內容萬無一失
- Java中悲觀鎖的實現就是synchronized和Lock相關類
- 互斥同步鎖的劣勢
- 阻塞和喚醒帶來的性能劣勢
- 永久阻塞:如果持有鎖的線程被永久阻塞,比如遇到了無限循環、死鎖等活躍性問題,那么等待該線程釋放鎖的那幾個悲催的線程,將永遠也得不到執行
- 優先級反轉
- synchronized和lock接口屬于悲觀鎖
悲觀鎖流程


開銷對比
- 悲觀鎖的原始開銷要高于樂觀鎖,但是特點是一勞永逸,l合||四界區持鎖時間就算越來越差,也不會對互斥鎖的開銷造成影響
- 相反,雖然樂觀鎖一開始的開銷比悲觀鎖小,但是如果自旋時間很長或者不停重試,那么消耗的資源也會越來越多
- 悲觀鎖:適合并發寫入多的情況,適用于臨界區持鎖時間比較長的情況,悲觀鎖可以避免大量的無用自旋等消耗,典型情況:
- 臨界區有IO操作
2. 臨界區代碼復雜或者循環量大3臨界區競爭非常激烈
- 樂觀鎖:適合并發寫入少,大部分是讀取的場景,不加鎖的能讓讀取性能大幅提高。
可重入鎖和非可重入鎖
可重入?
再次申請鎖,無需釋放這把鎖,繼續使用這把鎖,也叫遞歸鎖。同一線程可多次獲取同一把鎖。
優點:
避免死鎖。
提升封裝性。
coding:
public class GetHoldCount {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
}
=======輸出結果========
0
1
2
3
2
1
0
coding
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();
try {
System.out.println("已經對資源進行了處理");
if (lock.getHoldCount()<5) {
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
==========輸出結果============
已經對資源進行了處理
1
已經對資源進行了處理
2
已經對資源進行了處理
3
已經對資源進行了處理
4
已經對資源進行了處理
4
3
2
1
非可重入?
ThreadPoolExecutor的Worker類
公平鎖和非公平鎖
公平指的是按照線程請求的順序,來分配鎖;非公平指的是,不完全按照請求的順序,在一定情況下,可以插隊。
注意:非公平也同樣不提倡“插隊”行為,這里的非公平,指的是“在合適的時機”插隊,而不是盲目插隊。
避免喚醒帶來的空檔期,提交高效.
ReentrantLock創建公平鎖
new ReentrantLock(true);
公平情況

不公平情況
- 如果在線程1釋放鎖的時候,線程5恰好去執行lock()
- 由于ReentrantLock發現此時并沒有線程持有lock這把鎖(線程2還沒來得及獲取到,因為獲取需要時間)
- 線程5可以插隊,直接拿到這把鎖,這也是ReentrantLock默認的公平策略,也就是“不公平”

coding
/**
* 描述: 演示公平和不公平兩種情況
*/
public class FairLock {
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完畢");
}
}
class PrintQueue {
private Lock queueLock = new ReentrantLock(true);//false不公平鎖
public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();//創建的是公平鎖,當前線程想再次拿到鎖,發現隊列當中有線程排隊等待拿鎖,當前線程會進入隊列排隊等待
//創建的是不公平鎖,當前線程會利用空檔期再次拿到鎖,向下繼續運行。
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
==============公平鎖輸出結果============
Thread-0開始打印
Thread-0正在打印,需要6
Thread-1開始打印
Thread-2開始打印
Thread-1正在打印,需要2
Thread-2正在打印,需要10
Thread-0正在打印,需要6秒
Thread-0打印完畢
Thread-1正在打印,需要3秒
Thread-1打印完畢
Thread-2正在打印,需要3秒
Thread-2打印完畢
說明:公平鎖,線程會按順序排隊獲取鎖
=============不公平鎖輸出結果=============
Thread-0開始打印
Thread-0正在打印,需要7
Thread-1開始打印
Thread-2開始打印
Thread-0正在打印,需要4秒
Thread-0打印完畢
Thread-1正在打印,需要1
Thread-1正在打印,需要8秒
Thread-1打印完畢
Thread-2正在打印,需要6
Thread-2正在打印,需要4秒
Thread-2打印完畢
特例
- 針對tryLock()方法,它是很猛的,它不遵守設定的公平的規則
- 例如,當有線程執行tryLock()的時候,一旦有線程釋放了鎖,那么這個正在tryLock的線程就能獲取到鎖,即使在它之前已經有其他現在在等待隊列里了
優缺點

共享鎖和排它鎖
- 排他鎖,又稱為獨占鎖、獨享鎖。
synchronized屬于排它鎖
- 共享鎖,又稱為讀鎖,獲得共享鎖之后,可以查看但無法修改和刪除數據,其他線程此時也可以獲取到共享鎖,也可以查看但無法修改和刪除數據
- 共享鎖和排它鎖的典型是讀寫鎖ReentrantReadWriteLock,其中讀鎖是共享鎖,寫鎖是獨享鎖
作用
- 在沒有讀寫鎖之前,我們假設使用ReentrantLock,那么雖然我們保證了線程安全,但是也浪費了一定的資源:多個讀操作同時進行,并沒有線程安全問題
- 在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,提高了程序的執行效率
規則
a)多個線程只申請讀鎖,都可以申請到
b)如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
c)如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
d)一句話總結:要么是一個或多個線程同時有讀鎖,要么是一個線程有寫鎖,但是兩者不會同時出現(要么多讀,要多一寫)
換一種思路更容易理解:讀寫鎖只是一把鎖,可以通過兩種方式鎖定:讀鎖定和寫鎖定。讀寫鎖可以同時被一個或多個線程讀鎖定,也可以被單一線程寫鎖定。但是永遠不能同時對這把鎖進行讀鎖定和寫鎖定。這里是把“獲取寫鎖”理解為“把讀寫鎖進行寫鎖定”相當于是換了一種思路,不過原則是不變的,就是要么是一個或多個線程同時有讀鎖(同時讀鎖定),要么是一個線程有寫鎖(進行寫鎖定),但是兩者不會同時出現
ReentrantReadWriteLock用法
public class CinemaReadWrite {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
}
========輸出=======
Thread1得到了讀鎖,正在讀取
Thread2得到了讀鎖,正在讀取
Thread2釋放讀鎖
Thread1釋放讀鎖
Thread3得到了寫鎖,正在寫入
Thread3釋放寫鎖
Thread4得到了寫鎖,正在寫入
Thread4釋放寫鎖
交互規則
不允許讀鎖插隊(公平)
允許寫鎖降為讀鎖,不允許讀鎖升為寫鎖
非公平和公平的ReentrantReadWriteLock的策略
公平鎖:不允許插隊
非公平鎖:
寫鎖可以隨時插隊
讀鎖僅在等待隊列頭結點不是想獲取寫鎖的線程時可以插隊
非公平:假設線程2和線程4正在同時讀取,線程3想要寫入拿不到鎖,于是進入等待隊列,線程5不在隊列里,現在過來想要讀取
此時有2種策略



public class NonfairBargeDemo {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
true); //false非公平鎖
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
System.out.println(Thread.currentThread().getName() + "開始嘗試獲取讀鎖");
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到讀鎖,正在讀取");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void write() {
System.out.println(Thread.currentThread().getName() + "開始嘗試獲取寫鎖");
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到寫鎖,正在寫入");
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->write(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->read(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
new Thread(()->read(),"Thread5").start();
new Thread(new Runnable() {
@Override
public void run() {
Thread thread[] = new Thread[1000];
for (int i = 0; i < 1000; i++) {
thread[i] = new Thread(() -> read(), "子線程創建的Thread" + i);
}
for (int i = 0; i < 1000; i++) {
thread[i].start();
}
}
}).start();
}
}
鎖升降級
寫鎖可降級讀鎖
讀鎖不可升級寫鎖,否則死鎖。
public class Upgrading {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void readUpgrading() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
Thread.sleep(1000);
System.out.println("升級會帶來阻塞");
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "獲取到了寫鎖,升級成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void writeDowngrading() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
Thread.sleep(1000);
readLock.lock();
System.out.println("在不釋放寫鎖的情況下,直接獲取讀鎖,成功降級");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
// System.out.println("先演示降級是可以的");
// Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
// thread1.start();
// thread1.join();
// System.out.println("------------------");
// System.out.println("演示升級是不行的");
Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
thread2.start();
}
}
自旋鎖和阻塞鎖
- 阻塞或喚醒一個Java線程需要操作系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間
- 如果同步代碼塊中的內容過于簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長
- 在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失
- 如果物理機器有多個處理器,能夠讓兩個或以上的線程同時并行執行,我們就可以讓后面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖
- 而為了讓當前線程“稍等一下”,我們需讓當前線程進行自旋,如果在自旋完成后前面鎖定同步資源的線程已經釋放了鎖,那么當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。
- 阻塞鎖和自旋鎖相反,阻塞鎖如果遇到沒拿到鎖的情況,會直接把線程阻塞,直到被喚醒
- 在java1.5版本及以上的并發框架java.util.concurrent的atmoic包下的類基本都是自旋鎖的實現
- AtomicInteger的實現:自旋鎖的實現原理是CAS,AtomicInteger中調用unsafe進行自增操作的源碼中的do-while循環就是一個自旋操作,如果修改過程中遇到其他線程競爭導致沒修改成功,就在while里死循環,直至修改成功
/**
* 描述: 自旋鎖
*/
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println(current.getName() + "自旋獲取失敗,再次嘗試");
}
}
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始嘗試獲取自旋鎖");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "獲取到了自旋鎖");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "釋放了自旋鎖");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
可中斷鎖
- 在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖,因為tryLock(time)和lockInterruptibly都能響應中斷。
- 如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以中斷它,這種就是可中斷鎖
鎖優化
- 縮小同步代碼塊
- 盡量不要鎖住方法
- 減少請求鎖的次數
- 避免人為制造“熱點”
- 鎖中盡量不要再包含鎖
- 選擇合適的鎖類型或合適的工具類


浙公網安備 33010602011771號