Java并发编程原理与实战十七:AQS实现重入锁

1、什么是重入锁

    可重入锁就是当前持有锁的线程可以屡次获取该锁,无需等待java

2、什么是AQS

 AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,这个基础框架的重要性能够这么说,JCU包里面几乎全部的有关锁、多线程并发以及线程同步器等重要组件的实现都是基于AQS这个框架。AQS的核心思想是基于volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操做来实现对当前锁的状态进行修改。当state的值为0的时候,标识改Lock不被任何线程所占有。node

3、ReentrantLock锁的架构

 ReentrantLock锁主要包括一个Sync的内部抽象类以及Sync抽象类的两个实现类多线程

                 

 AQS的父类AbstractOwnableSynchronizer(后面简称AOS),AOS主要提供一个exclusiveOwnerThread属性,用于关联当前持有该锁的线程。另外、Sync的两个实现类分别是NonfairSync和FairSync架构

4、AQS的等待队列

假设目前有三个线程Thread一、Thread二、Thread3同时去竞争锁,若是结果是Thread1获取了锁,Thread2和Thread3进入了等待队列,那么他们的样子以下:并发

             

AQS的等待队列基于一个双向链表实现的,HEAD节点不关联线程,后面两个节点分别关联Thread2和Thread3,他们将会按照前后顺序被串联在这个队列上。这个时候若是后面再有线程进来的话将会被当作队列的TAIL。框架

一、入队列分布式

当这三个线程同时去竞争锁的时候发生了什么ide

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

三个线程同时进来,他们会首先会经过CAS去修改state的状态,若是修改为功,那么竞争成功,所以这个时候三个线程只有一个CAS成功,其余两个线程失败,也就是tryAcquire返回false。工具

 

接下来,addWaiter会把将当前线程关联的EXCLUSIVE类型的节点入队列:oop

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

若是队尾节点不为null,则说明队列中已经有线程在等待了,那么直接入队尾。对于咱们举的例子,这边的逻辑应该是走enq,也就是开始队尾是null,其实这个时候整个队列都是null的

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

若是Thread2和Thread3同时进入了enq,同时t==null,则进行CAS操做对队列进行初始化,这个时候只有一个线程可以成功,而后他们继续进入循环,第二次都进入了else代码块,这个时候又要进行CAS操做,将本身放在队尾,所以这个时候又是只有一个线程成功,咱们假设是Thread2成功,哈哈,Thread2开心的返回了,Thread3失落的再进行下一次的循环,最终入队列成功,返回本身。

二、并发问题

基于上面两段代码,他们是如何实现不进行加锁,当有多个线程,或者说不少不少的线程同时执行的时候,怎么能保证最终他们都可以乖乖的入队列而不会出现并发问题的呢?这也是这部分代码的经典之处,多线程竞争,热点、单点在队列尾部,多个线程都经过【CAS+死循环】这个free-lock黄金搭档来对队列进行修改,每次可以保证只有一个成功,若是失败下次重试,若是是N个线程,那么每一个线程最多loop N次,最终都可以成功。

三、挂起等待的线程

节点入队列以后会继续发生什么呢?那就要看看acquireQueued是怎么实现的了,为保证文章整洁,代码我就不贴了,同志们自行查阅,咱们仍是以上面的例子来看看,Thread2和Thread3已经被放入队列了,进入acquireQueued以后:

  1. 对于Thread2来讲,它的prev指向HEAD,所以会首先再尝试获取锁一次,若是失败,则会将HEAD的waitStatus值为SIGNAL,下次循环的时候再去尝试获取锁,若是仍是失败,且这个时候prev节点的waitStatus已是SIGNAL,则这个时候线程会被经过LockSupport挂起。

  2. 对于Thread3来讲,它的prev指向Thread2,所以直接看看Thread2对应的节点的waitStatus是否为SIGNAL,若是不是则将它设置为SIGNAL,再给本身一次去看看本身有没有资格获取锁,若是Thread2仍是挡在前面,且它的waitStatus是SIGNAL,则将本身挂起。

