并发编程系列之锁的内存语义

前言

上节我们介绍了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的基础和底层实现,今天就到这了,感谢您的关注与阅读!!!

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