JUC并发编程基础AQS

JUC并发编程基础AQS

程序员勾三(J3)

个人简历:j3code.cn

1、Lock使用案例

前提:改源码环境为JDK11

lock.lock();
try {
    if (getNumber() > 0) {
        this.number--;
        System.out.println(Thread.currentThread().getName() + ",买到一张车票,当前还剩车票数量:" + getNumber());
    }
} finally {
    //资源关闭
    lock.unlock();
}

2、分析锁的创建流程

Lock lock = new ReentrantLock();

ReentrantLock创建

private final Sync sync;

public ReentrantLock() {
    sync = new NonfairSync();
}

Sync同步对象

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
    // ...
}

Sync同步对象子类

NonfairSync非公平锁

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    // ...
}

FairSync公平锁

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    // ...
}

2.1 图解 ReentrantLock 类继承关系图

在这里插入图片描述

3、分析加锁流程

关键代码

lock.lock();

1、java.util.concurrent.locks.ReentrantLock#lock

public void lock() {
    // 通过 Sync 对象去获得一个资源
    sync.acquire(1);
}

2、java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

3.1 tryAcquire

尝试获取资源(锁)

1)java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

2)java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
    // 获得当前线程
    final Thread current = Thread.currentThread();
    // 拿到锁的 state(这是没一把锁都默认拥有的,默认值 0),
    int c = getState();
    if (c == 0) {
        // acquires = 1,State 修改成功
        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;
}

3.2 tryAcquire流程总结

  1. 获取当前线程
  2. 获取当前锁的状态:state,默认为 0
  3. 判断是否被加锁,就是 state 不为 0 的时候
    • 第一种:没有加锁,调用这个方法 compareAndSetState,修改 state 的值将 0 变为 1,接着讲属性 exclusiveOwnerThread 的值赋为当前线程,至此当前线程加锁成功!
    • 第二种:加锁,判断加锁的线程是否为当前线程,如果是则表示重入,将 state 的值累加就行,否则就是加锁失败

3.3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

3.2.1 addWaiter(Node.EXCLUSIVE)

1)java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter

private Node addWaiter(Node mode) {
    // 创建node节点
    Node node = new Node(mode);

    for (;;) {
        // 将尾指针赋值,尾指针默认null
        Node oldTail = tail;
        if (oldTail != null) {
            // 将 node 的前指针指向 oldTail
            node.setPrevRelaxed(oldTail);
            // 设置尾指针,指向 node
            if (compareAndSetTail(oldTail, node)) {
                // 将 oldTail 的后指针指向 node
                oldTail.next = node;
                // 返回 node
                return node;
            }
        } else {
            // 第一次尾指针为null,初始化同步队列
            initializeSyncQueue();
        }
    }
}


private final void initializeSyncQueue() {
    Node h;
    // 初始化头节点
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        // 初始化尾节点
        tail = h;
}

3.2.2 acquireQueued(Node, arg))

1)java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued

