目录
并发编程发展到如今几乎已经成为了开发人员的必备技能。很多高级工程师、专家,甚至于架构师都站在“山顶”聊并发,讲怎么并发编程。作为一个以Java为主语言的低级开发人员,我也想尝试从“山脚”的角度,仰望一下并发。
为什么有并发编程
很久很久以前,那个时候计算机还没有操作系统,一台计算机从头到尾只运行一个程序,并且这个程序可以访问计算机的所有资源。这种情况下,编写运行程序是相当的困难,而且一次运行一个程序对于资源也是极大的浪费。
后来操作系统就出现了,操作系统可以支持计算机运行多个进程,并且操作系统为每个独立的进程分配各种资源。但是调度进程又有很多的麻烦之处:
- 进程的时空开销大
- 进程间通信十分复杂,需要操作系统中的系统调用来支持
- 并发粒度低,在多CPU计算机的发展下,进程间的并发度提高,但是进程内的并发度却无法提高
因此,就产生了线程的概念,在一个进程中可以包含多个可以并发执行的线程。系统按进程分配所有除CPU意外的系统资源(如主存、外设、文件等),而程序则依赖于线程运行,系统按线程分配CPU资源。
引入了线程后,CPU的调度单位就变为了线程,对于同一个进程内的线程来讲,因为他们共享相同的资源,在多个线程访问相同的资源时,就会产生并发问题。
例如:
public class UnsafeCount {
private int value;
public int getNext(){
return value++;
}
}
当有多个线程调用getNext方法时,返回的值一定是递增的吗?若是以下的线程调度情况,两个线程返回的结果是相同的:
这只是其中一种场景而已,因为value++并不是一个原子操作,实际包含了三个步骤:读取value的值、value加1和将结果写入value。CPU的线程调度会发生在任意时刻,所以无法保证线程之间执行的先后顺序性。在多CPU系统中,并发问题会更多,因为多个线程可以真正的并行执行,还涉及到了可见性的问题。
所以我们可以说,为了提高资源的利用率、提高资源利用的公平性、提高系统的吞吐率等等,使得我们原来很简单的编码工作变的越来越复杂。
但是对于系统性能的提升、建模便捷性的提高,这些相对复杂的编码工作又显的性价比很高。
并发涉及的问题
并发涉及到了两个最基础的问题:线程安全性与内存可见性。
线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
往往我们所写的类都不是线程安全的,例如上面的实例代码中,我们希望每次获取的数值可以递增加一,但是在多线程的环境下,结果却并不尽如人意。
这种因为不恰当的执行顺序而产生的的不正确的结果,称之为竞态条件。
因为操作并不是原子的,所以我们执行的结果要依赖与不确定的执行顺序,所以就会有竞态条件,那么该类就不是线程安全的。如果我们把value++变为原子操作,就可以满足我们的期望:
public class UnsafeCount {
private AtomicInteger value = new AtomicInteger();
public int getNext(){
return value.getAndIncrement();
}
}
我们把value变为了原子类AtomicInteger,对于value的getAndIncrement操作是一个原子操作,即对于两个线程都要执行getAndIncrement时,一个线程要不完全执行完该操作,要不完全不执行,这就保证了不会产生竞态条件。
但是只有上面所描述的原子性一般也无法满足线程安全的要求,如下:
public class UnsafeCount {
private AtomicInteger value = new AtomicInteger();
private AtomicInteger key = new AtomicInteger();
public int getNext() {
return value.getAndIncrement() + key.getAndIncrement();
}
}
value和key都是原子操作,但是两个操作之间却无法保证原子性。所以多个原子操作之间也是存在竞态条件的,所以我们需要在单个原子操作上完成所有的状态改变。那么我们就有了加锁的机制,加锁可以保证加锁内的代码以串行的方式来执行,保证线程的安全性。
仅仅有线程安全性还不足以保证我们构建线程安全类,以为我们不仅仅需要保证状态变化的原子性,还需要在状态变化后,其他线程可以立刻感知到该变化,也就是内存可见性。
所以对于加锁来讲,不仅仅是对共享资源访问的互斥,还有内存可见性。
锁
对于Java来讲,锁的支持分为了两个层面,JVM层的支持与JDK层面的支持。像经常使用的synchronized关键字就是JVM层面的锁支持,而JUC中的lock都是JDK层面的支持。
说锁之前,我们要先弄明白两个概念CAS与对象头。
CAS
CAS即compare and swap,比较并交换,这是一种无锁化的思想,即我们假设访问共享资源是不会发生冲突的,所以我们就不需要阻塞其他的线程。如果出现了冲突,我们重试当前的操作直到成功为止。CAS设计三个参数:v内存中实际的值,o我们期望的值,n新的值。当v==o时,即内存中实际的值与我们期望的值相等,说明没有发生冲突,我们把内存中的值更新为n。如果v!=n,即内存中的值与我们期望的值不相等,就说明发生了冲突,我们返回内存中的值v即可。在多线程中,可以保证有且只有线程可以执行成功,其他的线程需要利用返回的新的内存中的值v来重试操作,直到成功。当然了,这里v与o的比较,并且更新内存中的值需要是原子操作,否则会产生竞态条件,这里是利用了汇编指令cmpxchg来实现的原子性。
CAS有ABA的问题,即数据的更新路径为A--->B--->A,我们以为内存中的数据A没有发生过变化,而实际上已经发生了若干次的变化,同样该问题也很好解决,为每次的数据变更增加版本号即可。在JDK中的AtomicStampedReference就是采用的这种方式,我们简单看一下他的cas方法是如果使用的:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
这里的Pari就是封装了value与版本号的类,只需要比较当前的值与我们期望的值相等,当前的版本号与我们期望的版本号相等,并且新的值和版本号已经与我们期望的相等了或者casObject方法返回成功。
对象头
再看对象头,所有的Java对象都会有一个对象头,以下内容来自《java并发编程艺术》:
其中有锁的标志位,一共有四种:无锁、偏向锁、轻量级锁和重量级锁。锁的状态会随着竞争的情况而升级,从偏向锁升级到轻量级锁,最终升级为重量级锁。这个过程是不可逆的。对于每种锁的对象头分布如下;
synchronized
有了前面的基础内容,我们再来看synchronized关键字,一般synchronized关键字可以来修饰方法、代码块,我们先来看例子:
public class Sync {
public void test1() {
synchronized (Sync.class) {
}
}
public synchronized void test2(){
}
}
public class Sync
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
{
public Sync();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class Sync
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
public synchronized void test2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
}
通过字节码可以清楚地看到,当synchronized修饰代码块时,利用的字节码monitorenter与monitorexit来做加锁与释放锁。当synchronized修饰方法时,会利用ACC_SYNCHRONIZED标志来标记该方法。
synchronized加锁的过程就是前面所述的锁升级过程
首先是偏向锁,当一个线程访问同步代码并获取锁时,会先在对象头中设置该线程id,并标记为偏向锁。在没有竞争的情况下,该线程在加锁和释放锁时只需要比较对象头中的线程id是否为当前线程即可。
只有当出现竞争时,即当前偏向锁对象头中保存的线程id与要加锁的线程不是同一个,则会撤销偏向锁,如果原来保存的线程已经不存在了,则继续设置锁为偏向锁,并保存偏向锁id为新线程id。如果原来保存的线程还存在,则会升级为轻量级锁。
轻量级锁:首先是在当前线程栈帧中创建用来保存锁记录的空间,并将对象头的markword复制到锁记录中,然后再通过CAS的方式尝试替换对象markword为指向锁记录的指针。如果CAS成功,则加锁成功,如果CAS失败,则会自旋。
因为一般情况下,线程阻塞等待锁需要上下文切换,是一个极其耗时的操作,并且一个线程不会长时间占用锁,所以线程会通过自旋的方式来尝试获取锁。如果自旋成功,说明在自旋时,加锁线程释放锁了,则新线程继续以轻量级锁的方式加锁。如果自旋失败,则会设置对象头为重量级锁。
轻量级锁释放锁时,通过会通过CAS操作把对象的markword替换回去,如果CAS失败了,则说明出现了竞争,此时会把锁膨胀为重量级锁。
即通过前面的monitorenter和monitorexit来实现,线程进入阻塞状态等待唤醒。
以上是由JVM层面实现的锁,接下来看JDK中的锁。
AQS
在JUC包中定义了一系列的锁和各式各样的同步器,他们几乎都是基于AQS来实现的。
AQS,从JDK的文档中可以看到:
AbstractQueuedSynchronizer依赖于一个先进先出的等待队列,提供了一种实现阻塞锁和各种同步器的基本框架。
AQS提供了共享模式和独占模式。
AQS利用volatile与CAS操作来实现线程间的通信。
在AQS中最关键的两个数据结构,一个是volatile类型的int变量status,用来表示线程的同步状态。另一个就是变种的CLH锁队列。
volatile保证了内存可见性,可见https://blog.youkuaiyun.com/ly262173911/article/details/105317251之前写的博客有简单介绍。CAS操作前文所述保证正了原子性。
CLH锁队列一般是用来实现自旋锁的,当有线程获取锁失败时,加入到队尾,然后一直自旋等待前驱节点释放锁。这就保证了当有线程释放锁后,只有其后继节点可以竞争获取锁,减少了锁冲突。
而在AQS中使用的CLH锁队列用来实现了线程的阻塞与唤醒。
我们从源码来看一下AQS是怎么做的,首先看独占模式的加锁操作:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先尝试获取锁,如果获取锁失败,则加入到等待队列中,然后在等待队列中获取锁,一直到成功或者被中断,返回是否中断状态,如果线程中断的话,则调用当前线程的interrupt方法。获取锁方法在AQS中没有定义,需要子类自己来实现。
再看加入等待队列的方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
如果当前的尾节点不为空,则把新节点的前驱节点设置为尾节点,然后通过CAS的方式,尝试把尾节点设置为新节点,如果成功的话,则把原来的尾节点的后继节点设置为新节点,就完成了加入等待队列的操作。如果CAS失败,则说明在CAS操作前尾节点已经被替换,则会进行enq操作。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里就是无锁化标准的CAS自旋操作的实践,通过自旋+CAS的方式,把新节点加入到队尾。
当入队后,则阻塞的获取锁,直到成功或者被中断:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
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);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
如果当前节点的前驱节点是头结点,则说明在新节点加入队列后,前驱结点已经释放锁了,所以这里可以再次尝试获取锁,如果获取锁成功,则把自己设置为头结点,然后返回非中断状态。
否则的话阻塞线程,阻塞线程之前还需要把前驱节点的状态设置为SIGNAL,如果前驱节点是CANCEL状态,则会跳过该节点继续往前找。这是因为只有前驱节点是SIGNAL状态时,在释放琐时,才会唤醒后继节点。最后就是当前线程进入阻塞状态,直到被前驱节点唤醒或者被中断。
再看加共享锁:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
同样首先尝试加锁,加锁成功则直接返回,该方法同样由子类自己来实现,该方法的返回值需要注意,负数表示加锁失败,0表示加锁成功但是已经没有剩余的资源给后继节点继续加锁了,正数表示后继节点还可以继续加锁。如果加锁失败,则继续以阻塞的方式,直到加锁成功。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
与独占模式唯一不同的地方在于如果获取锁成功,还会继续唤醒后继节点,即setHeadAndPropagate方法:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
我们可以看到,除了设置当前节点为头结点外,还根据是否有剩余资源等条件来继续唤醒后继节点。
我们再看下释放锁的操作,首先是独占模式的释放锁:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
首先也是尝试释放锁,该操作依旧是由子类来实现,如果释放锁成功,则会根据当前节点的状态来唤醒后继节点。如果当前节点的状态不等于0,说明还有后继节点等待唤醒。首先会把当前节点的状态设为0,方式重复唤醒。然后获取当前节点的后继节点,如果后继节点不为空则直接唤醒,如果为空或者为CANCEL状态,则会从队尾向前遍历找到第一个不为空并且可以通知的节点,唤醒。为什么从队尾向前遍历是有两个原因,一方面如果s==null,我们无法通过s继续获取next的。另一方案,如果waitStatus>0,说明取消了该节点的获取锁操作,在cancelAcquire方法中会把取消的节点的next设置为自己,所以也无法通过next来继续获取下一个节点。这里就只能同尾节点向前遍历。
共享模式的释放锁:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
同样先释放锁,该方法由子类来实现。如果释放锁成功,继续唤醒后继节点,这里与独占锁的区别就在与共享锁释放需要连续判断后继节点的状态,知道没有后继节点可以获取锁。
首先是判断如果当前节点的状态是SIGNAL状态,说明该节点有后继节点需要通知,注意这里需要CAS并且循环操作,方式防止并发,因为在加锁时也会调用该方法。如果设置状态成功,则该节点没有唤醒过,则唤醒该节点。如果CAS失败,说明该节点被其他线程唤醒,如果head没有改变,说明该节点加锁失败即资源部足够后继节点加锁,则结束循环。如果head发生变化,说明剩余资源足够后继节点加锁,则后继节点解锁成功,继续再唤醒后继节点。如果状态==0,通过CAS方式设置节点状态为传播状态,保证共享锁的唤醒操作可以继续向后继节点传播。
以上就是AQS的基本加锁、释放锁的流程。
在JDK的JUC包中有大量的AQS的使用最佳实践案例,向关于锁的重入锁、读写锁,还实现了公平与非公平锁。关于同步器的CountDownLatch、Semaphore等等。
ReentrantLock
我们简单介绍ReentrantLock锁,来看看如果利用AQS来构建锁。ReentrantLock是独占方式的可重入锁,支持公平锁、非公平锁两种,在创建ReentrantLock的时候可以指定是否公平。
在JUC包中所有的锁、同步器都没有继承AQS,而是采用了组合的方式来实现AQS的功能。在ReentrantLock中同样定义了公平锁FairSync与非公平锁NonFairSync,他们都继承了ReentrantLock中定义的抽象类Sync。在ReentrantLock中status用来表示锁是否已经被占有,0表示没有锁被占用,大于0时表示锁已经被持有,并且数值表示重入的次数。
我们先看非公平锁:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
非常的简单,定义了两个加锁方法,一个是阻塞的方式一直等待获取锁直到成功,一个是尝试获取锁直接返回结果。先看lock方法,首先通过CAS的方式尝试加锁,如果加锁成功则设置当前持有锁的线程为当前线程。失败的话,则通过AQS中的acquire方法来尝试获取锁。
tryAcquire方法则直接调用了父类Sync的nonFairTryAcquire方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
首先也是获取state,如果state等于0,同样通过CAS的方式尝试加锁,如果CAS成功则设置持有锁线程为当前线程,返回加锁成功。如果state不等于0,说明锁已经被占有,通过比较判断当前线程是否为持有锁的线程,如果是的话,则把持有锁的数量增加,返回加锁成功。否则返回加锁失败。
再看公平锁:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
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;
}
}
同样也是两个加锁方式,lock和tryLock,和非公平锁还是有很大区别的。lock方法没有直接尝试获取锁,而是直接调用了AQS的acquire方法,保证公平性。
在tryAcquire中,当state等于0时,也没有直接利用CAS抢占锁,而是先判断当前CLH锁队列中是否有比当前线程更早等待获取锁资源的节点,如果没有,才尝试获取锁。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
判断方法也非常简单,如果队列为空或者队列第一个等待的线程就是当前线程,说明当前线程可以直接获取锁。否则,需要排队等待。这也就保证了加锁的公平性。
再看释放锁:
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);
}
setState(c);
return free;
}
释放锁时,同样判断如果持有锁线程不是当前线程,则抛出异常。否则的话判断释放锁后,当前线程重入锁的次数是否为0,即当前线程是否全部释放锁,如果全部释放的话,清空持有锁线程状态。否则设置剩余的重入次数即可。
在JUC中还有一个常用的读写锁,实现原理几乎相同,只不过是更改了对于state的利用,state的低16位用来表示写锁的状态,state的高16位用来表示读锁的状态。不再详细介绍。
再简单介绍一个同步器,看一下在JUC中同步器是如何利用AQS来构造的
semaphore
即信号量,通常用来限制并发访问资源线程的数量。它把state用来表示许可的数量,即可以并发访问资源的线程数量。同样也分为了公平与非公平两种方式。
先看非公平:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
标准的循环+CAS的无锁化方式来尝试获取许可,如果当前剩余的许可数即state值,不足够这一次需要获取的许可数,或者CAS失败,则直接返回。会进入到AQS的doAcquireSharedInterruptibly方法,阻塞等待直到获取足够的资源。
公平的方式与非公平的区别就在于会先判断CLH锁队列是否有其他线程先于当前线程,在等待资源。
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
可以看到JUC包中的锁、同步器都是利用AQS来实现,只不过各种方式的利用state,可见AQS的框架实现的扩展性是非常的好,这种设计的思想非常值得学习。
未完待续......
原创公众号,期待大家关注: