1.爲什麼使用鎖,不使用鎖會有什麼影響?
public class Test {
public static int count=0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for(int i = 0 ;i<10000;i++){
new Thread(()->{
try{
lock.lock();
//count++不是原子操作
count++;
}finally {
//unlock一定要在finally中,避免死鎖
lock.unlock();
}
}).start();
}
try {
Thread.sleep(2000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
當count++
在lock.lock()
/lock.unlock()
中間時,輸出的count
就是想要得到的結果;
不加lock的時候多執行幾次,經常會比預期值小。因爲count++
不是原子操作,這裏就體現了lock
的作用。
2.ReentrantLock源碼分析
2.1思考實現原理
ReentrantLock
作用如此強大,他主要完成了幾個功能點。
- 記錄鎖是否被佔用
- 記錄佔用鎖的線程(處理鎖重入)
- 通過某種方式記錄鎖的順序(要考慮公平鎖)
- 所有的鎖進入阻塞狀態。
大概完成了這麼幾個功能點。如此就可以主要考慮ReentrantLock
是如何實現這些功能的。
ReentrantLock類圖
如此所示,很明顯,ReentrantLock
有三個內部類,公平鎖類FairSync
和非公平鎖類NonfairSync
而且這兩個類都繼承了Sync
類,重寫了Sync
的lock方法。
Sync
類繼承AbstractQueuedSynchronizer
類,AbstractQueuedSynchronizer
類通常說AQS
,實現線程排隊阻塞的一個機制。AQS
主要是維護了互斥變量,維護雙向鏈表,線程阻塞等。
new ReentrantLock()是非公平鎖,new ReentrantLock(true)是公平鎖。
ReentrantLock
構造器源碼:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.2ReentrantLock源碼
2.2.1 NonfairSync.lock()方法
ReentrantLock
中有抽象類Sync
,根據構造方法區分執行FairSync
還是NonfairSync
的實現。
以NonfairSync
實現爲例。
final void lock() {
//在state預期值爲0的時候,修改值爲1,此方法線程安全
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
compareAndSetState
方法類似數據庫中的樂觀鎖,預計值爲0的時候修改值爲1,如果修改成功,則成功獲取鎖,返回true
,並通過setExclusiveOwnerThread
方法記錄獲取鎖的線程;如果修改失敗,說明鎖已經被佔用,通過acquire(1)方法嘗試獲得鎖。
2.2.2 compareAndSetState(0,1)方法
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
//如果內存中的值爲expect則修改爲update,修改成功返回true,修改失敗返回false
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
// stateOffset 等於state的值
//state=0表示沒有線程獲取鎖,state=1表示有線程獲取鎖 state>1表示鎖重入
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
compareAndSwapInt
屬於CAS
相關(樂觀鎖。ConcurrentHashMap
,ConcurrentLinkedQueue
也有用CAS
來實現樂觀鎖),Unsafe
類提供了手動管理內容的能力,可以直接對內容進行處理。
至此,已經瞭解了ReentrantLock
通過AbstractQueuedSynchronizer.state
判斷是否佔用鎖,state=0表示沒有線程獲取鎖,state=1表示有線程獲取鎖 state>1表示鎖重入;
通過AbstractOwnableSynchronizer.exclusiveOwnerThread
記錄佔用鎖的線程。
2.2.3 acquire(1)方法
public final void acquire(int arg) {
//判斷是否是重入鎖
if (!tryAcquire(arg) &&
//addWaiter加入隊列
//acquireQueued線程阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//響應線程中斷
selfInterrupt();
}
2.2.4 tryAcquire(arg)方法
final boolean nonfairTryAcquire(int acquires) {
// 獲取當前線程
final Thread current = Thread.currentThread();
// 通過state狀態
int c = getState();
// 如果沒有線程獲取鎖
if (c == 0) {
//通過CAS操作獲取鎖,如果獲取成功,則設置佔用鎖的線程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 判斷是否是鎖重入getExclusiveOwnerThread 方法返回佔用鎖的線程
else if (current == getExclusiveOwnerThread()) {
//state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//設置state的值
setState(nextc);
return true;
}
//如果鎖已經被其他線程佔用,返回false
return false;
}
2.2.5 addWaiter(Node.EXCLUSIVE)方法
既然獲取鎖失敗,那麼就要將線程記錄起來,如下所示,通過鏈表的形式將所有線程保存起來(通過附錄查看Node
屬性)。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
// mode 傳入Node.EXCLUSIVE 表示互斥鎖 Node.SHARED 表示共享鎖(讀寫鎖中的讀鎖)
private Node addWaiter(Node mode) {
// 創建一個節點保存當前線程
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//獲取節點的尾節點。
//如果是線程B是第一個阻塞的節點,這裏是空值,通過enq方法進行設置head節點和tail節點。
Node pred = tail;
if (pred != null) {
//如果線程C在線程B之後,線程C執行到這裏,那麼這裏的pred是B
node.prev = pred;
//這時C爲最後一個節點,設置尾節點爲C
if (compareAndSetTail(pred, node)) {
//如果設置成功,設置B節點的next節點爲C
pred.next = node;
return node;
}
}
//第一個阻塞的線程進入這裏
//當然這裏可能B和C同時進入enq方法
enq(node);
return node;
}
2.2.5 enq(node)方法
//這裏可能很多線程一起進入。
private Node enq(final Node node) {
//for(;;)和while(true)效果是一樣的,但是一般是用for(;;)因爲指令少
for (;;) {
//獲取尾節點
Node t = tail;
if (t == null) { // Must initialize
//這裏又是CAS操作,預計head節點爲空時,修改值爲new Node()
//不管幾個線程執行此方法,但是隻有一個線程能執行成功。也就是第一個阻塞的線程
if (compareAndSetHead(new Node()))
//創建頭節點成功之後,因爲此時只有一個節點,所以這個節點即使頭節點也是尾節點。
tail = head;
} else {
//如果只有B線程進入enq方法,第一次循環設置頭節點,尾節點。
//第二次循環將線程B設置爲尾節點,並且把頭節點的next節點設置程B
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
這裏比較複製,如下所示:
通過A,B,C同時執行lock.lock()
方法,A最快,成功獲取鎖,線程B,C阻塞;
B和C都會執行addWaiter
方法,假如B的速度比C的速度快,而且在B已經執行完addWaiter
方法之後,C才進入addWaiter
方法;
那麼B會執行到enq
方法中進行for
循環,第一次循環創建頭節點,尾節點,第二次循環將B節點和頭節點進行關聯;
2.2.6 acquireQueued方法
// node爲當前線程節點的前一個節點
// arg爲1,表示想要修改state=1進行搶佔鎖
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 獲取當前節點的prev節點
final Node p = node.predecessor();
//如果當前節點的prev節點是head節點,證明當前線程馬上就要獲取到鎖。
// tryAcquire 方法與2.2.4方法效果一致
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//判斷是否阻塞線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 清理狀態
cancelAcquire(node);
}
}
2.2.7 shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// SIGNAL狀態說明線程準備好阻塞,等待喚醒
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// wx>0說明放棄獲取鎖,通過循環將這些節點從鏈表中剔除。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//通過CAS嘗試修改pred狀態爲SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.2.8 parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
//阻塞當前線程 this表示當前線程
LockSupport.park(this);
// 清理中斷狀態
return Thread.interrupted();
}
附錄:
AbstractQueuedSynchronizer與Node的關係。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
省略其他代碼
**/
//記錄頭節點
private transient volatile Node head;
//記錄尾節點
private transient volatile Node tail;
//記錄鎖狀態
private volatile int state;
static final class Node {
//記錄前一個節點
volatile Node prev;
// 記錄後一個節點
volatile Node next;
//取消獲取鎖
static final int CANCELLED = 1;
//準備好阻塞,等待線程喚醒
static final int SIGNAL = -1;
// 指示線程正在等待條件
static final int CONDITION = -2;
// 共享鎖中用到
static final int PROPAGATE = -3;
}
}
數據庫樂觀鎖簡單展示:
場景:商品表中有有一個商品,庫存爲1,如果有N個線程同時查到還有一個商品,進行下單,就會有超賣等現象,必須加鎖進行控制。
id | goodsname | goodsnum | version | |
---|---|---|---|---|
1 | 手機 | 1 | 1 |
- 線程A,B,C同時查到商品表中查到還有手機一部。
- 三個線程減庫存操作。
- 線程A通過執行
update t_goods set goodsnum=goodsnum-1,version=version+1 where id=1 and version=1 and goodsnum>0
修改成功。這時線程B,C修改的時候version
已經變成了2,不滿足where
條件,所以修改失敗。 - 線程A修改成功,可以進行支付操作,線程B,C修改庫存失敗,報異常說明庫存不足。
LockSupport.park/unpark
關於線程阻塞並喚醒,最快會想到wait/notify
組合。但是在此場景有缺陷,notify
只能隨機喚醒一個線程,notifyAll
喚醒所有線程,park/unpark
可以精準的實現喚醒某一個線程。