Java锁之AbstractQueuedSynchronizer源码解析

目录

目录

1.AbstractQueuedSynchronizer

1.1整体架构

1.2同步队列和同步器的状态

1.3同步队列——获取排他锁源码解析

1.4同步队列——获取共享锁源码解析

1.5同步队列——释放排他锁源码解析

1.6同步队列——释放共享锁源码解析

1.7条件队列——入队等待源码解析

1.8条件队列——signal源码解析

1.9条件队列——signalAll源码解析


1.AbstractQueuedSynchronizer


1.1整体架构

  • AQS是一个抽象类,定义了如何获取和释放锁的方法,需要子类去实现,自己实现了同步队列+条件队列的获取和释放(共享和排他锁)方法
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable{...}
  • 继承了 AbstractOwnableSynchronizer,它的作用就是为了知道当前是那个线程获得了锁,方便监控用的
  • AQS 的属性可简单分为四类:同步器简单属性、同步队列属性、条件队列属性、公用 Node
// 简单属性:
// 同步器的状态,子类会根据状态字段进行判断是否可以获得锁
// 比如 CAS 成功给 state 赋值 1 算得到锁,赋值失败为得不到锁, CAS 成功给 state 赋值 0 算释放锁,赋值失败为释放失败
// 可重入锁,每次获得锁 +1,每次释放锁 -1
private volatile int state;
// 自旋超时阀值,单位纳秒
// 当设置等待时间时才会用到这个属性
static final long spinForTimeoutThreshold = 1000L;

//同步队列属性:
// 同步队列的头。默认有哨兵节点,head = tail = new Node();
private transient volatile Node head;
// 同步队列的尾
private transient volatile Node tail;

//条件队列属性:
// 条件队列,从属性上可以看出是链表结构
public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    // 条件队列中第一个 node
    // 条件队列的头和尾默认是null。没有哨兵节点,head = tail = null;
    private transient Node firstWaiter;
    // 条件队列中最后一个 node
    private transient Node lastWaiter;
} 

ps:需要注意同步队列是有哨兵节点的,而等待队列是没有哨兵节点的,这会导致他们判断为空的条件是不同的

  • Node是同步队列和条件队列的共享节点,有共享和排他模式
static final class Node {
    /**
     * 同步队列单独的属性
     */
    //node 是共享模式
    static final Node SHARED = new Node();
    //node 是排它模式
    static final Node EXCLUSIVE = null;
    // 双向链表
    volatile Node prev;
    volatile Node next;

    /**
     * 两个队列共享的属性
     */
    volatile int waitStatus;
    // waitStatus 的状态有以下几种
    // 被取消
    static final int CANCELLED =  1;
    // SIGNAL 状态的意义:同步队列中的节点在自旋获取锁的时候,
    // 如果前一个节点的状态是 SIGNAL,那么自己就可以阻塞休息了,否则自己一直自旋尝试获得锁
    static final int SIGNAL = -1;
    // 表示当前 node 正在条件队列中,当有节点从同步队列转移到条件队列时,状态就会被更改成 CONDITION
    static final int CONDITION = -2;
    // 无条件传播,是在共享模式下,该状态的进程处于可运行状态
    static final int PROPAGATE = -3;
    // 当前节点的线程
    volatile Thread thread;
    // 同步队列中,nextWaiter 只是表示当前 Node 是排它模式还是共享模式,next表示下一个节点
    // 条件队列中,nextWaiter 就是表示下一个节点元素
    Node nextWaiter;
}
  • Condition 实例是绑定在锁上的,通过 Lock#newCondition 方法可以产生该实例;提供了一种线程协作方式:一个线程被暂停执行,直到被其它线程唤醒;
// 可以被进行中断
// 返回的 long 值表示剩余的给定等待时间,如果返回的时间小于等于 0 ,说明等待时间过了
// 选择纳秒是为了避免计算剩余等待时间时的截断误差
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 虽然入参可以是任意单位的时间,但底层仍然转化成纳秒
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 唤醒条件队列中的一个线程,在被唤醒前必须先获得锁
void signal();
// 唤醒条件队列中的所有线程
void signalAll();

1.2同步队列和同步器的状态

  • state 是锁的状态,是 int 类型,子类继承 AQS 时,都是要根据 state 字段来判断有无得到锁,比如当前同步器状态是 0,表示可以获得锁,当前同步器状态是 1,表示锁已经被其他线程持有,当前线程无法获得锁;
  • waitStatus 是节点(Node)的状态,种类很多,一共有初始化 (0)、CANCELLED (1)、SIGNAL (-1)、CONDITION (-2)、PROPAGATE (-3);
  • 同步队列,头节点是当前获取锁的节点

1.3同步队列——获取排他锁源码解析