final boolean acquireQueued(final Node node, int arg) {
    // 响应中断标志位,默认false
    boolean interrupted = false;
    try {
        for (;;) {
            // 获取 node 的前指针对象
            final Node p = node.predecessor();
            
            // 判断 node 的前指针是否为头指针,如果是,再次尝试获取锁资源
            // 注意,,,,,,只有当阻塞队列仅有一个节点的时候才会去尝试获取资源
            if (p == head && tryAcquire(arg)) {
                // 获取到资源了,将头尾指针的地址重新指向 new Node()
                setHead(node);
                // 将无用节点置空,方便垃圾回收
                p.next = null; // help GC
                // 返回中断标志位
                return interrupted;
            }
            // 未获取到锁资源,则应该阻塞,之后判断响应中断
            if (shouldParkAfterFailedAcquire(p, node))
                // 阻塞当前线程,并且获取中断响应标志
                // 如果 false 与 false = false 如果 false 与 true = true
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前指针节点的状态 waitStatus 的值
    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 {
            // 将同步队列中节点大于 1 的 node 给踢出同步队列(waitStatus = 1 表示该节点无效)
            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.
             */
        // 将 pred 节点中 waitStatus 的值由 ws = 0 改为 Node.SIGNAL = -1
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}


// 阻塞,并检查中断
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

3.2.3 总结acquireQueued方法

1、封装当前获取锁的线程,封装成一个 Node 节点

2、将封装的节点,加入到同步队列,并且分以下几个情况:

  • 第一种:同步队列为空,那么 head、tail 指针同时指向该 Node ,并且当前线程会再次去尝试获取锁资源,如果获取到锁,那么同步队列将再次重置未一个空队列。
  • 第二种:同步队列不空,那么该线程则没有再一次获取锁资源的机会,进入下一个流程

3、自旋及修改该节点的前一个节点的 waitStatus 值为 -1 表示该节点可以被唤醒获取资源,然后将当前线程阻塞,等待下一次被唤醒。

4、在将当前节点置为 -1 的过程中,如果前一个Node 的 waitStatus > 0 那么会触发踢出同步队列中 Node 节点waitStatus = 1 的无效节点。

3.4 图解加锁流程

在这里插入图片描述

4、释放锁流程

代码切入点

lock.unlock()

1、java.util.concurrent.locks.ReentrantLock#unlock

public void unlock() {
    // 释放资源 1 次
    sync.release(1);
}

2、java.util.concurrent.locks.AbstractQueuedSynchronizer#release

public final boolean release(int arg) {
    // 尝试释放资源
    if (tryRelease(arg)) {
        // 拿到同步队列头节点
        Node h = head;
        // 判断同步队列不为空,并且同步队列中头节点指向的 Node 节点中 waitStatus 值不为 0 才回去唤醒
        if (h != null && h.waitStatus != 0)
            // 真正去唤醒代码
            unparkSuccessor(h);
        return true;
    }
    return false;
}

3、java.util.concurrent.locks.ReentrantLock.Sync#tryRelease

protected final boolean tryRelease(int releases) {
    // 获取锁的 state ,并减去要释放锁的次数 releases
    int c = getState() - releases;
    // 判断当前获取锁的线程是否为当前要释放锁的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        // 两者不一致,报错
        throw new IllegalMonitorStateException();
    // 两者一致
    boolean free = false;
    // 判断释放锁之后,判断 state 是否为 0 无所状态,这是判断重入
    if (c == 0) {
        // state = 0 ,彻底释放锁
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 重入,那么仍然占有锁资源,并且将状态值再次设置给 state
    setState(c);
    // 返回 false(重入)
    return free;
}

4、java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor

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)
        node.compareAndSetWaitStatus(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) {
        // 下面这部分逻辑就是找到要唤醒的线程,我这里可以理解为就是离 head 节点最近的一个 waitStatis 不大于 0 的节点
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        // 唤醒
        LockSupport.unpark(s.thread);
}

唤醒之后,会来到下面这段代码,继续执行:

// 阻塞,并检查中断
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    // 唤醒之后,会继续下面的代码流程
    return Thread.interrupted();
}

4.1 总结

  1. 先尝试去释放锁一次,这里考虑了重入。
  2. 如果释放锁一次之后,state = 0 ,那么将占有锁的线程置为 null,反之则只改变 state 的值(-1)。
  3. 如果释放锁成功,那么继续判断同步队列是否有等待的 Node 节点线程,如果没有则直接返回,反之则有其他线程被阻塞在同步队列中,需要唤醒。
  4. 找到要唤醒的线程节点,我这里可以理解为就是离 head 节点最近的一个 waitStatis 不大于 0 的节点,并且触发提出节点状态(waitStatis)的值大于 0 的节点,因为它是无效的,所以要踢出掉
  5. 被唤醒的线程会在 parkAndCheckInterrupt() 方法中醒来,并继续抢占锁资源

4.2 图解释放锁流程

在这里插入图片描述

5、锁的阻塞使用案例

Condition 使用案例

public class ConditionDemo {

    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();

    public static void main(String[] args) throws Exception {
        ConditionDemo conditionDemo = new ConditionDemo();
        System.out.println(Thread.currentThread().getName() + "开始执行....");

        new Thread(() -> {
            conditionDemo.lock.lock();
            try {
                try {
                    System.out.println(Thread.currentThread().getName() + "我要开始等待了.....");
                    conditionDemo.conditionA.await();
                    System.out.println(Thread.currentThread().getName() + "我等待结束了.....");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                //资源关闭
                conditionDemo.lock.unlock();
            }

        }, "J3,快来关注我-").start();


        Thread.sleep(2 * 1000);

        System.out.println(Thread.currentThread().getName() + "主线程要开始唤醒其他线程了.....");
        conditionDemo.lock.lock();
        try {
            conditionDemo.conditionA.signal();
        }finally {
            //资源关闭
            conditionDemo.lock.unlock();
        }

        Thread.sleep(2 * 1000);
        System.out.println(Thread.currentThread().getName() + "主线程结束.....");
    }
}

6、锁的条件等待流程

代码

conditionDemo.conditionA.await();

1、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#await()

public final void await() throws InterruptedException {
    // 判断中断标志位,响应中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 生成条件等待队列的节点,并且初始化条件队列,并将节点放入条件队列
    Node node = addConditionWaiter();
    // 完全释放锁,为什么说是完全,因为有重入的可能,所以,你释放了多少,在下一次拿到CPU资源的时候,就要还原到释放前的加锁次数
    int savedState = fullyRelease(node);
    // 中断响应次数
    int interruptMode = 0;
    // 循环判断,node 节点是否在 同步队列里面
    while (!isOnSyncQueue(node)) {
        // 不在同步队列里面,那么将该线程阻塞
        LockSupport.park(this);
        // 被唤醒之后(被唤醒的节点,是会别调用唤醒逻辑的线程放入同步队列中),判断是否响应中断
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 这里是说明这个线程已经在同步队列里面了,那么就只要按部就班的执行获取锁的流程即可(注意,这里的那拿锁次数是,savedState)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 去除条件队列中,waitStatus = -2 的节点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 响应中断
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

2、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#addConditionWaiter

private Node addConditionWaiter() {
    // 判断是否是持有锁的线程过来生成条件队列中的节点
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取条件队列中的尾节点
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    // 这里最先触发一次清除条件队列中,waitStatus = -2 的节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        // 找到最后一个 waitStatus 不为 -2 的尾节点
        t = lastWaiter;
    }

    // 生成条件队列中的节点
    Node node = new Node(Node.CONDITION);

    // 将节点放入条件队列中,并将指向关系给生成好
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    // 返回条件队列中的节点
    return node;
}

6.1 总结

1、判断是否响应中断

2、生成条件等待队列的节点,并且初始化条件队列,并将节点放入条件队列

3、完全释放锁,为什么说是完全,因为有重入的可能,所以,你释放了多少,在下一次拿到CPU资源的时候,就要还原到释放前的加锁次数

4、循环判断节点是否在同步队列中,如果在,则跳过阻塞,直接去竞争锁资源,反之阻塞,等待唤醒。

5、如果唤醒,则跳出循环,竞争锁资源,如果争抢到锁资源后,会移除条件队列中Node 属性 waitStatus = -2 的节点,最后才是完成一次 await 流程。

6.2 图解锁阻塞等待流程

在这里插入图片描述

7、锁的唤醒流程

代码

conditionDemo.conditionA.signal()

1、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#signal

public final void signal() {
    // 判断执行唤醒逻辑的线程是否时当前持有锁的线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 先拿到条件队列中头指针
    Node first = firstWaiter;
    // 条件队列不为空,则去执行唤醒的动作
    if (first != null)
        // 开始去唤醒线程
        doSignal(first);
}

2、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#doSignal

private void doSignal(Node first) {
    do {
        // 如果条件队列中的节点只有一个,那么会复位条件队列中的头尾指针
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // 转移唤醒
    } while (!transferForSignal(first) &&
             // 转移唤醒失败,继续转移唤醒下一个节点,知道同步队列末尾
             (first = firstWaiter) != null);
}

3、java.util.concurrent.locks.AbstractQueuedSynchronizer#transferForSignal

final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    // 设置 Node 节点状态,从条件队列中移到同步队列中,肯定是要把条件队列中的标识给移除掉
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
        return false;

    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    // 将 node 节点放入到同步队列尾部,然后返回同步队列中倒数第二个节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // ws = 0,然后将 同步队列中倒数第二个节点的状态由 0 改为 -1
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
        // 如果修改失败,那么就把阻塞的线程唤醒,让它去争取锁,顺便检查一下同步队列中的各个节点的状态是否合理
        LockSupport.unpark(node.thread);
    // 唤醒成功!
    return true;
}

7.1 总结

1、先判断执行唤醒逻辑的线程是否时当前持有锁的线程

2、判断条件队列不为空,则去执行唤醒的动作

3、真正转移唤醒

  • 设置 Node 节点状态,从条件队列中移到同步队列中,肯定是要把条件队列中的标识给移除掉
  • 将 node 节点放入到同步队列尾部,然后返回同步队列中倒数第二个节点
  • 然后将同步队列中倒数第二个节点的状态由 0 改为 -1
    • 第一种修改失败,则唤醒等待的线程,去竞争锁资源
    • 第二种修改成功,则代表唤醒线程成功,结束流程

7.2 图解锁唤醒流程

在这里插入图片描述

8、AQS总图

本片文章中所涉及的所有流程图可访问下面地址获取

链接:https://gitmind.cn/app/flowchart/95a4909721
密码:6284

好了,今天的内容到这里就结束了,我是 【J3】关注我,我们下期见


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J3code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值