【并发编程】 深入理解ReentrantLock

249 阅读5分钟

1. ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized,是一种可重入互斥锁。相比于 synchronized 自动加锁和解锁,ReentrantLock 提供了更加灵活的加解锁操作,同时支持加锁的公平性。

理解ReentrantLock需要先理解AQS,理解AQS需要先了解自旋锁、LockSuport类、CAS算法、双向队列。

1.1 自旋锁

当一个线程尝试区获取锁时,发现锁已经被其他线程占用,那么该线程将循环去尝试获取锁,直到正常占用锁。

举个通俗的例子,当洲洲去网吧上网,问网管:有机子没?然后网管头也不回就告诉洲洲:没有要等。然后洲洲每隔一段时间【自旋】 就会问网管:有机子没?直到她网管我:有了!然后洲洲才能上网。本来大家都在排队,挨个挨个上网【公平锁】。这个时候网管说:我们3点就关门了,你们要上网的快点!大家就疯了,有个大个儿占着身高体胖,把原来洲洲等到的电脑 抢占了 【非公平锁】

1.2 LockSuport类

LockSuport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞和唤醒。
通过 LockSuport.park / LockSuport.unpark 可以操作线程阻塞和唤醒。
LockSuport.park 都不会释放当前线程占有的锁资源。

1.3 CAS算法

CAS (Compare And Swap)比较并交换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。CAS 有效地说明了“ 我认为位置 V 应该包含值 A,如果真的包含A值,则将 B 放到这个位置,否则,不要更改该位置,只告诉我这个位置现在的值(A)即可。 ”整个比较并交换的操作是原子操作。

1.4 双向队列

队列中的每个元素都维护了preNode(前驱节点)、nextNode(后置节点)和其他数据信息。

1.5 AQS简介

AQS全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS在多线程并发执行的情况下:当一个线程已经占用了AQS,其他线程将在队列中等待,并通过自旋实时检查(CAS算法尝试加锁)锁是否被释放,当然由于自旋比较消耗性能,所以通过LockSuport类定点阻塞和唤醒。

2. ReentrantLock 该如何使用

延续使用 synchronized 这篇博文中的例子,以便两者对比

    public static volatile int num = 0;
    public static ReentrantLock lock = new ReentrantLock(true);
    public static void main(String[] args) throws InterruptedException {
        Java j = new Java();
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                lock.lock();
                try{
                    j.getNumStatic();
                }finally {
                    lock.unlock();
                }
            });
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(num);
    }

    public int getNumStatic(){
        for (int i1 = 0; i1 < 1000; i1++) {
            num++;
        }
        return num;
    }

在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

  • 如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
  • 如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
  • 在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。

3. ReentrantLock 与 AQS

ReentrantLock如何实现synchronized不具备的公平与非公平性呢?

在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized(AQS),对该抽象类的部分方法做了实现,同时定义了两个字类分别实现了公平和不公平加锁逻辑。

  • FairSync 公平锁
  • NonfairSync 非公平锁

我们将 基于FairSync公平锁 分析 ReentrantLock 是如何完成加/解锁操作。

3.1 FairSync 公平锁加锁源码分析

3.1.1 源码时序图

3.1.2 源码分析

ReentrantLock.lock()方法会根据初始化 ReentrantLock 时传入的参数,调用 FairSync.lock() 或者 NonfairSync.lock() ,本文分析的是公平锁(FairSync.lock())

FairSync.lock()方法
   static final class FairSync extends Sync {
        // 锁的入口
        final void lock() {
            acquire(1);
        }
        ...
        ...
    }

FairSync.lock() 调用AQS.acquire()方法

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

AQS.acquire()先调用FairSync.tryAcquire()方法尝试加锁。

  • 如果加锁成功,返回true。加锁成功---等待unlock().
  • 如果加锁失败,通过acquireQueued(addWaiter(Node.EXCLUSIVE), arg),将当前线程加入队列等待。
