【JUC源码】JAVA并发(三)_AQS独占锁_保姆级源码解析

AbstractQueuedSynchronizer(以下简称AQS)作为java.util.concurrent包的基础,它提供了一套完整的同步编程框架,开发人员只需要实现其中几个简单的方法就能自由的使用诸如独占,共享,条件队列等多种同步模式。我们常用的比如ReentrantLock,CountDownLatch等等基础类库都是基于AQS实现的,足以说明这套框架的强大之处。鉴于此,我们开发人员更应该了解它的实现原理,这样才能在使用过程中得心应手。

总体来说个人感觉AQS的代码非常难懂,本文就其中的独占锁实现原理进行分析。

执行过程概述

获取锁过程:

  1. 首先执行acquire()方法尝试获取锁,如果成功则进入临界区
  2. 如果获取失败进入一个FIFO等待队列,然后被挂起等待唤醒
  3. 如果队列中的等待线程被唤醒会重新尝试获取锁资源,如果成功则进入临界区,否则继续挂起等待

释放锁过程:

  1. 使用release()尝试释放锁,如果没有其他线程在等待则释放完成
  2. 如果有其他线程等待则在释放锁成功后将第一个等待线程唤醒

源码深入分析

基于上面的独占锁释放过程,下面就独占锁实现原理进行分析:

同步队列

同步队列节点源码:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ......
    //AQS通过头尾指针管理同步队列
    private transient volatile Node head;//头节点
    private transient volatile Node tail;//尾节点
    private volatile int state;//同步状态
    ......
    static final class Node {
        //waitStatus的值
        static final int CANCELLED =  1;//节点从同步队列中取消
        static final int SIGNAL    = -1;//后继节点的线程被唤醒,处于等待状态。当前节点释放同步状态后会通知后继节点,使得后继节点线程能够运行
        static final int CONDITION = -2;//当前节点处于阻塞队列中,无法在同步队列中使用,直到调用signal()方法将其转移到同步队列中
        static final int PROPAGATE = -3;//表示下一次共享模式下获取同步状态会无条件持续传播下去
        
        volatile int waitStatus;//节点状态,初始状态为0
        volatile Node prev;//前驱
        volatile Node next;//后继
        volatile Thread thread;//线程引用
        ......
    }
    ......
} 

通过源码,可以知道:

1.同步队列节点的数据结构,节点的状态等信息
2.AQS的同步队列是一个链式双向队列,通过头尾指针来管理同步队列。

该双向队列示意图如下:
在这里插入图片描述

acquire()方法(获取独占锁)

下面先贴上源码

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

由源码可知,acquire()方法它会先调用tryAcquire()尝试获取(独占)锁,若成功则直接返回;如果获取失败,它会再调用acquireQueued()addWaiter()方法将当前线程加入同步队列。

那么它是如何将线程加入同步队列的呢,下面我们研究addWaiter()acquireQueued()方法。

addWaiter()

我们还是直接看addWaiter方法的源码:

//私有方法
private Node addWaiter(Node mode) {
		//使用当前线程构造同步队列节点
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        //如果同步队列尾节点不为空,则将该节点加入同步队列
        if (pred != null) {
            node.prev = pred;
            //这一步,无论有多少个线程尝试将自己变为同步队列尾节点,最终只有一个成功,
            //这一步与enq方法中的相同,只是此处只是尝试一次,而enq方法中是循环执行!
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);//如果尝试失败调用enq方法,enq方法具体实现请往下看
        return node;
    }

这里我们小结一下addWaiter方法中做的事情:

节点进入addWaiter方法后,如果同步队列尾节点不为空(同步队列为空的情况在enq方法里处理),会尝试一次将自己插入队列尾部(此处尝试的意义应该是为了提高性能),若成功则直接返回;失败则进入enq方法。

enq()

还是直接上源码

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //此处处理队列为空的情况。队列如果为空则CAS操作将当前线程节点设置为头结点
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	//在多线程下,此处会有多个新创建的节点指向尾节点
                node.prev = t;
                //队列不为空,将当前线程节点插入队列尾部(与addWaiter方法中的操作完全相同)。
                //如果有多个线程都进行这个操作,最终只有一个线程插入成功,
                //剩余的线程节点进入下一个循环重新"竞争"插入同步队列的机会
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    //该循环体唯一退出循环的操作,否则就要无限重试
                    return t;
                }
            }
        }
    }

此处小结一下enq方法

以下的操作均是在循环体内!
1.处理队列为空的情况。处理操作是新建空节点作为同步队列的头结点
(此处需要注意!初始化队列触发的条件是已经有线程占有了锁资源!所以这个空节点可以看成当前占有锁资源的节点,(虽然它没有任何属性))
2.将当前线程节点插入同步队列,若成功则返回;失败则进入下一个循环

acquiredQueued()

经过上面的操作,节点已经成功插入同步队列,下面节点要做的就是挂起当前线程,等待被唤醒。这个逻辑的实现就在acquiredQueued()方法中,下面我们先看acquiredQueued()源码:

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
                    //锁被成功获取,失败标记位设为false
                    failed = false;
                    //返回中断标记位,表示节点是被正常唤醒还是被中断唤醒
                    return interrupted;
                }
                //如果获取锁失败,则进入挂起逻辑
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        //最后分析获取锁失败处理逻辑
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire()


