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