tryAcquire 是如何加锁的呢?
static final class FairSync extends Sync {
        ...
        ....
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            /**
             * 当第一个线程没有被unlock时,第二个线程进入,此时state = 1(第一个线程修改未还原)。
             */
            int c = getState();
            if (c == 0) {
                /**
                 * 第一个线程访问代码时,进入这里:hasQueuedPredecessors()方法判断队列是否为空或者当前线程是否为队列第二位【第一位永远为null的node节点】。
                 * 通过CAS 尝试覆写AQS.State状态码 0 -> 1
                 * 修改ExclusiveOwnerThread为当前线程。
                 */
                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;
        }
    }

多线程抢占的情况下

  1. 第一个线程执行时:
    • 第一个线程访问由于AQS.state还没有被初始化,所以默认是 0 ,进入if()内部逻辑,
    • 首先通过 hasQueuedPredecessors() 方法判断 AQS 队列是否为空或者当前线程是否为队列第二位【第一位永远为null的node节点】。(由于是第一个访问的线程所以队列必为空)
    • 通过 compareAndSetState(0, acquires) 以 CAS算法 将AQS.state设置为1(表示线程抢占成功),同时将AQS.ExclusiveOwnerThread 设置为当前线程。(将当前线程设置为 AQS的线程拥有者)
  2. 第一个线程没有释放,其他线程进入时
    • 第一个抢占锁的线程没有释放,state=1,直接返回false
  3. 第一个线程没有释放,再次加锁
    • 可重入性:会对state+1,解锁时,每次释放锁 —1
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

如果抢占锁失败,会先执行 addWaiter 将锁加入队列,然后通过 acquireQueued 进行自旋。

addWaiter

private Node addWaiter(Node mode) {
        /**
         * 该方法会在队列尾部追加一个 node()
         * node 包含 线程信息
         */
        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;
            }
        }
        /**
         * 如果发现是第一个加入队列的node节点。会先创建一个空的node节点做为head节点,然后追加新增的节点。
         */
        enq(node);
        return node;
    }

如果是第一个加入队列的node节点,会先创建一个空的node节点做为head节点,然后追加新增的节点。 否则会在尾部直接追加。
追加方式:将之前的尾部node节点的next属性设置为当前node节点,将当前node节点的pred属性设置为尾部节点。同时将全局对象tail设置为新的尾部node节点。

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;
                }
                /**
                 * shouldParkAfterFailedAcquire()先把前驱节点的 waitStatus 设置为 -1(// todo 上一线程结束时unlock时有用)
                 * 然后通过parkAndCheckInterrupt()将线程 LockSupport.park(this);将线程阻塞。等待唤醒
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在该方法中通过自旋检查判断是否是队列中的第二节点如果是,会尝试进行加锁(CAS算法尝试加锁),如果加锁失败,则根据waitStatus决定是否需要挂起线程(通过 LockSupport.park(this) 将线程阻塞)。 最后通过cancelAcquire取消获得锁的操作

shouldParkAfterFailedAcquire

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 当前节点的前驱节点的 waitStatus = SIGNAL 时,表示当前节点是一个等待被唤醒的线程
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
        	// ws > 0 说明前驱节点已经失效了,将当前节点追加到 前驱的前驱节点上。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	// 如果前驱节点的waitStatus < 0 将它设置为singal
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

把前驱节点的 waitStatus 设置为 SIGNAL,在前驱节点释放后唤醒下一线程时,会进行校验。

parkAndCheckInterrupt

    private final boolean parkAndCheckInterrupt() {
        // 阻塞当前线程
        LockSupport.park(this);
        return Thread.interrupted();
    }

3.2 FairSync 公平锁解锁源码分析

公平锁的解锁过程相对比较简单,只做了两件事情

  • 当前线程解锁
  • 下一线程唤醒
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            // 当前线程解锁成功,再唤醒下一节点。
            Node h = head;
            if (h != null && h.waitStatus != 0) // 这里进行了判断,在阻塞前将上一节点的waitStatus设置为single。
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease --- 当前线程解锁

protected final boolean tryRelease(int releases) {
      /**
       * 释放锁:由于可重入性的原因,需要将State每次重入,被累加
       * 所以每次释放需要累减。
       * 直到state = 0 ,才将锁正式释放
       */
      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;
}