//此处先拿出waitStatus的几个取值方便下面查看
//    static final int CANCELLED =  1;//节点从同步队列中取消
//    static final int SIGNAL    = -1;//后继节点的线程被唤醒,处于等待状态。当前节点释放同步状态后会通知后继节点,使得后继节点线程能够运行
//    static final int CONDITION = -2;//当前节点处于阻塞队列中,无法在同步队列中使用,直到调用signal()方法将其转移到同步队列中
//    static final int PROPAGATE = -3;//表示下一次共享模式下获取同步状态会无条件持续传播下去


//node是当前线程节点,pred是它的前驱节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//获取前置节点的waitStatus
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
        	//如果前置节点的waitSatus是Node.SIGNAL则返回true,然后执行parkAndCheckInterrupt()方法进行挂起
            return true;
        if (ws > 0) {
        	//waitStatus大于0代表节点被取消
            do {
            	//这里我们从当前节点的前置节点开始,一直向前面找第一个没有被取消的节点
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //注意:由于head是由new Node()创建,waitStatus为0,所以最多查到head,不会存在空指针的问题
            pred.next = node;
        } else {
        	//根据waitStatus的取值我们可知,此处waitStatus只能取得0或者Node.PROPAGATE,
        	//我们将前置节点的waitStatus设为Node.SIGNAL然后重新进入方法进行判断
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

上面这个方法逻辑比较复杂,它是用来判断当前节点是否可以被挂起,也就是唤醒条件是否已经具备,即如果挂起了,那一定是可以由其他线程来唤醒的。该方法如果返回false,即挂起条件没有完备,那就会重新执行acquireQueued方法的循环体,进行重新判断,如果返回true,那就表示万事俱备,可以挂起了,就会进入parkAndCheckInterrupt()方法看下源码:

parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        //被唤醒之后,返回中断标记,即如果是正常唤醒则返回false,如果是由于中断醒来,就返回true
        return Thread.interrupted();
    }

注意:Thread的interrupted方法在返回标记的时候会清除标记,也就是说如果中断醒来获得锁成功,则会返回true;如果中断醒来没有成功获得锁,则继续挂起,此时中断标记也会被清除,下一次醒来就是正常醒来的,acquireQueued方法只会返回false,表示是正常醒来的。

cancelAcquire()

最后我们看到finally模块,这里是对锁资源获取失败后进行一些善后工作,从acquireQueued()方法中我们可以看到只有当tryAcquire方法抛出异常时才会进入此方法,也就是说AQS框架针对开发人员自己实现的获取锁操作如果抛出异常,也做了妥善的处理。下面看源码:

private void cancelAcquire(Node node) {
        // 如果node不存在就忽略
        if (node == null)
            return;
        node.thread = null;
        // 跳过已经被取消的前驱节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        
        //得到前置节点的后继节点,由于前面的跳节点操作,此时的前置节点的后继节点不一定是node了,此处可以好好想一想
        Node predNext = pred.next;
        //将当前节点的状态设置为CANCELLED,这样别的节点在处理时就会忽略它
        node.waitStatus = Node.CANCELLED;

        // 如果当前节点是尾节点,则直接删除
        // 此处不用CAS失败的问题,因为无论CAS是否成功,当前节点都已经被成功删除
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                //这里的逻辑比较绕,大体意思是:如果当前节点的前置节点不是头结点 且 它后面的节点等待着它唤醒(waitStatus<0) 
                //且 它后面的节点没有被取消,则将它的前置和后继节点相连,相当于删除了当前节点
                if (next != null && next.waitStatus <= 0)
                
                    compareAndSetNext(pred, predNext, next);
            } else {
            //进入到这里,要么当前节点的前置节点是头结点,要么前置节点的waitStatus是PROPAGATE,直接唤醒当前节点的后置节点
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

上面就是独占模式获取锁的核心源码,容我缓一缓…

在这里插入图片描述

release()方法

下面看释放锁的过程,还是直接上源码:

public final boolean release(int arg) {
		//尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            //如果头结点不为空且waitStatus不等于0,则释放头结点,返回true
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        //释放锁失败返回false
        return false;
    }
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
        //把标记设为0,表示唤醒操作已经开始,提高并发环境下性能
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            //如果后继节点为空或者已经被取消
            //则从尾部向前查找离node最近的需要唤醒的节点,注意!此处循环体内没有break,所以会一直向前找!
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	//执行唤醒操作
            LockSupport.unpark(s.thread);
    }

这里给出一张从其他博客上看到的acquire方法的执行流程图:
在这里插入图片描述

总结

以上就是AQS独占锁的获取与释放过程,大致思想很简单,就是尝试去获取锁,如果失败就加入一个队列中挂起。释放锁时,如果队列中有等待的线程就进行唤醒。但是看源码,会发现细节好多好多。。以上是我结合自己的理解加上他人博客进行的一次总结,希望能对大家有所帮助,有错误之处还请各位指正。
觉得有用可以点个赞。^ _ ^

参考资料

1.深入浅出AQS之独占锁

2.github:Java-concurrency-master

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值