併發編程系列之鎖的內存語義

前言

上節我們介紹了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的基礎和底層實現,今天就到這了,感謝您的關注與閱讀!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章