【多线程高并发系列】透过ReentrantLock看AQS

需要注意的是:ReentrantLock和synchronized对于wait/await或notify/signal方法的调用不会积压,也就是说当等待队列为空时调用notify/signal不会产生任何效果,并且会消耗调用的效果。于此相反的是LockSupport中的park和unpark,对于这两种方法的调用会积压,也就是说先调用unpark再调用park的效果与先调用park再调用unpark的效果相同。

ReentrantLock常用API

public static void main(String[] args) throws InterruptedException{
        ReentrantLock lock=new ReentrantLock();
        Condition condition=lock.newCondition();
        lock.lock();
        condition.await();
    	condition.signal();
        lock.unlock();
}

结果是main线程一直阻塞,主要利用这段代码探究ReentrantLock加锁解锁、等待唤醒的过程。

类图

在这里插入图片描述

  • ReentrantLock实现了Lock接口,Lock接口定义了锁的基本操作,如lock()、unlock()等。

  • AQS队列主要负责维护同步队列,实现了作为同步队列最通用的方法,如出队入队和对state的获取释放流程等,其中有比较重要的几个成员变量:

    volatile int state:表示临界值,在不同的锁实现中代表着不同的意义。比如在ReentrantLock中state=0则代表没有线程持有锁,state>0则代表有线程持有锁,锁重入则state+1,解锁state-1.

    volatile Node head,tail:表示同步队列的头节点和尾节点,队列本质上是双向链表。

    VarHandle STATE,HEAD,TAIL:主要用于修改state、head和tail的值。VarHandle类能以CAS的方式修改对象或变量的值(包括基本类型),类似于阉割版的Unsafe类。

    VarHandle是对变量或参数定义的变量系列的动态强类型引用,包括静态字段,非静态字段,数组元素或堆外数据结构的组件。 在各种访问模式下都支持访问这些变量,包括简单的 read/write 访问,volatile 类型的 read/write 访问,和 CAS(compare-and-swap)等。

    VarHandles是不可变的,没有可见状态。 VarHandles不能被用户子类化。

  • Sync继承了AQS,主要实现对state的获取(tryAcquire)和释放(tryRelease),子类FairSync和NonfairSync分别对应公平锁和非公平锁同步队列,分别实现了在公平锁和非公平锁状态下获取锁的方法,两种锁共用父类Sync中的释放锁(tryRelease)方法。

锁初始化

/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
    sync = new NonfairSync();
}

锁初始化时如果不指定参数则默认为非公平锁
公平锁与非公平锁的区别就是:

  • 公平锁在尝试获取锁的时候,会检查同步队列中是否有节点(节点中保存着相应线程),也就是检查是否有其他线程正在等待获取锁。如果有,则当前线程必须先入队等待;如果没有才能尝试获取锁。

  • 非公平锁在尝试获取锁的时候,不会检查同步队列,直接尝试去获取锁。若获取失败,则同样进入等待队列。

    非公平锁效率上高于公平锁,因为在非公平锁状态下,线程有几率直接获取成功而不需要入队,减少了挂起线程和入队的开销。

lock()上锁

lock方法首先调用同步队列Sync的acquire()方法。

public void lock() {
    sync.acquire(1);
}

acquire()是AQS中的方法,AQS中定义了线程获取锁的流程。

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

当线程准备获取锁时,首先调用tryAcquire()方法尝试一下能否获取锁(CAS尝试更改同步队列的state值)。tryAcquire()由Sync的两个子类分别实现公平锁与非公平锁下不同的操作,这里以非公平锁为例,tryAcquire()会调用nonfairTryAcquire()方法。

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取同步队列中的state
    int c = getState();
    //state=0表示当前没有线程持有锁
    if (c == 0) {
        //CAS尝试改变state,也就是尝试获取锁
        if (compareAndSetState(0, acquires)) {
            //获取成功则将当前线程设为这把锁的独占者(ReentrantLock是排他锁)
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //锁重入
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //只有当前线程持有锁,因此无需任何同步措施即可修改state
        setState(nextc);
        return true;
    }
    return false;
}

如果tryAcquire()失败则进入同步队列。首先将当前线程包装成Node节点入队。

先看一下Node的构造方法

Node(Node nextWaiter) {
    this.nextWaiter = nextWaiter;
    THREAD.set(this, Thread.currentThread());
}

THREAD是VarHandle类型的成员变量,用于CAS修改成员变量thread,在Node类的静态代码块中已经为4个VarHandle成员变量赋值,分别对应着要更改的四个成员变量。

private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
    try {
        MethodHandles.Lookup l = MethodHandles.lookup();
        //使每个VarHandle与一个变量相关联
        NEXT = l.findVarHandle(Node.class, "next", Node.class);
        PREV = l.findVarHandle(Node.class, "prev", Node.class);
        THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
        WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
    } catch (ReflectiveOperationException e) {
        throw new ExceptionInInitializerError(e);
    }
}

接着看入队方法addWaiter()

private Node addWaiter(Node mode) {
    //将当前线程包装为Node准备入队
    Node node = new Node(mode);

    //死循环保证CAS成功
    for (;;) {
        Node oldTail = tail;
        //同步队列中已有其他节点
        if (oldTail != null) {
            //将链表原来的尾节点设为新入队节点的前驱节点(尾插)
            node.setPrevRelaxed(oldTail);
            //CAS改变链表尾指向新入队的节点
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            //同步队列中还未添加过任何节点则初始化同步队列
            //采用懒加载的方式,当真正有节点需要入队时才初始化,降低开销
            initializeSyncQueue();
        }
    }
}