// 排它模式下,尝试获得锁
public final void acquire(int arg) {
    // tryAcquire 方法是需要实现类去实现的,实现思路一般都是 cas 给 state 赋值来决定是否能获得锁
    if (!tryAcquire(arg) &&
        // addWaiter 入参代表是排他模式
        // acquireQueued 唤醒节点获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

如果没有获取到锁(设置state状态保证线程安全),将互斥模式节点加入到节点尾部,在尝试将节点进行唤醒,

如果获取到锁,方法就返回!

ps:一定注意这个方法的思想,获取到和没有获取到的区别!

// 方法主要目的:node 追加到同步队列的队尾
// 入参 mode 表示 Node 的模式(排它模式还是共享模式)
private Node addWaiter(Node mode) {
    // 初始化 Node
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // 尝试一下,如果失败就需要无限for了
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果还未初始化,自旋保证node加入到队尾
    enq(node);
    return node;
}
// 这里需要重点注意的是,返回值是添加 node 的前一个节点
private Node enq(final Node node) {
    for (;;) {
        // 得到队尾节点
        Node t = tail;
        // 如果队尾为空,说明当前同步队列都没有初始化,进行初始化
        // tail = head = new Node();
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        // 可能进来很多线程进行初始化,只有一个可以cas成功,其他线程都需要走这里
        } else {
            node.prev = t;
            // node 追加到队尾
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

其实就是将节点通过cas操作设置到队尾,但是需要判断未初始化的情况!

// 主要做两件事情:
// 1:通过不断的自旋尝试使自己前一个节点的状态变成 signal,然后阻塞自己。
// 2:获得锁的线程执行完成之后,释放锁时,会把阻塞的 node 唤醒,node 唤醒之后再次自旋,尝试获得锁
// interrupted :返回 false 表示获得锁成功,返回 true 表示失败
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 选上一个节点
            final Node p = node.predecessor();
            // 有两种情况会走到 p == head:
            // 1:node 之前没有获得锁,进入 acquireQueued 方法时,才发现他的前置节点就是头节点,于是尝试获得一次锁;
            // 2:node 之前一直在阻塞沉睡,然后被唤醒,此时唤醒 node 的节点正是其前一个节点,也能走到 if
            // 如果自己 tryAcquire 成功,就立马把自己设置成 head,把上一个节点移除
            // 如果 tryAcquire 失败,尝试进入同步队列
            if (p == head && tryAcquire(arg)) {
                // 获得锁,设置成 head 节点(也就是null节点)
                setHead(node);
                //p被回收
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // shouldParkAfterFailedAcquire 把 node 的前一个节点状态置为 SIGNAL
            // 只要前一个节点状态是 SIGNAL了,那么自己就可以阻塞(park)了
            // parkAndCheckInterrupt 阻塞当前线程
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果获得node的锁失败,将 node 从队列中移除
        if (failed)
            cancelAcquire(node);
    }
}
// 关键操作:
// 1:确认前一个节点是否有效,无效的话进行删除,并且一直往前找到状态不是取消的节点。
// 2: 把前一个节点状态置为 SIGNAL。
// 1、2 两步操作,有可能一次就成功,有可能需要外部循环多次才能成功(外面是个无限的 for 循环),但最后一定是可以成功的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 如果前一个节点 waitStatus 状态已经是 SIGNAL 了,直接返回,不需要在自旋了
    if (ws == Node.SIGNAL)
        return true;
    // 如果当前节点状态已经被取消了。
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    // 否则直接把节点状态置 为SIGNAL
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
// 阻塞,只有在排他锁被释放的时候才会进行唤醒下一个节点
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

@shouldParkAfterFailedAcquire先设置上一个节点的signal的waitStatus状态,

@parkAndCheckInterrupt然后阻塞自己,

@无限for循环,最后等待上一个节点释放锁将自己唤醒并且尝试获取锁

1.4同步队列——获取共享锁源码解析

  • 第一个不同,尝试获得锁的地方,有所不同,排它锁使用的是 tryAcquire 方法,共享锁使用的是 tryAcquireShared 方法
  • 第二个不同,在于节点获得排它锁时,仅仅把自己设置为同步队列的头节点即可(setHead 方法),但如果是共享锁的话,还会去唤醒自己的后续节点(setHeadAndPropagate 方法)
// 主要做两件事情
// 1:把当前节点设置成头节点
// 2:看看后续节点有无正在等待,并且也是共享模式的,有的话唤醒这些节点
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    // 当前节点设置成头节点
    setHead(node);
    // propagate > 0 表示已经有节点获得共享锁了
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //共享模式,还唤醒头节点的后置节点
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
// 释放后置共享节点
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 还没有到队尾,此时队列中至少有两个节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果队列状态是 SIGNAL ,说明后续节点都需要唤醒
            if (ws == Node.SIGNAL) {
                // CAS 保证只有一个节点可以运行唤醒的操作
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;        
                // 进行唤醒操作
                unparkSuccessor(h);
            }// 代表是初始化状态
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;          
        }
        if (h == head)  
            break;
    }
}

1.5同步队列——释放排他锁源码解析

