一、鎖的概念與種類
鎖通常用於保證多線程訪問共享資源的安全性,它能有效地避免資源競爭,確保多線程之間的數據同步。Java中的鎖可以分為以下幾種:
1. synchronized鎖
public synchronized void method() {
// critical section
}
使用synchronized關鍵字修飾的方法或代碼塊,即可將其變成同步方法(或同步代碼塊)。它是Java中最基本的鎖實現方式。
2. ReentrantLock鎖
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
ReentrantLock是一個可重入鎖,支持公平性和非公平性操作,並提供了比synchronized更加靈活的鎖控制。
3. ReadWriteLock鎖
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
// read
} finally {
lock.readLock().unlock();
}
lock.writeLock().lock();
try {
// write
} finally {
lock.writeLock().unlock();
}
ReadWriteLock允許多個線程同時讀取共享資源,但只能由一個線程修改資源。因此,它的讀寫鎖分別有readLock和writeLock兩種。
二、死鎖與活鎖
1. 死鎖
死鎖是指多個線程在獲取鎖的過程中,由於互相等待對方釋放鎖而陷入阻塞的狀態,無法繼續執行下去。如下代碼就存在死鎖的風險:
public class Deadlock {
private static final Object obj1 = new Object();
private static final Object obj2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println("Thread1 finished");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (obj2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println("Thread2 finished");
}
}
});
t1.start();
t2.start();
}
}
以上代碼中,t1和t2先後獲取到obj1和obj2,但又同時等待對方釋放鎖,從而導致死鎖的產生。
2. 活鎖
活鎖是指多個線程在獲取鎖的過程中,由於過度地釋放鎖或者過度地交換執行權,導致線程一直在重複執行同一段代碼,無法繼續向前執行。
三、鎖的優化
1. 鎖的粗化
鎖的粗化是指將一些連續的同步操作拼接在一起,減少線程競爭的次數。比如以下代碼:
public synchronized void method() {
// step1
// step2
// step3
// step4
}
以上代碼中,step1 ~ step4是一系列的同步操作,如果不進行粗化處理,則在每個步驟之間都需要獲取和釋放鎖,造成較大的時間和性能消耗。可以將代碼粗化,將所有同步操作都放在一個同步塊中:
public void method() {
synchronized (this) {
// step1
// step2
// step3
// step4
}
}
2. 鎖的細化
鎖的細化是指將原本的鎖拆解成多個鎖,從而減少競爭的粒度。比如以下代碼:
public synchronized void method() {
// step1
// step2
// step3
// step4
}
以上代碼中,如果step1 ~ step4之間存在比較重的計算操作,會導致該方法在執行時無法同時處理其它的請求。可以將鎖進行細化,將步驟拆解出來,從而減少競爭的粒度:
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private final Object lock3 = new Object();
private final Object lock4 = new Object();
public void method() {
synchronized (lock1) {
// step1
}
synchronized (lock2) {
// step2
}
synchronized (lock3) {
// step3
}
synchronized (lock4) {
// step4
}
}
3. 樂觀鎖
樂觀鎖是一種無鎖的實現方式,它假設並發的衝突非常少,並通過CAS操作來檢查和更新數據。對於不頻繁的衝突,樂觀鎖可以在多線程並發中提供良好的性能和吞吐量。Java中的Atomic類就是樂觀鎖的一種實現方式。
四、鎖的使用場景
1. 高並發訪問的共享資源
在高並發訪問下,共享資源容易被多個線程同時訪問,導致數據不一致等問題。這時可以使用鎖來保證數據的一致性。例如收銀台模擬程序中,多個售票員同時向一個賬戶扣款,需要使用鎖保證操作的原子性。
2. 多線程間的通信
在多線程間的通信中,常常需要一個線程等待另一個線程執行完畢,在該線程繼續執行。這時可以使用鎖來實現線程之間的互斥和同步。例如生產者和消費者模型中,需要用鎖機制來實現對共享資源(緩衝區)的同步訪問。
3. 避免數據競爭
數據競爭是指當兩個或多個線程同時訪問某個變量,並且至少其中一個線程對變量進行了寫操作,此時可能會導致變量的值不確定性,出現嚴重錯誤。這時可以使用鎖來避免數據競爭的產生。
五、案例演示
以下是一個使用ReentrantLock鎖的示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public int incrementAndGet() {
lock.lock();
try {
Thread.sleep(100); // 模擬長時間計算
return ++count;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable runnable = () -> {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": " + counter.incrementAndGet());
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count: " + counter.count);
}
}
運行以上代碼,可以看到輸出結果:
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-1: 4
Thread-1: 5
Thread-1: 6
count: 6
以上代碼中,counter是一個計數器,在incrementAndGet方法中使用了ReentrantLock鎖,來保證count變量的線程安全。在main方法中,開啟兩個線程同時訪問incrementAndGet方法,從而驗證鎖的效果。
原創文章,作者:DXURW,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/333402.html