学习小记 -- 并发之Java中的锁(AQS)

本文探讨了Java并发中Lock接口与Synchronized的差异,重点讲解了AQS队列同步器如何实现锁获取与释放,以及ReentrantLock的非公平锁与公平锁原理,包括性能比较和共享锁机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Lock接口和Sychronized

 

在谈AQS之前先来谈谈我们常见的Lock 和 Sychronized。在Java SE1.5以前,Java程序是靠Sychronized关键字实现锁功能的,1.5之后在并发包中新增了Lock接口等,那两者的区别是什么?Lock接口在功能上和Sychronized一样,只是在使用时显示地获取和释放锁缺少隐式释放锁的便捷性。但是拥有了锁获取和释放的可操作性可中断的获取锁以及超时获取锁,这些是Sychronized不具备的。

在使用Lock的时候,要在fnally块中释放锁,目的是保证在获取到锁之后,最终能狗被释放。

注:不要将获取锁的过程写在try块中,应为如果获取锁(自定义锁实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

队列同步器 -- AbstractQueuedSynchronizer(AQS)

提到并发,就不得不提AQS了,队列同步器 AbstractQueuedSynchronizer是用来构建锁或者其他同步组建的基础框架,它使用了一个int类型的变量作为共享资源,表示同步状态。

同步器的实现是基于模版模式的,有Set三个方法可以开访问或修改同步状态:

  1. getStatus:获取当前同步状态。
  2. setStatus(int nuwStatus):设置当前同步状态。
  3. compareAndSetStatus(int expect,int update):使用CAS设置当前状态-保证原子性。
  4. tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回true,失败则返回false。

  5. tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回true,失败则返回false。

  6. tryAcquireShared(int):共享方式获取锁。返回值>0,成功,否侧失败。

  7. tryReleaseShared(int):共享方式释放锁。

只有掌握了同步器的工作原理才能更加深入地理解并发包中其他的并发组件,下面来介绍下AQS的实现分析。

同步器内部其实是使用一个FIFO双向队列来管理同步状态,当前线程获取状态失败时,同步器会将当前线程和等待状态构建成一个Node加入同步队列,并阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,再次尝试获取同步状态。

 

下面通过ReentrantLock的非公平锁来具体看下AQS获取锁的实现:

static final class NonfairSync extends Sync {
        
        final void lock() {
            //使用CAS获取锁,成功将state状态设置为1,并设置对象独占线程为当前线程
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);   //抢占失败
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

假如线程1抢占成功后,线程2来了必然会抢占失败,进入else逻辑:

独占式获取锁:

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

tryAcquire尝试获取锁,若返回false(线程2必定返回false),会执行addWaiter将自己加入FIFO队列,此方法会返回当前线程创建的节点信息,接着执行acquireQueued方法,使该节点以“死循环”的方式获取同步状态,如果获取不到则阻塞节点中的线程。

tryAcquire在ReentrantLock中的实现方法是nonfairTryAcquire(非公平):

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    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;
}
先获取同步状态,若state=0,说明获取锁成功,通过CAS设置state=1,如果获取失败,说明当前对象已经被其他线程占有,接着判断所对象线程是否为当前线程,如果是,则state累加,这就是所谓的可重入。
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;
}

上述代码使用compareAndSetTail方法来确确保节点能够被线程安全的添加;继续看下enq方法:

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

在enq方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成尾节点之后,当前线程才能在该方法中返回。可以看出,enq方法将并发添加节点的请求通过CAS变得“串行化”了。

节点进入同步队列后,就会进入一个自旋的状态,只有当条件满足,才能获取到同步状态,就可以从自旋退出,否则依旧原地自旋。

待addWaiter(Node.EXCLUSIVE)返回后,进入acquireQueued方法:

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; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

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

上述方法中,线程同样通过“死循环”来尝试获取同步状态,并且要满足前驱节点必须是头节点的条件,加锁成功过则将当前节点设置为head节点,然后空置之前的head节点,方便后续被垃圾回收掉。如果加锁失败或者Node的前置节点不是head节点,就会通过shouldParkAfterFailedAcquire方法 将head节点的waitStatus变为了SIGNAL=-1,最后执行parkAndChecknIterrupt方法,调用LockSupport.park()挂起当前线程。

下方为独占式同步状态的获取流程:

同步器的release方法:

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

 这里还是假设线程1,释放锁,线程2(队列的头节点)获取锁,线程1首先会执行tryRelease()方法,这个方法具体实现在ReentrantLock中,如果tryRelease执行成功,则继续判断head节点的waitStatus是否为0,headwaitStatueSIGNAL(-1),这里就会执行unparkSuccessor()方法来唤醒head的后置节点,也就是我线程二对应的Node节点。

ReentrantLock实现的tryRelease方法:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

 执行完tryRelease方法后,state被设置成0Lock对象的独占锁被设置为null

接着执行java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()方法,唤醒head的后置节点:

private void unparkSuccessor(Node node) {
        
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

   
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;    //删除next指向,方便垃圾回收
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);    //唤醒下个线程
}

这里主要是将head节点的设置waitStatus=0,然后解除head.next=null,使head节点空置,等待着被垃圾回收。

重新将head指针指向线程2对应的Node节点,且使用LockSupport.unpark方法来唤醒线程2,线程2继续执行acquireQueued方法。

被唤醒的线程2会接着尝试获取锁,用CAS指令修改state数据。

以上介绍的ReentrantLock的加锁场景都是基于非公平锁来实现的,是ReentrantLock默认实现

非公平锁公平锁的区别

  • 非公平锁性能高于公平锁性能。非公平锁可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 非公平锁性能虽然优于公平锁,但是会存在导致线程饥饿的情况。在最坏的情况下,可能存在某个线程一直获取不到锁。

队列同步器还有共享式的获取锁:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}


private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

共享式获取同步状态成功并且退出自旋的条件是tryAcquireShared方法的返回值>=0。

共享的方式释放锁:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

该方法在释放锁之后,会唤醒后续处于等待状态的节点。对于能支持多个线程同时访问的并发组件(如Semaphore),它和独占式区别主要在于tryReleaseShared(arg)方法必须确保同步状态线程安全释放,一般是通过循环CAS来保证的,应为释放同步状态的操作会同时来自多个线程。

参考:

  1. 《Java并发编程艺术》 --方腾飞
  2. https://mp.weixin.qq.com/s?__biz=MzAwNDA2OTM1Ng==&mid=2453142063&idx=2&sn=cbb98bf1591de954d053c8c3e310470c&scene=21#wechat_redirec
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值