AQS与ReentrantLock底层源码分析

文章目录

  • 1、AQS底层原理
  • 2、手写AQS与ReentrantLock
  • 3、ReentrantLock原理概述
  • 4、ReentrantLock源码分析
    • 1、非公平锁底层
      • 1、acquire(1)
      • 2、tryAcquire(1)
      • 3、addWaiter(Node mode)
      • 4、enq(final Node node)
      • 5、acquireQueued()
      • 6、shouldParkAfterFailedAcquire()
      • 7、parkAndCheckInterrupt()
      • 8、release(int arg)
      • 9、tryRelease(int arg)
      • 10、unparkSuccessor()
    • 2、可重入锁底层
    • 3、公平锁和非公平锁的区别
    • 4、公平锁和非公平锁的释放
    • 5、不可中断原理
    • 6、可中断原理
    • 7、条件变量await()原理
      • 1、await()方法
      • 2、addConditionWaiter()方法
      • 3、fullyRelease(node)方法
    • 8、条件变量的single()原理
      • 1、single()方法
      • 2、doSignal(first)方法
      • 3、transferForSignal(Node node)方法

1、AQS底层原理

java并发包下很多API都是基于AQS来实现的,AQS是java并发包的基础类。里面维护者一个同步状态state,和一个同步队列FIFO以及操作state和同步队列的方法。

1、同步状态state:

AbstractQueuedSynchronizer维护了一个state变量,来表示同步器的状态,state可以称为AQS的灵魂,基于AQS实现的好多JUC工具,都是通过操作state来实现的,state=0表示没有任何线程持有锁;state=1表示某一个线程拿到了一次锁,state=n(n > 1),表示这个线程获取了n次这把锁,用来表达所谓的“可重入锁”的概念。

2、FIFO双向队列:

Node结点:作为获取锁失败线程的包装类, 组合了Thread引用, 实现为FIFO双向队列。 下图为Node结点的属性描述:

AQS与ReentrantLock底层源码分析_第1张图片

同步队列的实现原理:

AQS与ReentrantLock底层源码分析_第2张图片

链表初始化的头节点其实是一个虚拟节点,英文名称之为dummy header, 因为它不会像后继节点一样真实的存放线程,并且这个节点只会在真正产生竞争排队情况下才会延迟初始化,避免性能浪费。
AbstractQueuedSynchronizer 类是一个模版类,维护着这个同步队列(双向链表),提供着同步队列一些操作的公共方法,JUC并发包里基于此类实现了很多常用的并发工具类,如 Semaphore, CountDownLatch等。

同步队列结构:
AQS与ReentrantLock底层源码分析_第3张图片

2、手写AQS与ReentrantLock

AQS与ReentrantLock底层源码分析_第4张图片
ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。

把AQS当成一个基础框架,而这个框架提供了一些方法给你,你可以依靠这个框架来实现一个独占锁或者共享锁,然后再看提供出来的方法的功能,进而进行调用即可。

@Slf4j(topic = "c.TestAQS")
public class TestAQS {
    public static void main(String[] args) {
        MyLock lock = new MyLock();
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("locking...");
                sleep(1);
            } finally {
                log.debug("unlocking...");
                lock.unlock();
            }
        },"t1").start();

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("locking...");
            } finally {
                log.debug("unlocking...");
                lock.unlock();
            }
        },"t2").start();
    }
}

