前言
上節我們介紹了volatile的內存語義,今天講講另一個同步原語鎖是如何保證同步的,我們同樣從鎖的內存語義講解,上節也有提到volatile的內存屏障能達到和鎖同樣的效果,那麼我們就來看看鎖的內存語義吧,OK,開始今天的併發編程之旅吧。
基於鎖建立的先行發生原則
鎖可以保證臨界區的執行互斥,而且可以讓釋放鎖的線程向獲取該鎖的線程發送消息,下面我們看下鎖是如何保證先行發生原則的:
鎖的釋放和獲取的內存語義
鎖的釋放內存語義:當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中去,如下所示:
鎖的獲取內存語義:當線程獲取鎖時,JMM會把該線程對應的本地內存設置爲無效,然後從主內存中讀取共享變量的值,如下所示:
鎖的內存語義過程總結:
對比上節的volatile我們可以看出來:鎖的釋放和volatile寫有着相同的內存語義,鎖的獲取與volatile的讀有着相同的內存語義;下面我們總結下鎖的內存語義幾點:
-
線程1釋放一個鎖:實際上就是線程1向接下來要獲取該鎖的某個線程(包括線程2,多線程下同一個時刻會有多個線程競爭該鎖,但是隻會有一個線程競爭成功)發出消息;
-
線程2獲取到該鎖:實際上就是線程2接收了之前某個線程(線程1)發出的消息;
-
線程1釋放鎖-線程2獲取鎖整個過程,實際上就是線程1通過主內存向線程2發送了變量更新的消息;
鎖內存語義的實現
我們以重入鎖ReentrantLock爲例子來講解寫鎖的內存語義:我們知道ReentrantLock類中獲取鎖和釋放鎖的方法分別爲:
ReentrantLock lock = new ReentrantLock();
lock.lock(); //獲取鎖
lock.unlock(); //釋放鎖
ReentrantLock分爲公平鎖和非公平鎖,我們分別展開講解
公平鎖獲取鎖,使用公平鎖時獲取鎖的lock()方法調用軌跡如下:
-
第一步:ReentrantLock自己的lock方法,其調用sync的lock()
ReentrantLock類:
public void lock() {
sync.lock();
}
-
第二步:Sync的Lock方法,調用AbstractQueuedSynchronizer的acquire方法
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
}
-
第三步:AbstractQueuedSynchronizer的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
-
第四步:真正調用ReentrantLock的tryAcquire方法獲取鎖
public class ReentrantLock implements Lock, java.io.Serializable {
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 獲取鎖的時候讀volatile變量
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
}
從上面我們能看出來加鎖的方法首先讀volatile的變量state
private volatile int state;
protected final int getState() {
return state;
}
公平鎖釋放鎖:
-
第一步:調用ReentrantLock的unlock方法
public void unlock() {
sync.release(1);
}
-
第二步:調用AbstractQueuedSynchronizer的release方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
-
第三步:調用ReentrantLock的tryRelease方法
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//釋放鎖的時候,寫volatile變量
setState(c);
return free;
}
}
}
下面是寫volatile變量的源碼:
private volatile int state; protected final void setState(int newState) { state = newState; }
結論:從上面對ReentrantLock公平鎖的源碼分析,我們可以得出,公平鎖在釋放鎖的最後寫volatile變量state,在獲取鎖的時候先讀volatile變量,根據先行發生原則釋放鎖的線程對volatile變量的寫,必須對volatile變量的讀可見,也就是上節講的volatile的可見性,所以公平鎖本質是通過volatile變量和AQS(這裏暫時不對AbstractQueuedSynchronizer講解)來實現同步的;
講完公平鎖,我們再看下非公平鎖,看看他又是怎麼實現的呢?
非公平鎖獲取鎖:
-
第一步:同公平鎖加鎖
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.lock();
}
}
-
第二步:調用NonfairSync的lock,方法如下
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
-
第三步:調用AQS的compareAndSetState方法
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
該方法以原子操作方式更新state
public final class Unsafe {
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
}
從之前文章我們知道compareAndSwapInt是sun.misc.Unsafe類裏面,編譯時會生成一條相關的CAS指令,CAS是可以保證原子性的(如果對這點不熟悉的小夥伴可以移步先看下我之前的文章:https://blog.csdn.net/chengyabingfeiqi/article/details/106597572);
之前說過CAS會生成一條帶有LOCK前綴的指令,該指令其實會有下面兩個作用(也算對前面文章的補充部分吧):
-
禁止該指令與之前和之後的讀和寫指令重排序
-
把寫緩衝區中的所有數據刷新到內存中
這兩點其實就已經滿足volatile的內存語義了,所以說CAS具有與volatile相同的內存語義;
鎖的內存語義實現總結:
綜上我們可以得出,鎖的內存語義有2種方式來保證同步:
-
使用volatile讀寫所具有的內存語義來實現
-
使用CAS的原子操作來實現具有volatile相同的內存語義效果
以上就是今天的鎖的內存語義相關內容,到目前這些內容包括volatile、CAS、鎖內存機制和原子類(Atomic)等都是我們後期講解Java併發編程包concurrent的基礎和底層實現,今天就到這了,感謝您的關注與閱讀!!!