07、ReentrantLock锁机制原理/源码分析

一、Lock原理

以ReentrantLock为例,其是基于AQS实现的可重入锁。

  1. ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,Sync类继承了AbstractQueuedSynchronizer。
  2. Sync有两个子类:NonfairSync(非公平锁、提高了吞吐量)、FairSync(公平锁)。
  • 公平锁:利用阻塞队列FIFO的特性、先到先得
  • 非公平锁:线程节点加到阻塞队列之前,不管是否有线程排队,先tryAcquire()抢一下锁试试。
  • 默认选择非公平锁,因为通过大量的测试下来,发现非公平锁的性能优于公平锁。

 

获取锁流程和释放锁流程建议结合下面的源码分析一起理解;此处是做的一个总述。

1、lock()获取锁流程

首先,直接调用AQS实现类Sync的lock()方法,由于ReentrantLock有公平锁和非公平锁之分,所以这里sync.lock()有两种实现:

非公平锁:NonfairSync#lock()

  1. 首先通过CAS操作尝试将state从0变成1,如果成功,则将当前线程设置为锁的持有者,快速返回;
  2. 只有当存在锁重入、锁竞争的时候,才会进入到acquire()方法,尝试去获取锁。而acquire()方法是AQS提供的模板方法,ReentrantLock中只需实现tryAcquire()方法,自定义获取锁的逻辑即可;即:NonFairSync#tryAcquire()方法用来判断锁是否可以获取成功,
  • 如果state==0表示没有线程正在竞争该锁,通过CAS将state状态值设置为1,并将当前线程设置为锁的持有者。
  • 如果state!=0说明已经有线程已经拥有了该锁;

如果是当前线程就是锁的持有者,则直接state++;
否者,如果是别的线程持有锁,则tryAcquire()方法返回false;

  • tryAcquire()方法返回false表示获取锁失败,进入到AQS的线程阻塞入队逻辑,其中包括:

调用addWriter()方法就会把线程包装为一个Node节点添加到等待队列队尾。
接下来**acquireQueued()方法将已经追加到队列的线程节点(即addWriter()的返回值)进行阻塞;

  • 阻塞前会调用tryAccquire()尝试是否能再次获得锁。

公平锁:FairSync#lock()

和非公平锁逻辑基本一致,有两个不同点;

  1. 直接进入AQS的acquire()方法尝试获取锁,不会先尝试CAS将state从0改为1。
  2. 在FairSync#tryAcquire()方法中,及时某个时间点没有线程锁,也会通过hasQueuedPredecessors()方法去判断CLH队列中有没有线程在排队?
  • 如果有,并且当前线程没有持有锁,直接进入到AQS的CLH队列。
  • 否则和非公平锁一个逻辑。

2、unlock()释放锁流程:

直接进入到AQS的release()方法,不论是公平锁还是非公平锁,针对release()模板方法,Sync只提供一个tryRelease()方法的实现,用于判断锁是否可以成功释放:

  • 如果释放锁的线程不是锁的持有者,直接抛出异常IllegalMonitorStateException;
  • 否者,修改状态值state,做state–操作,当state=0时释放锁、清空锁的持有者信息。
  • 此外,由于释放锁不存在竞争,所有没有采用CAS修改state。

3、面试题2:为什么非公平锁的性能优于公平锁?

  1. 因为公平锁需要两个时间:阻塞队列中排队、线程唤醒;所以经过线程的上下文切换之后 再加到 运行队列中有一个延时的过程:
  • 阻塞队列中的线程状态要先转换为RUNNING就绪状态、放到运行队列去;
  • 然后再根据优先级、调度算法调度执行起来。
  • 也就是说公平锁有线程上下文切换时间 + 线程调度的延迟时间
  1. 而非公平锁,直接上来就去争抢线程调度的资源。

二、ReentrantLock源码分析

1、变量和构造器

NonfairSync和FairSync有很多共用逻辑,所以抽象出一个Sync类。

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    // 默认是非公平锁
    public ReentrantLock() {

        sync = new NonfairSync();
    }

    // true 表示公平锁
    public ReentrantLock(boolean fair) {

        sync = fair ? new FairSync() : new NonfairSync();
    }
}

公平锁?

阻塞队列FIFO、先到先得

非公平锁?

线程节点加到阻塞队列之前,不管是否有线程排队,先tryAcquire()抢一下锁试试。

默认是非公平锁

2、lock()加锁

通过组合的Sync类实现加锁功能;

public void lock() {

    // 直接通过sync的lock方法上锁
    sync.lock();
}

而Sync包含两个子类NonfairSync、FairSync,分别表示非公平锁和公平锁。

1)非公平锁的加锁

static final class NonfairSync extends Sync {

    // 由ReentrantLock调用获取锁
    final void lock() {

        // 非公平锁,直接抢锁,不管有没有线程排队
        if (compareAndSetState(0, 1))
            // 上锁成功,那么标识当前线程为获取锁的线程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 抢锁失败,进入AQS的标准抢锁逻辑
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {

        // 使用父类提供的获取非公平锁
        return nonfairTryAcquire(acquires);
    }
}

Sync中非公平的获取锁

abstract static class Sync extends AbstractQueuedSynchronizer {

    // 非公平锁获取锁的方法
    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;
            // 超过了int的表示范围,表明符号溢出,抛出异常。
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 返回false,表示需要AQS来将当前线程放入阻塞队列,然后进行阻塞操作等待其他线程唤醒
        return false;
    }
}

2)公平锁的加锁

static final class FairSync extends Sync {

    final void lock() {

        // 没有尝试抢锁,直接进入AQS标准获取锁流程
        acquire(1);
    }

    // AQS调用,子类自己实现获取锁的逻辑
    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;
        }
        // 返回false,表示需要AQS来将当前线程放入阻塞队列,然后进行阻塞操作等待其他线程唤醒
        return false;
    }
}

3、unlock()释放锁

主要体现在AQS.release()方法和Sync.tryRelease()方法中,修改状态值让它自减,state=0时释放锁(此处由于没有竞争出现,不会使用CAS),调整等待链表。

释放锁的时候不区分是公平锁还是非公平锁,并且整个实现过程中,Lock大量使用CAS+自旋。

1> 释放锁:

public void unlock() {

    sync.release(1);
}

由于Sync是AQS的子类,进入到AQS的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;
}

Sync类中tryRelease()对AQS的模板方法实现:

abstract static class Sync extends AbstractQueuedSynchronizer {

    // 释放锁操作
    protected final boolean tryRelease(int releases) {

        int c = getState() - releases;
        // 如果当前线程不是上锁的那个线程
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;

        // 不是重入锁,当前线程已经释放了锁。我们把AQS用于记录持有锁对象线程的变量exclusiveOwnerThread设置为null,表明释放锁成功。
        if (c == 0) {

            free = true;
            setExclusiveOwnerThread(null);
        }
        // 在setState之前,state全局变量并没有改变;也就意味着在setState之前,没有别的线程能获取锁,保证了上面2行操作的原子性。
        setState(c);

        // 告诉AQS,当前锁释放成功,可以去唤醒正在等待获取锁的线程了。
        return free;
    }
}