高并发之JUC——关于Doug Lea公平锁与非公平锁设计思想与源码全面解析

高并发之JUC——关于Doug Lea公平锁与非公平锁设计思想与源码全面解析_第1张图片

在上两篇关于《高并发之JUC——AQS源码深度分析(一)》、《高并发之JUC——AQS源码深度分析,有你不得而知的条件等待队列(二)》文中介绍了Doug Lea设计的JUC高并发工具类AQS的设计思想及源码分析,同时也说明了AQS是整个JUC包其他工具类的基石。本文将介绍JUC工具包下面的首个实现类ReentrantLock,即公平锁和非公平锁。

应用示例

    public static void main(String[] args) throws Exception{

        // 重入锁,显示锁
        ReentrantLock reentrantLock = new ReentrantLock();
        // 条件队列。可调用signal方法来唤醒其他线程,相当于notify,可调用await方法阻塞当前线程,相当于wait。
        Condition condition = reentrantLock.newCondition();

        new Thread(()-> {
            // 加锁
            reentrantLock.lock();
            System.out.println(Thread.currentThread().getId());
            try {
                Thread.sleep(1000);
                // 唤醒其他线程,其他线程有机会获取到锁
                condition.signal();
                // 等待其他线程唤醒
                condition.await();
                System.out.println(Thread.currentThread().getId()+"执行完毕!");
                Thread.sleep(1000);
                condition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 解锁,其他线程可获取到锁
            reentrantLock.unlock();
        }).start();


        new Thread(()-> {
            // 加锁
            reentrantLock.lock();
            System.out.println(Thread.currentThread().getId());
            try {
                Thread.sleep(1000);
                // 唤醒其他线程,其他线程有机会获取到锁
                condition.signal();
                // 等待其他线程唤醒
                condition.await();
                System.out.println(Thread.currentThread().getId()+"执行完毕!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 解锁,其他线程可获取到锁
            reentrantLock.unlock();
        }).start();

    }

上面的小例子很简单,就是一个显示锁的应用小例子。其中lock即为加锁,作用等同于synchronized,signal为唤醒其他线程,相当于notify,await阻塞当前线程,即为wait。

输出:

10
11
10执行完毕!
11执行完毕!

非公平锁

默认ReentrantLock是非公平锁,即获取锁不是按照先到先得的流程,而是谁先抢到谁先占有。

NonfairSync.lock

NonfairSync即为非公平锁,其是ReentrantLock的内部类。

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // cas操作将重入锁次数设置为1,如果cas成功,那么当前线程拿到了这把锁。
            // 这块也就是非公平锁与公平锁不一致的地方,公平锁没有这个逻辑。因为非公平锁线程来了就会立马去获取锁,不会排队。
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 如果没有拿到这把锁,说明已经有线程持有这把锁
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            // 默认获取非公平锁
            return nonfairTryAcquire(acquires);
        }
    }

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            // 取得当前线程
            final Thread current = Thread.currentThread();
            // 取得当前锁重入次数
            int c = getState();
            // c=0说明当前没有线程拥有这把锁,此时快速通过cas获取这把锁
            if (c == 0) {
                // cas操作将重入次数更新为1
                if (compareAndSetState(0, acquires)) {
                    // 更新独占锁标示为当前线程,此时即为成功获取锁。
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果上述没有获取到锁,那么判断当前独占锁是不是当前线程(也就是当前持锁的线程是不是当前线程)
            else if (current == getExclusiveOwnerThread()) {
                // 如果持有锁的线程正好是当前线程,那么将锁重入标示+1。即拿到了这把锁。
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
     }

其中Sync实现了AQS。我们先来看lock流程。上面代码中关于lock的注释已经很清楚了:

  • 首先cas操作将state值将从0设置为1,如果成功,那么将exclusiveOwnerThread变量赋值为当前线程。成功即为获取到锁。
  • 否则执行acquire。acquire是AQS中已经实现了的方法,其中只需要子类实现其tryAcquire方法即可。所以我们就来看下tryAcquire方法,此方法其实就是调用了内部类Sync.nonfairTryAcquire方法。
  • nonfairTryAcquire方法中,首先还是用cas操作将state值将0设置成1,并更新当前exclusiveOwnerThread的值。cas操作成功则获取锁成功,反之执行下面流程。
  • 如果再次获取锁失败,那么会判断目前持有锁的线程是否为当前线程(因为ReentrantLock支持重入锁),如果为当前线程,那么将state+1,成功获取到锁,返回。否则获取锁失败。
NonfairSync.unlock

释放锁流程即调用AQS的release方法。可以参照我之前的两篇文章。

公平锁

了解了非公平锁,那么公平锁就很容易理解了。

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

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

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取重入次数
            int c = getState();
            if (c == 0) {
            	// 重点看hasQueuedPredecessors方法:这个方法就是判断队列中是否还有等待获取锁的节点,
                // 如果队列中还存在等待获取锁的节点,那么当前线程就不去竞争锁,获取锁的机会留给队列中等待的节点。其他处理和非公平锁是一致的。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

	// AQS中的方法
	public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        // 如果当前线程之前有一个排队的线程,则为{@code true},如果当前线程位于该队列的开头或该队列为空,则为{@code false}
        return h != t && // 存在等待节点
                // h.next == null说明恰巧有一个线程正在持有锁
                // 或者不只有一个等待节点,并且s.thread != Thread.currentThread():说明第一个等待节点所属线程不属于当前线程(也就是不满足重入锁条件)
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

上面注释相信已经很清楚了。公平锁与非公平锁获取锁的流程区别在于:公平锁获取锁之前会判断AQS等待队列中是否存在还在等待的节点,如果存在等待的节点,并且第一个等待获取锁的节点不是当前线程,那么返回true。

会不会觉得这块有些绕呢?为什么hasQueuedPredecessors方法能判断出存在比当前线程请求还早的线程呢,我们仔细想一下:

h != t:说明AQS等待队列中一定存在等待节点,那么此方法返回true,表示这个线程不能去获取锁;那么如果h=t,这个方法就直接返回false,表示这个线程可以获取锁(因为这个线程之前没有等待获取锁的线程了)。

那么h.next = null说明什么呢?说明恰巧此时有一个线程拿到了锁并且在执行线程,那么此时该方法返回true,这个线程不允许在去竞争锁;否则判断h.next.thread是否是当前请求的这个线程;如果是,那么该方法返回false,可以去获取锁。否则就不允许获取锁。

signal

signal:唤醒其他等待获取锁的线程。相当于notify。

AQS:

public final void signal() {
	// protected final boolean isHeldExclusively() {
    //    // While we must in general read state before owner,
   //     // we don't need to do so to check if current thread is owner
   //     return getExclusiveOwnerThread() == Thread.currentThread();
   // }
   // 判断是否是持有锁的线程,也就是说只有持有锁的线程才能唤醒其他线程;
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // firstWaiter:是AQS中条件等待队列中的头节点(上篇文章中提到过)。
    Node first = firstWaiter;
    // 头节点不等于空,将条件等待队列中的线程移至到AQS同步队列中,等待唤醒。
    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);
}

上面的注解已经很清楚了,signal做了以下操作:

  • 判断调用该方法的线程是不是持有锁的线程,不是则抛出异常;
  • 如果是持有锁的线程,那么从条件等待队列中取出首个等待节点,如果不等于空,那么依次将条件等待队列的节点写入到AQS同步队列,等待唤醒;
await
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);
 }

上面重点部分已经有注释进行了说明,await主要处理逻辑如下:

  • 将当前线程加入条件等待队列。
  • 释放当前线程持有的锁,唤醒同步队列中的首个等待队列。
  • 判断当前线程是不是在同步队列中,如果不在,那么就阻塞当前线程(一般都是不在的)。

signal和await转换处理

还是看本文最开始给出的signal和await使用的小例子,接下来描述下signal、await是如果在两个线程之间切换锁来交替执行两个不同线程中的代码:

1、线程A拿到锁,执行线程逻辑;
2、此时线程B申请锁,但申请失败,会加入到同步队列并阻塞;
3、此时如果A线程调用signal方法,将在条件等待队列中的线程移至到同步队列,但是此时不会有线程在条件队列中;
4、接着A线程调用await方法阻塞当前线程,在await方法中会将A线程加入到条件等待队列,并释放当前线程的锁,唤醒同步队列中的线程,也就是B线程被唤醒,并阻塞当前线程A。
5、接着线程B被唤醒,并且获取到锁,执行线程B中的逻辑。
6、当B线程调用signal方法后,将条件队列中的线程移至到同步队列,也就是将线程B移至到同步队列。
7、当B线程调用await方法后,会将B线程加入到条件等待队列,并释放锁,唤醒同步队列中的线程A,并阻塞当前线程,此时线程A继续执行。
8、一旦线程A执行完毕,需调用signal方法后(将线程B从条件等待队列中移至到同步队列),然后调用unlock释放锁并唤醒线程B,线程B就可以继续执行了,直到执行结束。

最后

本文主要回顾了Doug Lea公平锁与非公平锁设计思路与源码,下一篇将会继续带来JUC中门闩及信号量相关的设计思路与源码分析。如果本文对您还有一点点价值,不要忘了关注、点赞、分享。

你可能感兴趣的:(多线程高并发)