// unlock 的基础方法
public final boolean release(int arg) {
    // tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明成功释放锁。
    if (tryRelease(arg)) {
        Node h = head;
        // 头节点不为空,并且非初始化状态
        if (h != null && h.waitStatus != 0)
            // 从头开始唤醒等待锁的节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
private void unparkSuccessor(Node node) {
    // node 节点是当前释放锁的节点,也是同步队列的头节点
    int ws = node.waitStatus;
    // 把节点的状态置为初始化
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 拿出 node 节点的后面一个节点
    Node s = node.next;
    // s 为空,表示 node 的后一个节点为空
    // s.waitStatus 大于0,代表 s 节点已经被取消了
    // 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            // t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
            if (t.waitStatus <= 0)
                s = t;
    }
    // 唤醒以上代码找到的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

先尝试释放锁(cas设置状态),然后需要判断无效节点,最终唤醒离头节点最近的一个线程

1.6同步队列——释放共享锁源码解析

// 共享模式下,释放当前线程的共享锁
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared方法里面实际还是调用的unparkSuccessor方法(释放互斥锁时的方法)

1.7条件队列——入队等待源码解析

// 线程入条件队列
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 加入到条件队列的队尾
    Node node = addConditionWaiter();
    // 标记位置 A:一定需要释放资源
    // 加入条件队列后,会释放 lock 时申请的资源
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 确认node不在同步队列上,再阻塞,如果 node 已经在同步队列上,是不能够上锁的
    // 比如多线程下有人调用了singlAll
    while (!isOnSyncQueue(node)) {
        // 阻塞在条件队列上
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 标记位置 B:被唤醒后移动到同步队列里
    // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中的数据结构中去了
    // 节点苏醒,直接尝试 acquireQueued(设置上一个节点的signal状态,并阻塞自己,然后等待线程释放锁唤醒)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        // 如果状态不是CONDITION,就会自动删除
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

直接将当前线程加入到条件队列队尾,

然后释放锁资源并且进行阻塞(因为多线程,需要判断当前线程是否在同步队列)

最后,signal会把线程从等待队列移动到同步队列里,并调用acquireQueued该节点获取同步状态。

// 增加新的 waiter 到队列中,返回新添加的 waiter
// 如果尾节点状态不是 CONDITION 状态,删除条件队列中所有状态不是 CONDITION 的节点
// 如果队列为空,新增节点作为队列头节点,否则追加到尾节点上
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 删除无效节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 新建条件队列 node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 队列是空的,直接放到队列头
    if (t == null)
        firstWaiter = node;
    // 队列不为空,直接到队列尾部
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
// 删除无效节点
private void unlinkCancelledWaiters() {
    Node t = firstWaiter; // 当前遍历节点的指针
    Node trail = null;   //  当前遍历节点的前一个有效的节点
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            //删除当前node
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            // 尾节点为无效节点,需要进行更新
            if (next == null)
                lastWaiter = trail;// 尾节点指向上一个有效的节点
        }// 更新有效节点
        else
            trail = t;
        // 进行迭代
        t = next;
    }
}

在等待队列的队尾增加节点,先要进行删除无效节点,然后才会在尾部添加(需要判断空队列)

删除无效节点,主要就是通过一个遍历节点和上一个有效节点两个指针进行处理的。

1.8条件队列——signal源码解析

// 唤醒阻塞在条件队列中的节点
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 从头节点开始唤醒
    Node first = firstWaiter;
    if (first != null)
        // doSignal 方法会把条件队列中的节点转移到同步队列中去
        doSignal(first);
}
// 把条件队列头节点转移到同步队列去
private void doSignal(Node first) {
    // 该循环只会移动一个节点
    do {
        //如果只有一个节点,删除后头尾节点全部置为null,代表空队列
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // transferForSignal 方法会把节点转移到同步队列中去
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}  
// 返回 true 表示转移成功, false 失败
// 大概思路:
// 1. node 追加到同步队列的队尾
// 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒
final boolean transferForSignal(Node node) {
    // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // 状态修改成 SIGNAL,如果成功直接返回
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒当前节点
        LockSupport.unpark(node.thread);
    return true;
}

signal:先进行判断是否为空队列,实际通过doSignal进行调用

doSignal:while通过transferForSignal函数一定会确保一个节点转移成功,并且只能移动一个节点!

transferForSignal:将节点转移到同步队列,并且设置节点的前置节点的状态为signal,如果设置失败直接唤醒当前节点

1.9条件队列——signalAll源码解析

    public final void signalAll() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 拿到头节点
        Node first = firstWaiter;
        if (first != null)
            // 从头节点开始唤醒条件队列中所有的节点
            doSignalAll(first);
    }
    // 把条件队列所有节点依次转移到同步队列去
    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
            // 拿出条件队列队列头节点的下一个节点
            Node next = first.nextWaiter;
            // 把头节点从条件队列中删除
            first.nextWaiter = null;
            // 头节点转移到同步队列中去
            transferForSignal(first);
            // 开始循环头节点的下一个节点
            first = next;
        } while (first != null);
    }

跟signal的处理方式类似,只不过会唤醒等待队列所有的节点

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值