AQS源码解析-线程的唤醒与阻塞

AQS简介

在一个JVM中采取同步的方式:

  • 显示锁:ReentrantLock
  • 内置锁:Synchronized
  • 原子类:如AtomicInteger、AtomicLong、AtomicBoolean

AbstractQueuedSynchronizer(AQS)是一个用于构建锁和同步器的框架。基于(AQS)构建的有ReentrantLockCountDownLatchSemaphoreFutureTask等。其中ReentrantLock是独占锁、CountDownLatchSemaphore是共享锁。CyclicBarrier阻塞队列是基于ReentrantLock来实现的。

AQS利用CSA来实现,即利用cpu的原子指令comparAndSet来实现。
基于AQS可以构建共享锁、独占锁。本文以独占锁为例。

AQS中的属性和要实现的方法

在这里插入图片描述
以上变量均通过CAS进行变量赋值(保证赋值时的原子操作)且都是volatile变量(值更改后线程可见)。变量赋值类似,如state赋值:

 protected final boolean compareAndSetState(int expect, int update) {
        // 当前state值与expect值相同,则把update的值赋给state
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

要点:state变量,锁标志状态(线程通过或者线程阻塞)。在ReentrantLock中是重入锁的个数。CountDownLatchSemaphore中是共享锁的个数。

AQS供子类实现的方法如下

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    //上述两个方法构建独占锁同步器需要实现
    
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //上述两个方法构建共享锁锁同步器需要实现
    
     protected boolean isHeldExclusively() {//该线程是否正在独占资源。只有用到condition才需要去实现它。
        throw new UnsupportedOperationException();
    }
    

AQS框架源码分析

问题提出:
①AQS如何把没有获取到锁的线程挂起,放到阻塞队列中?
②AQS什么条件唤醒挂起的线程?
③唤醒线程时,为什么从尾节点遍历?

AQS中的线程阻塞

ReentrantLock中的非公平锁为例:请打开它的源码,探得其中机理。第一次获取锁成功,此时不会产生阻塞队列,获得锁之后程序继续执行,假设线程名称为successThread。当successThread线程没有释放锁即程序没执行完,此时有其它线程获取lock锁,其它线程就会阻塞,阻塞的线程会放到阻塞队列中。阻塞队列就是双向链表(AQS中的node)。

 final void lock() {
 //原子操作执行成功代表获取锁成功。成功的条件必须当前的state值为0
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            //获取失败执行AQS中的acquire方法
                acquire(1);
        }

以AQS中的acquire方法作为切入点:

 public final void acquire(int arg) {
 //tryAcquire需要自己实现,重点解析下面的几个方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

调用tryAcquire时序图:
在这里插入图片描述

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //设置可重入锁的数量,当前线程必须与lock方法设置的线程相等
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

其它线程获取锁时会直接返回false

接着分析addWaiter方法。node数据结构参考LinkedList中node的数据结构。是双向链表。

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 第一个没有获取到锁的线程,此时tail必为null
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {//尾节点已指向node,不需要再把尾节点重新指向node
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

enq方法。在第一次插入node或设置尾节点失败,执行此方法。
作用:自旋直至把Node放入到阻塞队列中

private Node enq(final Node node) {//node的
        for (;;) {//自旋直至把Node放入到阻塞队列中
            Node t = tail;
            if (t == null) { 
                if (compareAndSetHead(new Node()))//初始化头节点
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {//尾节点指向node
                    t.next = node;
                    return t;
                }
            }
        }

若tail为null第一次遍历初始化头节点,new了一个node对象其中prev,next,thread均为null。第二次遍历才把参数中的node放到头节点的后面。

acquireQueued方法。
作用:自旋直至获取锁或者进行线程阻塞,若线程中断则把waitStatus设置成Node.CANCELLED,值为1

  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; //之前的head节点不存在
                    failed = false;
                    return interrupted;
                }
                //shouldParkAfterFailedAcquire与parkAndCheckInterrupt返回false则会继续循环
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//线程阻塞
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire方法。
作用:找到阻塞队列中没有线程中断的线程并把WaitStatus设置成Node.SIGNAL,值为-1。

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
            
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {//把阻塞的node设置成待唤醒状态
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt方法。
作用:阻塞线程,唤醒后返回线程中断标志。若线程被中断了则会调用selfInterrupt()方法。

 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//进行阻塞,this是调用acquire方法的对象,
        //在本例中指的是NonfairSync对象。并不会再往下执行,需要等待被唤醒
        return Thread.interrupted();//返回线程中断的标志
    }

注意

  • enq方法跟acquireQueued方法的自旋结束条件。插入第一个节点时,enq会创建一个node。
  • parkAndCheckInterrupt中把线程阻塞
  • acquireQueued方法获取到锁之后会把原head给释放,并把原head的第一个尾节点作为head。

AQS中的线程唤醒

ReentrantLock中的unlock方法,调用的是AQS中的release方法

 public final boolean release(int arg) {
        if (tryRelease(arg)) {//调用ReentrantLock.Sync中的tryRelease方法
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒线程
            return true;
        }
        return false;
    }

tryRelease方法。
作用:因为ReentrantLock是可重入的,返回true时唤醒线程。返回true的条件是state为0。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//为0返回的结果才是true
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

注意:比如重入锁的个数为3,前两次调用unLock时不会唤醒阻塞的线程,只会改变state的值。

unparkSuccessor方法。
作用:唤醒指定线程。

 private void unparkSuccessor(Node node) {
 //第一次调用传入的node为head。node.waitStatus值为0
        int ws = node.waitStatus;
        
        if (ws < 0)//在shouldParkAfterFailedAcquire方法中状态已设置为Node.SIGNAL即值为-1
            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中唤醒线程从尾节点遍历的原因

从尾节点遍历的原因是从头节点遍历会出现断链的情况。
在addWaiter方法、enq方法给尾节点赋值(compareAndSetTail)之后,还需从尾节点指向当前要插入链表的节点即t.next = node;执行compareAndSetTail是原子操作,执行t.next = node;不是原子操作不能保证线程间的顺序问题。
以使用ReentrantLock为例假设:
有三个线程,线程名为A、B、C。线程A正在执行,已抢到锁。线程B、线程C同时执行addWaiter中的compareAndSetTail方法,线程B设置尾节点成功,线程C在enq方法设置尾节点成功。线程B没有执行t.next = node;线程C已执行。此时线程A释放锁,执行unparkSuccessor寻找后续节点。如图:
在这里插入图片描述
说明:线程A正在执行
在这里插入图片描述
说明:线程B已执行addWaiter中的compareAndSetTail。但还没有执行pred.next = node;
在这里插入图片描述
说明:线程C已执行enq中的compareAndSetTail且已执行pred.next = node;线程A释放锁之后,若从头节点进行遍历则next指向的为null。获取不到节点。无论pre还是next指向的是node的地址,node包含pre跟next。并不是pre指向next,next指向的是pre。

总结:

  • 程序在运行的时候是基于线程的,线程阻塞即程序阻塞。
  • 使用显示锁,调用lock方法时程序便阻塞在parkAndCheckInterrupt()方法。程序不再继续执行,等待唤醒线程,进而再执行程序。
  • release方法调用tryRelease方法尝试唤醒(LockSupport.unpark),被唤醒的线程在阻塞的地方继续执行,即在acquireQueued方法的parkAndCheckInterrupt()方法继续执行。
  • 唤醒线程时,是指定唤醒,唤醒的是head节点中的第一个尾节点。
  • 唤醒线程时,采用的是尾节点遍历。链表有断链的问题。

本文只分析了独占锁,有理解不当之处望指出

每篇一语

花总是要开的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值