state:有一个int类变量表示持有锁的状态,通过CAS完成对state值的修改(0表示没有,1表示阻塞,大于1表示重入锁)
CLH:通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果


//此处是 Node 的部分属性
static final class Node {
//排他锁标识
static final Node EXCLUSIVE = null;
//如果带有这个标识,证明是失效了
static final int CANCELLED = 1;
//具有这个标识,说明后继节点需要被唤醒
static final int SIGNAL = -1;
//Node对象存储标识的地方
volatile int waitStatus;
//指向上一个节点
volatile Node prev;
//指向下一个节点
volatile Node next;
//当前Node绑定的线程
volatile Thread thread;
//返回前驱节点即上一个节点,如果前驱节点为空,抛出异常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化;
SIGNAL(-1):后面节点等待当前节点唤醒;
CONDITION(-2):Condition中使用,当前线程阻塞在Condition,如果其他线程调用了Condition的signal方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁;
PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;
0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成;
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(() -> {
lock.lock();
try{
System.out.println("-----A thread come in");
try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
}finally {
lock.unlock();
}
},"A").start();
//第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----B thread come in");
}finally {
lock.unlock();
}
},"B").start();
//第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----C thread come in");
}finally {
lock.unlock();
}
},"C").start();
}
}



线程A执行的上面截图圈中的代码,因为这时候state为0,所以线程A可以通过CAS自旋将state改成1,并把自己线程ID保存进去

因为线程A已经拿到了资源,所以线程B只能走else逻辑,一共三个核心方法
tryAcquire(arg)
addWaiter(Node.EXCLUSIVE), arg)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)


因为这时候队列还没有数据,尾节点是null,执行enq(node)

第一次自旋:
尾节点为空,创建一个空节点,并作为头节点,尾节点也指向这个空节点

第二次自旋:
将当前节点加入队列,尾节点指向当前节点(挂在哨兵节点后)

第一次自旋:


第二次自旋

执行结果如图,最终都调用到LockSupport.park(当前线程),均阻塞







公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的, 都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
非公平锁
final void lock() {
//直接尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
公平锁
final void lock() {
acquire(1);
}
还要一个重要的区别是在尝试获取锁时tryAcquire(arg),公平锁hasQueuedPredecessors()判断队列中是否还有其他线程:
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;
}
}
视频讲解:https://www.bilibili.com/video/BV1uX4y1u7ht?p=26&vd_source=b901ef0e9ed712b24882863596eab0ca
公平锁和非公平锁的区别:https://zhuanlan.zhihu.com/p/94015814