class MyLock implements Lock {
    /**
     * 同步器类
     *
     * 在Lock中维护了一个内部类对象MySync extends AbstractQueuedSynchronizer
     * ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。
     * 这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。
     *
     */
    class MySync extends AbstractQueuedSynchronizer{
        /**
         * 尝试获取锁
         */
        protected boolean tryAcquire(int arg){
            //CAS操作将status从0改成1
            if(compareAndSetState(0,1)){
                //加锁,并设置owner为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 尝试释放锁
         */
        protected boolean tryRelease(int arg){
            //将owner线程设置为null
            setExclusiveOwnerThread(null);
            //将同步状态设置为0
            setState(0);
            //因为status是volatile的,这个关键字可以保证在它之前的操作都会同步到主存中对其他线程可见
            //因此把setExclusiveOwnerThread(null)放到 setState(0)前面
            return true;
        }

        /**
         * 是否持有独占锁
         */
        public boolean isHeldExclusively(){
            //如果status的状态为1,说明持有独占锁
            return getState()==1;
        }

        /**
         * 返回一个条件变量
         */
        public Condition newCondition(){
            return new ConditionObject();
        }
    }


    /**
     * 下面的这些抽象方法如果都要自己实现太难了,但是还好有同步器类AQS
     * AQS已经把大部分的方法都实现好了,我们只需要知道具体作用,调用即可
     */

    MySync sync = new MySync();
    //加锁
    @Override
    public void lock() {
        //在外部直接调用同步器类的相关方法
        sync.acquire(1);
    }

    //加锁,可打断
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    //尝试加锁
    @Override
    public boolean tryLock() {
       return sync.tryAcquire(1);
    }

    //尝试加锁,带超时时间
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    //释放锁
    @Override
    public void unlock() {
        //注意tryRelease()不会唤醒等待队列中的线程,但是release()方法会唤醒等待队列中的线程
         sync.release(0);
    }

    //创建条件变量
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

测试结果:

21:29:03.742 c.TestAQS [t1] - locking...
21:29:04.755 c.TestAQS [t1] - unlocking...
21:29:04.755 c.TestAQS [t2] - locking...
21:29:04.756 c.TestAQS [t2] - unlocking...

3、ReentrantLock原理概述

如果有一个线程过来尝试用ReentrantLock的lock()方法进行加锁,会发生什么事情呢?

AQS对象内部有一个核心的变量叫做state,是int类型的并且加了volatile关键字,代表了加锁的状态。初始状态下,这个state的值是0。

AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。

AQS与ReentrantLock底层源码分析_第5张图片

接着线程1跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。

AQS与ReentrantLock底层源码分析_第6张图片
说白了,AQS就是并发包里的一个核心组件,里面有state变量、加锁线程变量等核心的东西,维护了加锁状态。你会发现,ReentrantLock这种东西只是一个外层的API,内核中的锁机制实现都是依赖AQS组件的

可重入加锁其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。

接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢?

线程2跑过来一下看到,state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!

接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,**“加锁线程”**这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。
AQS与ReentrantLock底层源码分析_第7张图片

线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了

AQS与ReentrantLock底层源码分析_第8张图片

接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!
AQS与ReentrantLock底层源码分析_第9张图片

接下来,会从等待队列的队头唤醒线程2重新尝试加锁。

好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。

此外,还要把**“加锁线程”**设置为线程2自己,同时线程2自己就从等待队列中出队了。

AQS与ReentrantLock底层源码分析_第10张图片

AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。它包含了state变量、加锁线程、等待队列等并发中的核心组件。

ReentrantLock就是使用AQS而实现的一把锁,它实现了可重入锁,公平锁和非公平锁。它有一个内部类用作同步器是Sync,Sync是继承了AQS的一个子类,并且公平锁和非公平锁是继承了Sync的两个子类。ReentrantLock的原理是:假设有一个线程A来尝试获取锁,它会先CAS修改state的值,从0修改到1,如果修改成功,那就说明获取锁成功,设置加锁线程为当前线程。如果此时又有一个线程B来尝试获取锁,那么它也会CAS修改state的值,从0修改到1,因为线程A已经修改了state的值,那么线程B就会修改失败,然后他会判断一下加锁线程是否为自己本身线程,如果是自己本身线程的话它就会将state的值直接加1,这是为了实现锁的可重入。如果加锁线程不是当前线程的话,那么就会将它生成一个Node节点,加入到等待队列的队尾,直到什么时候线程A释放了锁它会唤醒等待队列队头的线程。这里还要分为公平锁和非公平锁,默认为非公平锁,公平锁和非公平锁无非就差了一步。如果是公平锁,此时又有外来线程尝试获取锁,它会首先判断一下等待队列是否有第一个节点,如果有第一个节点,就说明等待队列不为空,有等待获取锁的线程,那么它就不会去同步队列中抢占cpu资源。如果是非公平锁的话,它就不会判断等待队列是否有第一个节点,它会直接前往同步对列中去抢占cpu资源。

4、ReentrantLock源码分析

AQS与ReentrantLock底层源码分析_第11张图片

1、非公平锁底层

lock()方法的逻辑: 多个线程调用lock()方法, 如果当前state为0, 说明当前没有线程占有锁, 那么只有一个线程会CAS获得锁, 并设置此线程为独占锁线程。那么其它线程会调用acquire方法来竞争锁(后续会全部加入同步队列中自旋或挂起)。当有其它线程A又进来想要获取锁时, 恰好此前的某一线程恰好释放锁, 那么A会恰好在同步队列中所有等待获取锁的线程之前抢先获取锁。也就是说所有已经在同步队列中的尚未被 取消获取锁 的线程是绝对保证串行获取锁,而其它新来的却可能抢先获取锁。

public class ReentrantLock implements Lock, java.io.Serializable {
    
    private final Sync sync;
	
    abstract static class Sync extends AbstractQueuedSynchronizer {
    	//......
    }
	
   	//非公平锁继承自Sync,Sync继承自AQS,在AQS中定义了加锁释放锁,同步状态,同步队列等
    static final class NonfairSync extends Sync {
       	//加锁
        final void lock() {
            //利用CAS将status=0,改成status=1
            if (compareAndSetState(0, 1))
                //如果CAS成功,就会将Owner线程指向当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //如果CAS失败,代表出现竞争,就会进入这个方法
                acquire(1);
        }
		
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
}

在没有竞争时:线程Thread-0通过CAS将同步状态从0设置为1,如果成功,就会将加锁线程设置为当前线程

AQS与ReentrantLock底层源码分析_第12张图片

当第一个竞争出现时:

AQS与ReentrantLock底层源码分析_第13张图片

1、acquire(1)

线程Thread-1同样想通过CAS操作将status从0修改为1,但是此时CAS失败,就会进入acquire(1)方法:

//如果出现竞争,就会进入 acquire(1)方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

2、tryAcquire(1)

由于Thread-1执行tryAcquire(1)方法一定会返回false(因为CAS会失败)

//尝试获取锁,这个AQS中的方法,内部逻辑需要由子类去实现
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

tryAcquire方法仍然尝试获取锁(快速获取锁机制),成功返回false,如果没有成功, 那么就将此线程包装成Node加入同步队列尾部。。Node.EXCLUSIVE 为null表示这是独占锁,如果为读写锁,那就是 共享模式(shared)。

3、addWaiter(Node mode)

!tryAcquire(arg)=true,接着就会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法:

//创建一个节点对象,并把它加入到等待队列的队尾
private Node addWaiter(Node mode) {
    //创建一个节点对象node
    Node node = new Node(Thread.currentThread(), mode);
    //指向tail节点的同步队列的尾节点
    Node pred = tail;
    //如果尾节点不为null,说明同步队列已经初始化
    if (pred != null) {
        //将节点对象node的prev指向pred(同步队列队尾的节点)
        node.prev = pred;
        /**
        	这里为什么需要CAS操作?(自我理解,多多指教)
        	如果线程Thread-0已经将同步状态从status=0修改成了status=1
        	此时又有其他线程Thread-1,Thread-2,Thread-3,...,来进行CAS操作,试图将status=0改成1
        	但是Thread-1,Thread-2,Thread-3,...,都会导致CAS操作失败。
        	被阻塞的线程会被构造成一个节点对象通过CAS操作加入到同步队列的尾部,之所以使用CAS操作,是因为			 tail指向该节点Thread-3时,可能Thread-2已经更改了tail的值,主要是为了保证线程安全和提高效率
        */
        
        /**
            因为tail是加了volatile关键字的,因此其数据的更改会被同步到主存中,被其他线程可见,
            在setTail之前会比较获取到的旧值pred和重新从主存中获取的新值进行比较:
            如果一致,就将node更新为pred,那么此时新的节点node指向tial节点
            如果不一致,CAS失败,就是说将线程加入到同步队列失败
        */
        //通过CAS操作设置新节点node为尾节点,指向tail节点,快速添加尾节点
        if (compareAndSetTail(pred, node)) {
            //CAS成功后,node成为尾节点,将pred节点的next指针指向node
            pred.next = node;
            return node;
        }
    }
    //1、如果尾节点为空,说明队列还未初始化,需要初始化head节点并将node加入到队列尾部
    //2、如果将节点快速添加到队尾中,没添加进去,会继续执行enq(node)方法以CAS自旋的方式添加,知道添加进去为止
    enq(node);
    return node;
}

addWaiter(Node mode)方法的作用就是将当前线程构造成一个节点对象然后加入到同步队列的队尾,这个方法有两层逻辑:

  1. 如果该线程是第一个出现竞争的线程Thread-1,那么就说明队列尾空,会直接执行enq(node)方法,先初始化同步队列,构造一个哨兵节点,该节点不关联任何线程,然后将当前线程节点加入到队列的尾部,同步器通过“死循环”来保证节点正确被添加,在“死循环”中只有通过CAS将节点设置为尾节点之后,当前线程才能从该方法中返回,否则,当前线程不断通过CAS尝试设置。
  2. 如果该线程不是第一个出现竞争的线程Thread-2,那么说明同步队列不为null,首先会执行该方法快速将该节点添加到同步对队列的队尾,如果没有添加成功,会继续执行enq(node)方法,以CAS自旋将该节点添加到队尾中。

总结addWaiter 逻辑:

  1. Node包装当前线程
  2. pred 尾指针不为null,即队列不为空, 则快速CAS将自己设为新的tail
  3. 如果队列为空, 则调用enq强制入队
  4. 如果CAS设置失败,说明在其它线程入队节点争抢了tail,则此线程只能调用enq强制入队

4、enq(final Node node)

  1. 由于Thread-1是第一个要竞争锁的线程,因此同步队列尾节点为null,说明队列还未初始化,需要初始化head节点,然后将当前节点添加到同步队列的队尾
  2. 将Thread-2节点添加到队尾中没有添加成功

以上两种情况都会执行enq(final Node node)方法:

//初始化同步队列,设置头节点和尾节点
/**
同步器通过“死循环”来保证节点正确被添加,在“死循环”中只有通过CAS将节点设置为尾节点之后,当前线程才能从该方法中返回,否则,当前线程不断通过CAS尝试设置。
*/
private Node enq(final Node node) {
	//死循环,CAS自旋,多次尝试,直到成功为止
    for (;;) {
        //指向tail节点的同步队列的尾节点
        Node t = tail;
        //如果t=null,说明队列还没有初始化,需要先通过CAS自旋设置一个头结点head
        if (t == null) { 
            if (compareAndSetHead(new Node()))
                //该head 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
                tail = head;
        //如果t!=null,说明队列已经初始化,那么会让节点t以自旋的方式加入到队列的尾部
        } else {
            //将当前节点的prev指向t
            node.prev = t;
            //通过CAS的方式将t节点更新为node节点,就是将node节点加入到队列的尾部,此时node节点指向tail
            if (compareAndSetTail(t, node)) {
                //如果CAS成功,说明node节点成功添加到同步队列尾部
                t.next = node;
                return t;
            }
        }
    }
}

方法内是一个for(;;),看来退出的条件只能是当前线程入队成功。之前也提到过,只有在产生锁竞争了,才会去初始化链表头节点。如果队列为空,初始化头尾节点,然后后续循环会走到else,else的逻辑和上线CAS入队的逻辑一样,只不过这里套在for循环里,直到入队成功才退出循环。

addWaiter的实现比较简单且实现功能明了:把当前线程构造成一个节点node对象,加入到同步队列队尾。

AQS与ReentrantLock底层源码分析_第14张图片

5、acquireQueued()

这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。

final boolean acquireQueued(final Node node, int arg) {
    //标记是否成功获取锁
    boolean failed = true;
    try {
        //标记是否被打断
        boolean interrupted = false;
        //死循环,调用tryAcquire(arg)方法,CAS自旋不断尝试获取锁
        for (;;) {
            //获取node节点的前驱节点
            final Node p = node.predecessor();
            //如果前驱节点为头节点Head,即该结点已成老二,那么便有资格去尝试获取锁
            //可能是老大释放完资源唤醒自己的,当然也可能被interrupt了
            //执行tryAcquire(arg)方法尝试获取锁
            if (p == head && tryAcquire(arg)) {
                //如果该节点获取到了锁,就将当前节点设置为头节点
                setHead(node);
                //原head节点出队,在某个时间点被GC回收
                p.next = null; // help GC
                //获取锁成功
                failed = false;
                //返回等待的过程中是否被中断过
                return interrupted;
            }
            
            //如果上一步获取锁失败后,判断是否可通过park()进入waiting状态,直到被unpark()
            //如果不可中断的情况下被中断了,会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                //如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
                interrupted = true;
        }
    } finally {
        if (failed)
            //若等待过程中没有成功获取资源(timeout,或可中断的情况下被中断了),取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

failed 标记最终acquire是否成功, interrupted标记是否曾被挂起过。注意到for(;;) 跳出的唯一条件就是if (p == head && tryAcquire(arg)) 即当前线程结点是头结点且获取锁成功。从这里我们应该看到,这是一个线程第三次又想着尝试快速获取锁:虽然此时该节点已被加入等待队列,在进行睡眠之前又通过p == head && tryAcquire(arg)方法看看能否获取锁。也就是说只有该线程结点的所有 有效的前置结点都拿到过锁了,当前结点才有机会争夺锁,如果失败了那就通过shouldParkAfterFailedAcquire方法判断是否应该挂起当前结点,等待响应中断。

6、shouldParkAfterFailedAcquire()

/**
	判断当前线程获取锁失败之后是否需要挂起.
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前驱节点的等待状态waitStatus
    int ws = pred.waitStatus;
    //如果前驱节点的等待状态为Node.SIGNAL=-1,就说明当前节点可以休息了
    //线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true。然后调用parkAndCheckInterrupt,将自己挂起。
    if (ws == Node.SIGNAL)
        return true;
    
    //如果ws > 0,前驱节点的状态为CANCELLED=1
    //如果ws>0说明前置结点是被自己取消获取同步的结点(只有线程本身可以取消自己)。
	//那么do while循环一直往头结点方向找waitStatus < 0的节点;
	//含义就是去除了FIFO队列中的已经自我取消申请同步状态的线程。
    if (ws > 0) {
        do {
            //从队尾向前寻找第一个状态不为CANCELLED=1的节点
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //如果ws<=0,那么就通过CAS将pred的前驱节点的等待状态改成Node.SIGNAL=-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

7、parkAndCheckInterrupt()

// 挂起当前线程,返回线程中断状态并重置
private final boolean parkAndCheckInterrupt() {
    //调用park()方法阻塞当前线程
    LockSupport.park(this);
    //返回中断状态,如果没有线程打断该线程的休息,就会返回false,一旦有其他线程打断睡眠,就会返回true
    return Thread.interrupted();
}

具体总结下上面这三个方法的作用:处于等待队列中的线程Thread-1尝试获取锁的过程

AQS与ReentrantLock底层源码分析_第15张图片

执行acquireQueued()方法内的第一次for循环,第一次尝试获取锁:

获取Thread-1线程节点的前驱节点,并判断该节点是不是头结点Head,如果是的话,Thread-1线程就会继续执行tryAcquire(1)方法尝试获取锁,很明显CAS会失败,因为Thread-0已经获取了同步资源。失败后接着会向下执行shouldParkAfterFailedAcquire()方法,该方法首先会获取前驱节点的等待状态waitStatus:

如果waitStatus=-1,就说明当Thread-1节点的前驱节点获取锁并且出队后会把Thread-1唤醒(“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”),同时return true;接着会继续执行parkAndCheckInterrupt()方法让Thread-1线程挂起(调用park()方法让该线程阻塞住,不用一直CAS自旋尝试获取锁)。

很明显Thread-1线程的前驱节点的waitStatus=0,那么就会执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL);方法将waitStatus设置为-1,同时return false

AQS与ReentrantLock底层源码分析_第16张图片

执行acquireQueued()方法内的第一次for循环,第二次尝试获取锁:

首先获取Thread-1线程节点的前驱节点,并判断该节点是不是head,如果是的话,Thread-1线程户继续执行tryAcquire(1)方法尝试获取锁,由于Status=1,因此获取CAS失败,就是说该线程有一次通过CAS获取锁失败。失败后会接着继续向下执行shouldParkAfterFailedAcquire()方法,该方法首先会获取前驱节点的等待状态waitStatus,进行判断:

如果waitStatus=-1,就说明Thread-1节点的前驱节点获取锁并出队后会把Thread-1线程唤醒,同时return true。接着会继续执行parkAndCheckInterrupt()方法让Thread-1线程挂起(调用park()方法让该线程阻塞住,不用一直CAS自旋尝试获取锁)。在挂起的过程中,如果没有被其他线程打断,就会一直处于阻塞状态。如果在阻塞的过程中被其他线程打断了,那么return Thread.interrupted();就会返回true。继而acquireQueued()方法就会继续向下执行interrupted = true;,同时被打断的线程会继续执行for循环尝试获取锁,如果获取不到会继续调用park()进入阻塞状态。

再次有多个线程经历上述过程竞争失败,变成这个样子:
AQS与ReentrantLock底层源码分析_第17张图片

8、release(int arg)

Thread-0 释放锁,进入 tryRelease 流程

public void unlock() {
    sync.release(1);
}
  
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        //找到头节点
        Node h = head;
        //当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
        if (h != null && h.waitStatus != 0)
            //唤醒等待队列里的下一个线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

9、tryRelease(int arg)

protected boolean tryRelease(int arg) {
	throw new UnsupportedOperationException();
}

跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。Thread-0 释放锁,进入 tryRelease 流程,如果成功设置 exclusiveOwnerThread 为 null、state = 0
AQS与ReentrantLock底层源码分析_第18张图片

10、unparkSuccessor()

//找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    
	//找到下一个需要唤醒的结点s
    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);
}

release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

在release(1)中,如果当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程,找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1,回到 Thread-1 的 acquireQueued 流程 ,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了。

AQS与ReentrantLock底层源码分析_第19张图片

如果加锁成功(没有竞争),会设置exclusiveOwnerThread 为 Thread-1,state = 1。head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread。原本的 head 因为从链表断开,而可被垃圾回收 。

如果这时候有其它线程来竞争(非公平锁的体现),例如这时有 Thread-4 来了
AQS与ReentrantLock底层源码分析_第20张图片

如果不巧又被 Thread-4 占了先,Thread-4 被设置为 exclusiveOwnerThread,state = 1。Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 。

总结:根据源码画了一个流程图,奈何页面太小,字体显示的不清楚
AQS与ReentrantLock底层源码分析_第21张图片

2、可重入锁底层

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取state变量的值
    int c = getState();
    //如果state=0,说明还没有线程来占用锁
    if (c == 0) {
        //通过CAS将state=0设置为1
        if (compareAndSetState(0, acquires)) {
            //占用锁成功,将独占线程设置为当前线程
            setExclusiveOwnerThread(current);
            //同时返回true,代表获取锁资源成功
            return true;
        }
    }
    //如果state=1,说明已经有其他线程获取了锁
    //判断ExclusiveOwnerThread指向的线程是否是当前线程,就是说判断是不是自己占用了锁,锁重入原理
    else if (current == getExclusiveOwnerThread()) {
        //如果是自己占有了锁,就说明出现了锁重入,那么就让state+=1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //更新state的重入次数
        setState(nextc);
        //返回true,同样表示占用锁成功
        return true;
    }
    //如果是其他线程占用了锁,那么获取锁失败,返回false.
    return false;
}

原理:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。

3、公平锁和非公平锁的区别

两者的区别主要体现在加锁过程上的区别,即tryAcquire(1)方法的不同:

对于非公平锁的tryAcquire(1)方法:

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取state变量的值
    int c = getState();
    //如果state=0,说明还没有线程来占用锁
    if (c == 0) {
        //通过CAS将state=0设置为1
        if (compareAndSetState(0, acquires)) {
            //占用锁成功,将独占线程设置为当前线程
            setExclusiveOwnerThread(current);
            //同时返回true,代表获取锁资源成功
            return true;
        }
    }
    //如果state=1,说明已经有其他线程获取了锁
    //判断ExclusiveOwnerThread指向的线程是否是当前线程,就是说判断是不是自己占用了锁
    else if (current == getExclusiveOwnerThread()) {
        //如果是自己占有了锁,就说明出现了锁重入,那么就让state+=1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //更新state的重入次数
        setState(nextc);
        //返回true,同样表示占用锁成功
        return true;
    }
    //如果是其他线程占用了锁,那么获取锁失败,返回false.
    return false;
}
  1. 检查state字段,若为0,表示锁未被占用,那么尝试占用
  2. 若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。
  3. 如果以上两点都没有成功,则获取锁失败,返回false。

非公平锁,首先是检查并设置锁的状态,如果state=0,就会去尝试加锁,并不会到同步队列中等待获取锁,这种方式会出现即使队列中有等待的线程,但是新的线程仍然会与同步队列中等待获取同步状态的线程竞争,所以新的线程可能会抢占已经在排队的线程的锁,这样就无法保证先来先服务,但是已经等待的线程们是仍然保证先来先服务的。

对于非公平锁的tryAcquire(1)方法:

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

    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        //获取当前线程
        final Thread current = Thread.currentThread();
        //获取state变量的值
        int c = getState();
        //如果state=0
        if (c == 0) {
            //判断AQS队列中是否有前驱节点,如果没有就尝试获取锁资源
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                //将当前线程设置为加锁线程
                setExclusiveOwnerThread(current);
                //结果返回true,表示加锁成功
                return true;
            }
        }
        //判加锁线程是不是当前线程,可重入锁的原理
        else if (current == getExclusiveOwnerThread()) {
            //如果是,就将同步状态state+=1
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            //更新state字段表示锁重入的次数
            setState(nextc);
            //结果返回true,表示加锁成功
            return true;
        }
        //否则返回false
        return false;
    }
}

hasQueuedPredecessors() 方法:

和非公平锁对比多了这个方法逻辑, 也就意味着没有了新来线程插队的情况,保证了公平锁的获取串行化。

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    //h != t表示队列中有node,
    return h != t &&
        //表示队列中还没有老二,或者队列中的老二线程不是当前线程
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

在公平锁中,每一次的tryAcquire都会检查CLH队列中是否仍有等待获取锁资源的线程,如果有返回false,进而自己也加入到等待队列中,通过这种方式来保证先来先服务的原则。

4、公平锁和非公平锁的释放

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;
}

//这个方法由子类去实现,这里公平锁和非公平锁的实现类逻辑相同
protected boolean tryRelease(int arg) {
   throw new UnsupportedOperationException();
}

自定义同步器ReentrantLock方法中 tryRelease()方法的具体实现类:

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    //尝试释放锁
    protected final boolean tryRelease(int releases) {
        //计算锁重入的次数
        int c = getState() - releases;
        //如果占有锁的线程不是当前线程,就抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //如果state=0,说明锁重入次数为0,表示释放成功
        if (c == 0) {
            free = true;
            //将owner设置为null
            setExclusiveOwnerThread(null);
        }
        //重置锁重入的次数
        setState(c);
        return free;
    }
}

5、不可中断原理

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

/**
    1、假如Thread-0线程正在阻塞状态,其他线程如Thread-1找到THread-0线程对象,
       调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
*/
private final boolean parkAndCheckInterrupt() {
	//让该线程阻塞,如果打断标记为true,那么park()方法失效
    LockSupport.park(this);
    //返回是否被打断过,如果被打断了返回true,否则返回false,同时还会清除打断标记
    return Thread.interrupted();
}

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        /**
        	3、当线程被打断后,就会醒过来,醒过来以后将interrupted=true
        	  然后重新去判断if (p == head && tryAcquire(arg)) 并尝试获取锁
        	  只有获取锁成功,才会返回这个打断标记interrupted=true
        */
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            /**
            	2、当parkAndCheckInterrupt()返回true后,就会继续向下执行,
            		将打断标记置为true,但是并用到这个打断标记,进而继续进行下一次的for循环
            */
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                //将打断标记置为true,同时继续进行下一次的for循环
                interrupted = true;
            }
        } finally {
            if (failed)
            cancelAcquire(node);
    }
}
    
public final void acquire(int arg) {
    /**
    	4、如果acquireQueued()返回true,就会继续执行selfInterrupt()方法
    */
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

static void selfInterrupt() {
    //重新产生一次中断,即改变了中断状态,interrupted=true
	Thread.currentThread().interrupt();
}

总结:

  1. 假如Thread-0线程正在阻塞状态在同步队列中等待获取锁,其他线程如Thread-1找到Thread-0线程对象, 调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
  2. 当parkAndCheckInterrupt()返回true后,就会继续向下执行,打断标记置为true,但是并用到这个打断标记,进而继续进行下一次的for循环(继续向下运行了,就是说被打断后并没有放弃等待获取锁资源)
  3. 当线程被打断后,就会醒过来,醒过来以后将interrupted=true,执行for循环, 然后重新去判断if (p == head && tryAcquire(arg)) 并尝试获取锁,只有获取锁成功,才会返回这个打断标记interrupted=true。
  4. 如果acquireQueued()返回true,就会继续执行selfInterrupt()方法,停止等待锁资源

总的来说,如果其他线程打断了当前线程的阻塞状态,当前线程醒来后也不会放弃获取锁,而是继续向下执行,尝试获取锁,获取不成功,继续进入阻塞状态。即当前线程会一直在同步队列中死等,直到获取了锁为止。

6、可中断原理

static final class FairSync extends Sync {
	//...
    //调用ReentrantLock的lockInterruptibly()方法,就会执行AQS内的acquireInterruptibly
    //其实ReentrantLock就相当于AQS的外层API
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public final void acquireInterruptibly(int arg) throws InterruptedException {
        //判断线程是否处于中断状态,如果处于就说明中断状态为true,同时清除打断标记,将中断状态设置为false
        if (Thread.interrupted())
            //如果线程处于打断状态,就会直接抛出异常,由于这个异常并没有别捕获,因此后面的代码就不会再执行
            throw new InterruptedException();
        //尝试获取锁,如果CAS失败就会继续向下执行(2)
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
    
    //(2)
    
    /**
    	2、如果parkAndCheckInterrupt()返回true,继续执行throw new InterruptedException();
    	抛出异常就不会再进入死循环for(;;)中,这也是终止线程的方法之一
    	抛出异常后,那么线程就会停止等待获取锁,即不会再死等了
    */
    private void doAcquireInterruptibly(int arg) throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
           
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    /**
    	1、假如Thread-0线程正在阻塞状态,其他线程如Thread-1找到THread-0线程对象,
           调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
    */
    private final boolean parkAndCheckInterrupt() {
        //让该线程阻塞,如果打断标记为true,那么park()方法失效
        LockSupport.park(this);
        //返回是否被打断过,如果被打断了返回true,否则返回false,同时还会清除打断标记
        return Thread.interrupted();
    }
}

总结:

  1. 假如Thread-0线程正在阻塞状态在同步队列中等待获取锁,其他线程如Thread-1找到Thread-0线程对象, 调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
  2. 当parkAndCheckInterrupt()返回true后,就会继续向下执行,抛出异常,不再执行for()循环,方法执行结束,线程终止,停止等待获取锁资源。

7、条件变量await()原理

每个条件变量其实就对应着一个条件队列,其实现类是ConditionObject,该类是AQS的内部类,一个ConditionObject是一条等待队列。

1、await()方法

public class ConditionObject implements Condition, java.io.Serializable {
    
    /** 等待队列的头节点 */
    private transient Node firstWaiter;
    /** 等待队列的尾节点 */
    private transient Node lastWaiter;
    
    public final void await() throws InterruptedException {
        //如果等待的线程别打断,那么直接抛出异常InterruptedExceptio
        if (Thread.interrupted())
            throw new InterruptedException();
        //当前线程封装成一个Node节点,并将该节点添加到ConditionObject的等待队列中
        Node node = addConditionWaiter();
        //接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁,返回节点状态
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        //判断当前节点在不在同步队列中,如果不在,说明是条件队列中的节点
        while (!isOnSyncQueue(node)) {
            //调用park()将当前线程阻塞
            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);
    }
}

2、addConditionWaiter()方法

调用addConditionWaiter()方法将当前线程封装成一个Node节点,并将该节点添加到ConditionObject的等待队列中

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程,创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入条件队列尾部

AQS与ReentrantLock底层源码分析_第22张图片

3、fullyRelease(node)方法

接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

释放锁,返回同步状态。释放锁失败的话,节点的waitStatus为CANCELLED。

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

AQS与ReentrantLock底层源码分析_第23张图片

unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

AQS与ReentrantLock底层源码分析_第24张图片

总结:

  1. 调用addConditionWaiter()方法将当前线程封装成一个Node节点,并将该节点添加到ConditionObject的等待队列中
  2. 调用fullyRelease(node)方法将当前线程所持有的锁资源完全释放
  3. while (!isOnSyncQueue(node)){…},判断当前线程的节点是否在AQS的同步队列中,因为当前线程的节点刚刚被加入到ConditionObject的等待队列中,所以isOnSyncQueue(node)方法返回false,取反则会进入到while循环体中
  4. 调用LockSupport.park(this)方法,将当前线程阻塞,此时线程的状态为WAITING。此时线程等待被唤醒,唤醒后的线程会从LockSupport.park(this)方法开始继续向下执行。
  5. 当线程被唤醒后,执行语句if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)判断当前线程的唤醒方式,以及对应的后续操作。

有两种方法可以将一个状态为WAITING的线程唤醒

  • 调用LockSupport.unpark(Thread thread)方法。
  • 调用thread.interrupt方法。没错,调用中断方法会将一个线程唤醒

8、条件变量的single()原理

假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node 。

1、single()方法

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

2、doSignal(first)方法

将Condition的等待队列的头结点出队,并将符合条件的节点添加到AQS的等待队列中

进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

3、transferForSignal(Node node)方法

执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的
waitStatus 改为 -1

final boolean transferForSignal(Node node) {
    //如果node节点的等待状态不为Node.CONDITION,则表示该节点的线程已经被取消或者在调用signal()
    //方法之前已经被其他线程中断,所以直接将该节点从condition的等待队列中剔除
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //将当前节点添加到AQS的同步队列中,返回值p为当前节点在阻塞队列的前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}
  1. ws > 0 的情况是当前节点在AQS阻塞队列的前驱节点的状态为Node.CANCELLED,此时将会唤醒当前节点,并在acquireQueued方法中调用shouldParkAfterFailedAcquire方法将状态为Node.CANCELLED的节点从AQS的阻塞队列中剔除
  2. !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 为true,即当前线程准备通过CAS将前驱节点的状态改为Node.SIGNAL失败。从AQS获取锁和释放锁的代码中可以看出,AQS阻塞队列节点的前驱节点状态必须要为Node.SIGNAL。CAS修改失败可能可能的场景是在执行CAS的时候,前驱节点线程的状态变为Node.CANCELLED。

AQS与ReentrantLock底层源码分析_第25张图片

你可能感兴趣的:(Java并发编程)