从源码入手深入分析CAS以及AQS

1.CAS(Compare-and-Swap)

CAS在执行更新操作的时候会进行一次比对,如果要修改的值不是他认为的值,那么这次更新操作就不会完成。多个线程进行CAS操作,只有一个线程能成功,失败的线程可以多次尝试。

以AtomicInteger为例

java 复制代码
public class AtomicInteger extends Number implements java.io.Serializable {
  //唯一ID用来验证序列化是否成功
    private static final long serialVersionUID = 6214790243416807050L;
​
    // 申请使用unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //value的偏移地址
    private static final long valueOffset;
​
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }
  //保证可见性 必须要让其他线程马上看到这个value 
    private volatile int value;
​
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
  
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
      //不断调用CAS方法来尝试获取并更新值
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }
  //使用unsafe类调用操作系统底层CAS方法
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
}
  • ABA问题

线程A想要更新Value值,线程B先改动了Value值然后又更改回来了,这时候线程A使用CAS仍然会成功,ABA问题会导致丢失掉Value变动的这个状态,如果必须要避免这个问题,可以使用乐观锁版本号机制,对每次变动的数据加上一个version,也可以使用传统的互斥同步锁。

2.AQS(AbstractQueuedSynchronizer)

AQS支持独占锁(Exclusive)和共享锁(Share) 两种模式:

  • 独占锁:也叫互斥锁、排它锁,只能被一个线程获取到(如ReentrantLockReadWriteLock的写锁);
  • 共享锁:可以被多个线程同时获取(如CountDownLatchReadWriteLock的读锁)。

不管是独占锁还是共享锁,本质上都是对AQS内部的一个变量state的获取,state是一个原子性的int变量,可用来表示锁状态、资源数等,如下图。

arduino 复制代码
    /**
     * The synchronization state.
     */
    private volatile int state;

AQS的内部实现了两个队列:同步队列和条件队列

  • 同步队列 :在线程尝试获取资源失败后,会进入同步队列队尾,给前继节点设置一个唤醒信号后,自身进入等待状态(通过LockSupport.park(this)),直到被前继节点唤醒。
  • 条件队列 :是为Condition实现的一个同步器,一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。需要注意的是,如果一个线程被唤醒(condition.signal())后,它会从条件队列转移到同步队列来等待获取锁。

独占

tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

共享

tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。

isHeldExclusively():该线程是否正在独占资源(是否获取到锁)。只有用到Condition才需要实现。

3 基础同步器

acquire(int):独占模式下获取锁/资源(写锁lock.lock()内部实现)

release(int):独占模式下释放锁/资源(写锁lock.unlock()内部实现)

acquireShared(int):共享模式下获取锁/资源(读锁lock.lock()内部实现)

releaseShared(int):共享模式下释放锁/资源(读锁lock.unlock()内部实现)

以下叙述 资源等于锁

3.2 acquire()

3.2.1尝试获取资源

相比于自定以实现同步器少了try

scss 复制代码
//独占模式获取资源
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

简述流程,首先尝试获取锁,获取失败后就会加入同步队列,并且将这个Node标记为独占模式,如果在等待获取资源过程中产生中断,那么会在获取资源后补充上这一个中断状态。就可以根据这个中断状态,对线程进行下一步的操作。

如果没有获取到资源,节点会执行以下方法

3.2.2获取资源失败
arduino 复制代码
//获取资源失败后,检查并更新等待状态,如果线程需要阻塞返回true
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) {
        //如果前节点为CANCELLED状态,那就一直往前找到一个等待状态的节点,并排在它的后边
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 此时前继节点状态为0或PROPAGATE,说明正在等待获取锁/资源,
        // 此时需要给前继节点设置一个唤醒信号SIGNAL,但不直接阻塞,
        // 因为在阻塞前调用者需要重试来确认它确实不能获取资源。
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}
//阻塞当前线程,清除并返回中断状态
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

寻找可安全阻塞的前继节点 如果前节点是SIGNAL大于0也就是CANCLELLED的状态,那么会继续往前找,并且给前节点一个SIGNAL标志。

3.2.3 等待中发生中断