若是Thread1死死的握住锁不放,那么Thread2和Thread3如今的状态就是挂起状态啦,并且HEAD,以及Thread的waitStatus都是SIGNAL,尽管他们在整个过程当中曾经数次去尝试获取锁,可是都失败了,失败了不能死循环呀,因此就被挂起了。当前状态以下:

            

四、锁释放-等待线程唤起    

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先,Thread1会修改AQS的state状态,加入以前是1,则变为0,注意这个时候对于非公平锁来讲是个很好的插入机会,举个例子,若是锁是公平锁,这个时候来了Thread4,那么这个锁将会被Thread4抢去。。。

咱们继续走常规路线来分析,当Thread1修改完状态了,判断队列是否为null,以及队头的waitStatus是否为0,若是waitStatus为0,说明队列无等待线程,按照咱们的例子来讲,队头的waitStatus为SIGNAL=-1,所以这个时候要通知队列的等待线程,能够来拿锁啦,这也是unparkSuccessor作的事情,unparkSuccessor主要作三件事情:

  1. 将队头的waitStatus设置为0.

  2. 经过从队列尾部向队列头部移动,找到最后一个waitStatus<=0的那个节点,也就是离队头最近的没有被cancelled的那个节点,队头这个时候指向这个节点。

  3. 将这个节点唤醒,其实这个时候Thread1已经出队列了。

还记得线程在哪里挂起的么,上面说过了,在acquireQueued里面,我没有贴代码,本身去看哦。这里咱们也大概能理解AQS的这个队列为何叫FIFO队列了,所以每次唤醒仅仅唤醒队头等待线程,让队头等待线程先出。

5、AQS实现一个重入锁

package com;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
 
public class MyLock implements Lock {
    private Helper helper = new Helper();
 
    private class Helper extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            // 第一个线程进来,能够获取锁
            // 第二个线程进来,没法获取锁,返回false
            Thread thread = Thread.currentThread();
            // 判断是否为第一个线程进来
            int state = getState();
            if (state == 0) {
                if (compareAndSetState(0, arg)) {// 若是当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值
                    // 设置当前线程
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            } else if(getExclusiveOwnerThread() == thread) { // 容许重入锁,当前线程和当前保存的线程是同一个线程
                setState(state + 1);
                return true;
            }
            return false;
        }
 
        /***
         * 释放锁
              此方法老是由正在执行释放的线程调用。
         */
        @Override
        protected boolean tryRelease(int arg) {
            // 锁的获取和释放确定是一一对应的,那么调用此方法的线程必定是当前线程
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new RuntimeException();
            }
            
            boolean flag = false;
            int state = getState() -arg;
            if (state == 0) {// 当前锁的状态正确
                setExclusiveOwnerThread(null);
                flag = true;
            }
            setState(state);
            return flag;
        }
 
        protected Condition newCondition() {
            return new ConditionObject();
        }
    }
 
    @Override
    public void lock() {
        // 独占锁
        helper.acquire(1);
    }
 
    @Override
    public void lockInterruptibly() throws InterruptedException {
        // 可中断
        helper.acquireInterruptibly(1);
    }
 
    @Override
    public boolean tryLock() {
        return helper.tryAcquire(1);
    }
 
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return helper.tryAcquireNanos(1, unit.toNanos(time));
    }
 
    @Override
    public void unlock() {
        helper.release(1);
    }
 
    @Override
    public Condition newCondition() {
        return helper.newCondition();
    }
}

6、羊群效应

当有多个线程去竞争同一个锁的时候,假设锁被某个线程占用,那么若是有成千上万个线程在等待锁,有一种作法是同时唤醒这成千上万个线程去去竞争锁,这个时候就发生了羊群效应,海量的竞争必然形成资源的剧增和浪费,所以终究只能有一个线程竞争成功,其余线程仍是要老老实实的回去等待。AQS的FIFO的等待队列给解决在锁竞争方面的羊群效应问题提供了一个思路:保持一个FIFO队列,队列每一个节点只关心其前一个节点的状态,线程唤醒也只唤醒队头等待线程。其实这个思路已经被应用到了分布式锁的实践中,见:Zookeeper分布式锁的改进实现方案。

相关文章
相关标签/搜索