unparkSuccessor --- 下一线程唤醒

    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;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

此处的入参node是头节点,然后唤醒下一节点

总结

通过这篇文章基本将AQS队列的实现过程做了比较清晰的分析,主要是基于公平锁的独占锁实现。在获得同步锁时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

最后分享一个简单AQS实现便于理解
/**
* 手写一个基础版的AQS
* 公平锁
*/
public class AQS {
    /**
     * 锁状态:用于校验锁是否被占用
     */
    private static volatile int state = 0;
    /**
     * 锁的真实持有者
     */
    private static Thread LockHolder;
    /**
     * 没能抢占的线程 需要加入队列中等待
     * 由于是多线程环境下,这个队列需要是线程安全的
     * ConcurrentLinkedQueue:非阻塞,构造方法中没有长度,无法限制长度
     */
    private static ConcurrentLinkedQueue<Thread> queue = new ConcurrentLinkedQueue();
    /**
     * Unsafe.getUnsafe(); 这样实例化 UnSafe 方法会抛出异常 SecurityException
     * getUnsafe 方法中会判断 是否为Bootstrap 类加载器调用,所以必须使用反射去实例化
     */
    private static final Unsafe unSafe = UnSafeInit.initUnSafe();
    /**
     * 初始化时需要从内存中拿到状态值
     * 内存中保存的是 AQS 对象,如果不通过 jvm 直接获取state属性,需要先获取漂移量(表示改变量在内存中,相对于变量的位置)
     */
    private static final long stateOffset;

    static {
        try {
            stateOffset = unSafe.objectFieldOffset(AQS.class.getDeclaredField("state"));
        } catch (Exception ex) {
            throw new Error();
        }
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

    public static Thread getLockHolder() {
        return LockHolder;
    }

    public static void setLockHolder(Thread lockHolder) {
        LockHolder = lockHolder;
    }

    /**
     * 比较并赋值,如果期望值和真实值一致 则更新并返回true,否则不更新并返回false
     * @param expect
     * @param update
     * @return
     */
    protected final boolean compareAndSetState(int expect, int update) {
        return unSafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    /**
     * 线程尝试去加锁
     * @return
     */
    public boolean acquire(){
        Thread thread = Thread.currentThread();
        //锁定状态
        int lockState = 1;
        //如果初始状态等于0 说明锁没有被占有 CAS算法
        if(compareAndSetState(0,lockState)){
            //设置锁的持有者
            setLockHolder(thread);
            return true;
        }
        return false;
    }
    /**
     * 加锁
     */
    public void lock(){
        Thread thread = Thread.currentThread();
        //如果队列是空的或者你是队列的第一个,你可以去尝试加锁,否则排队
        if(queue.isEmpty()||thread==queue.peek()){
            //尝试获取锁
            if(acquire()){ //1
                return;
            }
        }
        //没有加锁成功需要将对应的 Thread ,加入队列并循环进行阻塞
        queue.add(thread);
        for(;;){
            if(thread==queue.peek()&&acquire()){ //2
                //将锁从队列里移除
                queue.poll();
                return;
            }
            //阻塞线程
            LockSupport.park();
        }
    }
    /**
     * 解锁操作
     */
    public void unLock(){
        Thread thread = Thread.currentThread();
        //判断当前线程是否是锁持有者,是则解锁
        if(thread == getLockHolder()){
            int unLockState = 0;
            if(compareAndSetState(1,unLockState)){
                //设置锁的持有者为空
                setLockHolder(null);
                //唤醒队列中下一个需要加锁的线程
                Thread peek = queue.peek();
                if(peek!=null){
                    //下一个线程唤醒
                    LockSupport.unpark(peek);
                }
            }
        }
    }
}