如果在这个过程中发生错误中断,就会调用cancleAcquire方法,将自己的节点退出同步队列,如果前置节点是head,那么就会调用unparkSuccessor方法唤醒指向自己的后置节点。

3.3 release()

java 复制代码
/**独占模式释放锁/资源*/
public final boolean release(int arg) {
    if (tryRelease(arg)) {//尝试释放资源
        Node h = head;//头结点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒head的下一个节点
        return true;
    }
    return false;
}

尝试释放资源,调用tryRelease,直接用state减去arg,如果state == 0 说明资源成功释放(参考可重入锁ReentryLock实现 成功释放资源后会调用unparkSuccessor()唤醒下一个节点

ini 复制代码
//唤醒给定节点的后继节点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 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);
}
3.3.1 acquireShared(int)

与独占模式几乎一样。区别就是在获取到资源后,会检查是否还有剩余资源 以唤醒后续线程

scss 复制代码
//获取共享锁
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//添加一个共享模式Node到队列尾
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();//获取前节点
            if (p == head) {
                int r = tryAcquireShared(arg);//前继节点为head,尝试直接获取资源
                if (r >= 0) {
                  //这里比较关键 获取资源成功后会检查是否还有剩余资源 
                    //获取资源成功,设置head为自己,如果有剩余资源继续唤醒之后的线程 
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node))//检查获取失败后是否可以阻塞
                interrupted |= parkAndCheckInterrupt();//阻塞当前线程,清除并返回中断状态
        }
    } catch (Throwable t) {
        cancelAcquire(node);//取消正在等待的节点操作
        throw t;
    } finally {
        if (interrupted)//如果期间线程被中断过,补上中断
            selfInterrupt();
    }
}
3.3.2 releaseShared(int)
arduino 复制代码
/**共享模式下释放给定资源数*/
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();//释放资源,并唤醒后继节点
        return true;
    }
    return false;
}

4 条件同步器

本节分析AQS内部对Condition的实现-ConditionObject

4.1 await()

scss 复制代码
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //添加新节点(当前线程)到条件队列
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);//释放锁并返回释放前锁状态
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {//当前节点是否在同步队列中
        LockSupport.park(this);
        //检查等待期间是否被中断过
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();//清除取消等待的节点
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);//报告中断状态,抛异常或补中断状态
}

如果要使用条件队列 就需要实现AQS的isHeldExclusively()方法用来判断该线程是否持有锁。如果不持有就会抛出IllegalMonitorStateException

4.2 signal()

唤醒条件队列对前面的节点,也就是等待时间最长的节点

java 复制代码
//移除等待时间最长的节点(firstWaiter)
public final void signal() {
    if (!isHeldExclusively())//检查是否持有锁
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;//获取条件队列的首个节点
    if (first != null)
        doSignal(first);
}
typescript 复制代码
//从条件队列唤醒节点线程
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)//先更新firstWaiter
            lastWaiter = null; //已经没有等待节点了
        first.nextWaiter = null;//解除当前节点的链接
    } while (!transferForSignal(first) && //把当前节点转移到等待队列等待获取锁
             (first = firstWaiter) != null);
}

也就是会把这个节点转移到同步队列进行等待

相关推荐
无尽的大道3 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
小鑫记得努力12 分钟前
Java类和对象(下篇)
java
binishuaio16 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE18 分钟前
【Java SE】StringBuffer
java·开发语言
老友@18 分钟前
aspose如何获取PPT放映页“切换”的“持续时间”值
java·powerpoint·aspose
wrx繁星点点33 分钟前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
Upaaui36 分钟前
Aop+自定义注解实现数据字典映射
java
zzzgd81636 分钟前
easyexcel实现自定义的策略类, 最后追加错误提示列, 自适应列宽,自动合并重复单元格, 美化表头
java·excel·表格·easyexcel·导入导出
友善的鸡蛋37 分钟前
解决:使用EasyExcel导入Excel模板时出现数据导入不进去的问题
java·easyexcel·excel导入
星沁城37 分钟前
240. 搜索二维矩阵 II
java·线性代数·算法·leetcode·矩阵