从AQS源码的角度理解锁重入,锁竞争,公平锁,不可重入锁

摘要:该文章深入探讨了Java并发编程中的关键组件之一,即AQS(AbstractQueuedSynchronizer),并对其源代码进行了详细解读。AQS是Java并发库中用于实现锁和同步器的重要基类。文章首先介绍了AQS的基本概念和工作原理,然后深入分析了AQS的核心方法和数据结构。通过对源码的逐行解析,文章解释了AQS如何使用内部的双向链表(CLH队列)来维护线程的等待队列,并通过状态变量来管理线程的获取和释放。此外,文章还涵盖了AQS的独占模式和共享模式的实现细节

AQS简介

在多线程编程中,AQS(AbstractQueuedSynchronizer)是一个重要的同步器框架。它提供了一种基于队列的方式来实现同步操作,使用了一个等待队列来管理线程的竞争和等待。

AQS的核心思想是通过一个整数状态变量(state)来表示共享资源的状态,并使用CAS(Compare and Swap)操作来实现对状态的原子更新。AQS提供了两种模式的同步器:独占模式(Exclusive mode)和共享模式(Shared mode)。

在AQS中,每个线程通过acquire操作来请求获取同步资源,如果资源已经被占用,则线程会进入等待队列并被阻塞。当资源释放时,AQS会从等待队列中选择一个或多个线程唤醒,并允许其继续执行。

AQS的具体实现是通过内部的Node节点和等待队列来实现的。每个等待获取同步资源的线程都会被封装成一个Node节点,并按照获取资源的顺序链接到等待队列中。如果一个线程成功获取到资源,则它会成为同步队列的头节点。

  1. 独占模式(Exclusive Mode):
    在独占模式下,只有一个线程能够获得资源的访问权,其他线程需要等待该线程释放资源才能继续访问。在Java并发库中,独占模式常用于实现互斥锁(Mutex)或者排他锁(Exclusive Lock),例如ReentrantLock类就是一种独占模式的锁。当一个线程获得独占锁后,其他线程会被阻塞,直到该线程释放锁。

  2. 共享模式(Shared Mode):
    在共享模式下,多个线程可以同时获得资源的访问权,它们可以并发地执行对资源的操作。共享模式常用于实现读写锁(ReadWrite Lock),其中读操作可以并发执行,而写操作需要独占资源。在Java并发库中,ReentrantReadWriteLock类提供了对读写锁的支持。在读写锁中,多个线程可以同时获取读锁,但只有一个线程可以获取写锁,读锁和写锁之间是互斥的,即写锁被持有时,其他线程不能获取读锁或写锁。

ReentrantLock

   每个ReentLock类中维护了一个继承自AbstractQueuedSynchronizer(aqs)的Sync类

   加锁过程: 

    1 首先线程调用lock方法

      

   final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

   线程尝试利用cas机制来修改aqs抽象类中的state字段,这是一个被volatile修饰的整形,当成功修饰后此线程不会被阻塞即视为获得锁

接下来假设还有线程要获得锁,那么就会进入acquire(1)方法

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

首先会再次尝试获取,假设失败后就会进入addWaiter方法,这个方法是利用链表创建了一个创建了队列,每次堵塞线程被设置为node的字段然后加入阻塞队列中

     

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

堵塞的实现主要在于parkAndCheckInterrupt(),其方法内部调用LockSuport.park()方法让当前线程阻塞

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

这里是阻塞的实现接下来是不可打断的实现

当其他地方调用当前线程的intereupte方法时,由于park方法可被打断就再次进入循环尝试获取锁,只用获取到锁时才会返回是否被打断过

假设途中被打断过,并且线程被unpark方法唤醒,那么就会在for(;;)循环中执行获取锁的方法,假设获取锁成功,被打断过的方法唯一不同的是返回的interrupted的值为true,然后执行上一级方法

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

,由于interrupted方法会把线程打断状态值为false,这里再次调用interrupt方法,标记打断状态,进而让用户自己做一些操作

可以看到只用得到锁的情况下才会执行,这就是为什么锁不可被打断

