AQS锁实现源码解析

以 ReentrantLock 为例,主要讲解ReentrantLock 中公平锁的实现步骤(FairSync):

ReentrantLock 继承Lock类,并提供了加锁(Lock)和解锁(UnLock)两个方法,但是方法内是调用了Snyc类的方法实现,而Snyc类其实是继承了AbstractQueuedSynchronizer这个类 也就是AQS实现的,所以最终 ReentrantLock 底层还是由AQS去实现的。

//实现Lock接口 
public class ReentrantLock implements Lock, java.io.Serializable {
 private static final long serialVersionUID = 7373984872572414699L;
 /** 提供所有实施机制的同步器 内部类*/
 private final Sync sync;
 /** 抽象类Sync继承了AQS */
 abstract static class Sync extends AbstractQueuedSynchronizer {
     private static final long serialVersionUID = -5179523762034025860L;
​
     /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();
//ReentrantLock类中加锁的方法 是调用sync实现 而Sync抽象类继承AQS 最终逻辑还是有AQS完成
public void lock() {
        sync.lock();
    }

首先得先看看AQS内部阻塞队列的数据结构:请看下图:

aqs-0

1.ReentrantLock 在内部用了内部类 Sync 来管理锁,所以真正的获取锁和释放锁是由 Sync 的实现类来控制的。

abstract static class Sync extends AbstractQueuedSynchronizer {
}

2.Sync 有两个实现,分别为 NonfairSync(非公平锁)和 FairSync(公平锁),我们看 FairSync 部分。

注意:初始化ReentrantLock对象 不传参数 默认是非公平锁

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

3.由乐观锁进来 最主要,最核心的几个方法 这块理解了 也就差不多理解他的逻辑了

    /**
   以独占模式获取,忽略中断。通过至少调用一次tryAcquire来实现,成功后返回。否则,线程将排队,可能会重复阻塞和    取消阻塞,调用tryAcquire直到成功。此方法可用于实现方法Lock.Lock。
    */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

        
        //第一步 tryAcquire方法  先尝试获取锁 
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //1.获取当前锁状态是否是无锁空闲状态
            if (c == 0) {
        //2.是无锁状态 则再判断队列中是否有排队的线程 判断自己是不是排在头节点的下一个或者当前线程不是自己
                if (!hasQueuedPredecessors() &&
                    //3.执行CAS 获取锁 并修改锁状态
                    compareAndSetState(0, acquires)) {
                    //4.标记一下 把自己设置为锁的线程持有者
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //走到这里 就说明进来时第一步获取锁没有成功
            //1.还是先判断下当前锁的持有者是不是自己 如果是自己持有 则就是重入 把当前线程state值加1
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //都没有返回  那说明真的获取失败 返回false
            return false;
        }
//如果前面没有获取成功 继续回到最外层的方法中 继续往下走
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

------------说明 tryAcquire返回false 取反满足条件 继续往下走 进入acquireQueued方法中-------------

先看下addWaiter方法:

//首先进来 是给定了一个独占模式的Node节点    
private Node addWaiter(Node mode) {
    
        //将当前线程包装成一个Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //获取尾节点
        Node pred = tail;
        if (pred != null) {
            //如果尾节点不为空 设置当前线程节点的前节点为尾节点
            node.prev = pred;
            //CAS设置当前节点为尾节点 
            // 上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                //线程入队 可以返回
                return node;
            }
        }
        //走到这里 说明尾节点为null 或者有其他线程竞争抢到了锁
        enq(node);
        return node;
    }
    // 采用自旋的方式入队
    // 之前说过,到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,
    // 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 之前说过,队列为空也会进来这里
            if (t == null) { // Must initialize
                // 初始化head节点
                // 细心的读者会知道原来 head 和 tail 初始化的时候都是 null 的
                // 还是一步CAS,你懂的,现在可能是很多线程同时进来呢
                if (compareAndSetHead(new Node()))
                    // 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
​
                    // 这个时候有了head,但是tail还是null,设置一下,
                    // 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
                    // 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return
                    // 所以,设置完了以后,继续for循环,下次就到下面的else分支了
                    tail = head;
            } else {
                // 下面几行,和上一个方法 addWaiter 是一样的,
                // 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

----ok 现在已经将当前线程节点加入队列 继续往下走 看看队列里面该如何操作呢?

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //无限循环
            for (;;) {
                //拿到当前节点的前节点
                final Node p = node.predecessor();
                //如果前节点是头节点 那就去重走一遍 tryAcquire 方法获取锁
                if (p == head && tryAcquire(arg)) {
                    //获取锁成功 设置头节点为当前线程节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //走到这 说明前一个节点不是头节点或者有其他线程抢占了 看*shouldParkAfterFailedAcquire*方法
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

继续往下走 到shouldParkAfterFailedAcquire这个方法。。。。。看名字即可猜出来 应该挂起获取失败的节点

注意 这里需要科普下waitStatus这个值的一些枚举含义:

在AQS中waitstatus有五种值:
​
1、INITAL 值为0,表示当前没有线程获取锁(初始状态)。
​
2、SIGNAL 值为-1,后继节点的线程处于等待的状态,当前节点的线程如果释放了同步状态或者被取消会通知后继节点,后继节点会获取锁并执行。
(当一个节点的状态为SIGNAL时就意味着在等待获取同步状态,前节点是头节点也就是获取同步状态的节点)
​
3、CANCELLED 值为1,因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。
处于这种状态的结点会被踢出队列,被GC回收。
(一旦节点状态值为1说明被取消,那么这个节点会从同步队列中删除)
​
4、CONDITION 值为-2,节点在等待队列中,节点线程等待在Condition,当其它线程对Condition调用了singal()方法该节点会从等待队列中移到同步队列中。
​
5、PROPAGATE 值为-3,表示下一次共享式同步状态获取将会被无条件的被传播下去。(读写锁中存在的状态,代表后续还有资源,可以多个线程同时拥有同步状态)
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
        if (ws == Node.SIGNAL)
            return true;
        //大于0 就说明当前节点处于取消状态 该状态下的节点不可以竞争锁 只能等待被GC回收
        if (ws > 0) {
            //这里的代码意思就是不断向前获取 直到获取到一个waitStatus<=0的结点 然后next指向当前节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 仔细想想,如果进入到这个分支意味着什么
            // 前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
            // 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0
            // 正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0
            // 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 这个方法返回 false,那么会再走一次 for 循序,
        // 然后再次进来此方法,此时会从第一个分支返回 true
        return false;
    }
​
    // private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
    // 这个方法结束根据返回值我们简单分析下:
    // 如果返回true, 说明前驱节点的waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤醒
    //        我们也说过,以后是被前驱节点唤醒,就等着前驱节点拿到锁,然后释放锁的时候叫你好了
    // 如果返回false, 说明当前不需要被挂起,为什么呢?往后看
​
    // 跳回到前面是这个方法
    // if (shouldParkAfterFailedAcquire(p, node) &&
    //                parkAndCheckInterrupt())
    //                interrupted = true;
​
    // 1. 如果shouldParkAfterFailedAcquire(p, node)返回true,
    // 那么需要执行parkAndCheckInterrupt():
​
    // 这个方法很简单,因为前面返回true,所以需要挂起线程,这个方法就是负责挂起线程的
    // 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒=======
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
​
​
    // 2. 接下来说说如果shouldParkAfterFailedAcquire(p, node)返回false的情况
​
   // 仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。
​
    // 解释下为什么shouldParkAfterFailedAcquire(p, node)返回false的时候不直接挂起线程:
    // => 是为了应对在经过这个方法后,node已经是head的直接后继节点了。剩下的读者自己想想吧。

AQS解锁操作:

首先也是 调用的sync的unlock方法:

//解锁方法
public void unlock() {
        sync.release(1);
    }
​
----------------------------------------------------------
//往里走可以看到
public final boolean release(int arg) {
        //这里如果解锁成功 进入里面
        if (tryRelease(arg)) {
            //拿到头节点 头节点不为空且等待状态不为初始状态(无锁)
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //进入这里
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
----------------------------------------------------------
    // 先进入tryRelease看看
        protected final boolean tryRelease(int releases) {
            //进来先是拿到锁的state值 - 解锁的1,得到c
            int c = getState() - releases;
            //判断当前持有锁的线程是不是与记录的线程是一样的 不一样则抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果c为0了 则说明全部都解锁成功 重入锁也全部释放
            if (c == 0) {
                free = true;
                //设置持有线程为null
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
​
----------------------------------------------------------
    // 进入unparkSuccessor方法内部看下是如何走的
    private void unparkSuccessor(Node node) {
        //这里node是头节点 拿到头节点waitStatus判断是否小于0 
        int ws = node.waitStatus;
        //小于0 则CAS一下 设置其状态为初始状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    
        //拿到头节点的下一个节点 如果是为已取消的节点 则将其踢出队伍
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            //然后从队尾开始往前遍历 直到找到一个不是已取消的且最靠前的节点 然后赋值
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //最后在这里进行唤醒
        if (s != null)
            LockSupport.unpark(s.thread);
    }

AQS总结:

最后 来总结下 :

简单来说,ReentrantLock内部是运用了AQS来实现的 AQS内部是怎么实现的呢?三个东西:1.线程的阻塞和解除阻塞,2.state,3.阻塞队列实现.

线程的阻塞和解除阻塞:AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。

state:锁状态值:0:无锁,其余数字有锁,大于1则说明重入

阻塞队列:里面就是FIFO双向链表,Node节点:{pred:前节点,next:后节点,head:头节点,tail:尾节点,thread:当前节点线程}

首先加锁 就是多条线程同时获取锁资源 第一条线程进去时会先判断队列是否有排队 自己的前置节点是不是头节点 如果是则去CAS一下 如果成功则修改掉锁的state值为1,还有把当前持有线程设置为自己,方便后续重入时可以继续获取,也防止其他线程获取,如果不成功 则会进行入队操作,首先会先判断队尾tail节点是否为空,如果不为空,则把自己放到队尾,让队尾的next指向自己,双向链接。如果队尾不为空或者被别人抢先入队,则会进入循环,直到加入队尾。

加入队列后,会判断自己的前节点是不是头节点,如果是则说明可以抢锁,这时候调用CAS抢锁,如果获取失败,则需要判断前节点的等待状态是不是处于-1 也就是正常状态,如果前节点处于-1(取消状态)则会循环往前找,直到找到一个等待状态为-1(正常状态)的节点,并把其设置为自己的前节点,最后一步就是将其park(挂起),等待被前节点线程唤醒。这就是一套完整的加锁流程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值