addWaiter()方法结束也就意味着新节点入队成功,接下来执行acquireQueued()方法开始排队。

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            //获取当前节点的前驱节点
            final Node p = node.predecessor();
            //如果前驱节点是头节点(当前节点是队列中的第二个节点)则尝试获取锁
            if (p == head && tryAcquire(arg)) {
                //获取到锁就将当前节点设为头节点(头节点除了在队列刚初始化时指向一个空的临时节点,
                //后来都指向已经获取锁的节点)
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            //当前驱结点不是头节点时或者尝试获取锁失败时判断是否应该阻塞当前线程
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        //若出现异常则将该节点的waitStatus置为CANCELLDE
        //如果当前节点是头节点(当前节点能获取锁但出现了异常)则唤醒后继节点
        //如果当前节点是尾节点则将自己从队列中移除
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

shouldParkAfterFailedAcquire(p, node)方法会判断线程尝试获取锁失败后是否应该阻塞。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前驱节点的waitStatus
    int ws = pred.waitStatus;
    //前置节点已经将waitStatus设为了SIGNAL,说明前置节点想让后面的节点(也就是当前节点)阻塞,
    //等待前置节点发出信号
    if (ws == Node.SIGNAL)
       /*
        * This node has already set status asking a release
        * to signal it, so it can safely park.
        */
        return true;
    //前置节点状态为CANCELLED,说明产生了异常,则当前节点直接越过前置节点
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        //当前节点需要被唤醒,但不立即阻塞,而是再尝试着获取一次锁
        //将前驱节点的waitStatus设置为SIGNAL,这样在下次循环中就会立即阻塞
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

如果在下一次尝试后还没有获取到锁,那么就调用parkAndCheckInterrupt()方法阻塞当前线程。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

至此加锁过程就结束了,要么获得锁,要么在同步队列里阻塞,等待前面的节点唤醒当前节点。

unlock()解锁

同样以非公平锁为例,先调用Sync的unlock() 方法。公平锁与非公平锁的解锁过程都是一致的,因此tryRelease()方法放到了Sync类中而不是子类中。

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

看一下Sync中的tryRelease()方法

protected final boolean tryRelease(int releases) {
    //释放锁对state做减法,获取锁做加法(独占锁),对于共享锁则相反
    int c = getState() - releases;
    //当前线程此时必须持有锁才能释放锁,否则抛异常(这里类似于synchronized,操作锁的前提是持有锁)
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果能完全释放锁
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

tryRelease()方法在线程完全释放锁之后(lock次数等于unlock次数)返回true。

再回到release()方法中

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        //waitStatus=0为初始化状态,说明没有后继节点,
        //也就是说当前节点的waitStatus没有被后继节点改为SIGNAL
        //如果waitStatus不为0,则意味着被后继节点改过,则需要唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

至此释放锁的过程也结束了。

下面是关于condition阻塞/唤醒线程,需要注意的无论在调用condition.await()或condition.signal()时,当前线程首先必须要获得锁才行,否则会抛出IllegalMonitorStateException异常。这种要求类似于synchronized。

condition.await()释放锁并等待

每个condition相当于一个队列,当调用condition.await()方法时会将当前线程包装成Node后加入等待队列。

首先从Condition condition=lock.newCondition()开始

public Condition newCondition() {
    return sync.newCondition();
}

final ConditionObject newCondition() {
    return new ConditionObject();
}

public ConditionObject() { }

可以看出只是new了一个ConditionObject对象,在ConditionObject对象中,保存着等待队列的队列头和队列尾指针(其实是通过单向链表将各个Node连接在一起,在Node中不光保存着指向前驱节点和后继节点的指针,还保存着指向下一个等待节点的指针nextWaiter。当然这只在等待队列中有效)。

再看await()方法

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //将当前线程包装为Node加入至等待队列
    Node node = addConditionWaiter();
    //先释放锁才能await,保存当前状态,以便于被唤醒后恢复状态
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        //阻塞当前线程
        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);
}

addConditionWaiter()与获取锁时向队列中添加节点类似,只是将当前节点加入等待队列并改变队列尾的指向。

看一下fullyRelease()方法。

final long fullyRelease(Node node) {
    try {
        long savedState = getState();
        //将state全部释放
        if (release(savedState))
            return savedState;
        throw new IllegalMonitorStateException();
    } catch (Throwable t) {
        //出现异常就取消
        node.waitStatus = Node.CANCELLED;
        throw t;
    }
}

public final boolean release(long arg) {
    //与释放锁时用到的Sync中的tryRelease(arg)方法是用一个方法
    if (tryRelease(arg)) {
        Node h = head;
        //如果有后继节点则唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

再回到await()方法中,如果成功释放锁则阻塞当前线程,此时当前节点已经处于等待队列中。

condition.signal()唤醒

当调用condition.signal()方法时,会从指定condition等待队列中取出头节点并转移到同步队列中等待获取锁

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    //如果等待队列不为空
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        //如果队列中只有一个节点
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
    	//将节点转移到同步队列中
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

参考文章

Java 9 变量句柄-VarHandle

Java API中文文档-VarHandle

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值