给定的cancelAcquire方法实现了在获取同步资源过程中取消节点(线程)的操作。以下是该方法的作用:

  1. 首先,如果传入的节点nodenull,表示节点不存在,直接返回,不进行任何操作。
  2. 将节点node的线程引用node.thread置为null,表示该节点对应的线程已被取消。
  3. 跳过已被取消的前驱节点,将节点node的前驱节点pred设置为最近一个状态不为CANCELLED的前驱节点。
  4. 获取前驱节点pred的后继节点predNext,这是一个暂时的后继节点,用于后续的操作。
  5. 将节点node的等待状态node.waitStatus设置为CANCELLED,表示节点已被取消。
  6. 如果节点node是尾节点(即node == tail),则尝试原子地将尾节点设置为前驱节点pred,如果成功则将前驱节点的下一个节点predNext设置为null,从而移除节点node
  7. 如果节点node不是尾节点,则检查前驱节点pred的等待状态pred.waitStatus,如果等待状态为SIGNAL或非正数(小于等于0),并且前驱节点的线程pred.thread不为null,则尝试将前驱节点的等待状态设置为SIGNAL,以确保后继节点会得到信号。如果设置成功,则检查节点node的下一个节点node.next,如果不为null并且等待状态node.next.waitStatus小于等于0,则尝试原子地将前驱节点的下一个节点predNext设置为节点node的下一个节点node.next
  8. 如果前述步骤都不满足,则调用unparkSuccessor(node)方法唤醒后继节点,以便传播信号。
  9. 最后,将节点node的下一个节点node.next设置为自身,用于帮助垃圾回收。

总体而言,cancelAcquire方法的作用是取消节点(线程)的获取同步资源的操作,并进行相应的处理,包括移除节点、传播信号或唤醒后继节点等。

可打断锁原理

 调用ReentLock的lockInterruptibly()方法 

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

   进一步查看doAcquireInterruptibly(arg);的源码和不可打断锁源码差不多

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

重点在于当被打断时这里是直接抛出异常,而不在方法内部捕获交由用户自己处理

重入锁

  当调用lock方法时会调用tryAcquire方法。

这里的getState就是获得aqs抽象类的state字段,如果是0就就说明当前没有线程获得锁,利用cas机制进行赋值(这个操作是原子性的),假设此时有线程再次加锁,就会判断当前线程和获得锁的线程是不是一致如果一致的话就会在state的基础上加一。这就是可重入锁的原理

公平锁和非公平锁

  非公平锁是指当阻塞队列中的线程被唤醒后(只唤醒一个),可能会有新的线程和它去竞争,这样就不保证先到先得到

  公平锁是指当线程被唤醒,只有排在阻塞队列最前方的线程才能获取锁

  非公平锁的实现

当调用lock方法后

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

会调用tryAcquire方法,这里有一个hasQueuedPredecessors方法,用来判断当前线程是不是位于头节点后的第二个线程,如果是就返回true,进而就不用进入阻塞,否则就进入阻塞队列。这就是公平锁的实现

条件变量: 

首先会创建一个Condition对象返回,本质还是创建利用链表创建队列

当调用条件变量的awit方法后

  public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int 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);
        }

首先创建新的node节点,接下来调用fullyRelease方法释放当前线程的锁(包括可重入锁),接下来进入阻塞

当调用signal方法后,会进而调用doSignal

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

首先就是要当前节点出队列,然后把当前节点利用transferForSignal方法转移到公平锁或者非公平锁的队列中

 
compareAndSetWaitStatus(node, Node.CONDITION, 0)把当前节点的状态设置为0

enq把当前节点加入阻塞队列并返回前一个节点接下来调用

compareAndSetWaitStatus(p, ws, Node.SIGNAL)把前一个节点的状态设置为-1,因为节点状态是-1同时表示要唤醒后面的节点

锁释放

  

    protected final boolean tryRelease(int releases) {
            //每次释放就让state的值减去1
            int c = getState() - releases;
            //如果不是当前线程调用的unlock方法就会抛出异常 
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果锁释放完毕就把state的值设置为0,并把当前线程的字段设置为null 
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

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

  (只有waitStatus不为0才可以唤醒下一个线程)接下来调用unparkSuccessor(h)方法

 

   private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }

唤醒队列中的头节点后的第一个节点。到这里就和公平锁和非公平锁联系了起来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值