AQS之ReentrantLock详解

AQS之ReentrantLock详解

  • 一、ReentrantLock类的继承关系
    • 1. AbstractQueuedSynchronizer:提供了一个同步器的框架。
    • 2. Lock:提供了锁的基本功能,包括加锁、解锁等。
    • 3. Sync:ReentrantLock的内部类,提供了锁的具体实现。
    • 4. FairSync:Sync的子类,实现公平锁。
    • 5. NonfairSync:Sync的子类,实现非公平锁。
  • 二、ReentrantLock的基本使用
    • 1、创建ReentrantLock对象
    • 2、获取锁
    • 3、尝试获取锁
    • 4、获取锁的同时设置超时时间
  • 三、ReentrantLock的优缺点
    • 1. 优点:
      • 1.1、可重入性:
      • 1.2、公平性:
      • 1.3、可中断性:
      • 1.4、高度灵活:
      • 1.5、高性能:
    • 2. 缺点:
      • 2.1、使用复杂:
      • 2.2、可能出现死锁:
      • 2.3、不支持隐式锁:
  • 四、源码分析
    • 1. 构造方法
    • 2. lock方法
      • 2.1. NonfairSync#lock
      • 2.2 AQS#acquire
      • 2.3 NonfairSync#nonfairTryAcquire
      • 2.4 AQS#addWaiter
      • 2.5 AQS#addWaiter#enq
      • 2.6 AQS#acquireQueued
      • 2.7 AQS#shouldParkAfterFailedAcquire
      • 2.8 AQS#parkAndCheckInterrupt
      • 2.9 扩展点:阻塞为啥要用LockSupport.park(this),wait、sleep不行么
      • 2.10. lock方法流程图:
    • 3. unlock方法
      • 3.1. unlock方法流程图:

一、ReentrantLock类的继承关系

AQS之ReentrantLock详解_第1张图片

1. AbstractQueuedSynchronizer:提供了一个同步器的框架。

  1. AbstractQueuedSynchronizer提供了一个同步器的框架,可以基于它来实现各种锁和同步器。它是ReentrantLock的基础实现,ReentrantLock中的锁和同步器都是基于AbstractQueuedSynchronizer来实现的。在实现锁和同步器时,需要继承AbstractQueuedSynchronizer类,并实现它的一些抽象方法。

  2. AbstractQueuedSynchronizer的主要作用如下:
    2.1 提供了一个同步器的框架,可以基于它来实现各种锁和同步器。
    2.2 通过state变量来记录锁的状态,包括锁的持有者、锁的状态等。
    2.3 提供了基于FIFO队列的线程阻塞机制,实现了线程的等待和唤醒。

2. Lock:提供了锁的基本功能,包括加锁、解锁等。

  1. Lock是一个接口,提供了锁的基本功能,包括加锁、解锁等。Lock接口定义了以下几个方法:
    1.1 lock():获取锁。
    1.2 unlock():释放锁。
    1.3 tryLock():尝试获取锁,如果锁可用,则获取锁并返回true,否则返回false。
    1.4 tryLock(long timeout, TimeUnit unit):在指定的时间内尝试获取锁,如果锁可用,则获取锁并返回true,否则返回false。
    1.5 newCondition():返回一个与该锁绑定的Condition对象,用于线程之间的通信。

3. Sync:ReentrantLock的内部类,提供了锁的具体实现。

  1. Sync是ReentrantLock的内部类,提供了锁的具体实现。Sync包含了FairSync和NoFairSync两个子类,分别实现了公平锁和非公平锁。
  2. Sync的主要作用如下:
    2.1 维护锁的状态,包括锁的持有者、锁的状态等。
    2.2 实现加锁和解锁的具体逻辑。
    2.3 提供了可重入锁的功能。

4. FairSync:Sync的子类,实现公平锁。

  1. FairSync是Sync的子类,实现公平锁。公平锁指的是锁的获取按照请求的顺序进行,先请求的先获得锁,避免线程饥饿现象。FairSync实现了公平锁的逻辑,它在锁的获取过程中,会先检查队列中是否有等待的线程,如果有,则先释放锁,让等待的线程获取锁。

  2. FairSync的主要作用是实现公平锁的逻辑。

5. NonfairSync:Sync的子类,实现非公平锁。

  1. NoFairSync是Sync的子类,实现非公平锁。非公平锁指的是锁的获取不按照请求的顺序进行,先请求的不一定先获得锁,可能会出现线程饥饿现象。NoFairSync实现了非公平锁的逻辑,它在锁的获取过程中,直接尝试获取锁,如果获取失败,则进入等待队列。
  2. NoFairSync的主要作用是实现非公平锁的逻辑。

二、ReentrantLock的基本使用

ReentrantLock是Java中的一种锁机制,支持重入锁。重入锁指的是同一个线程可以多次获得同一把锁,而不会出现死锁等问题。

1、创建ReentrantLock对象

ReentrantLock lock = new ReentrantLock();

2、获取锁

lock.lock();
try {
    // 执行需要加锁的代码块
} finally {
    lock.unlock(); // 记得释放锁
}

3、尝试获取锁

if (lock.tryLock()) {
    try {
        // 执行需要加锁的代码块
    } finally {
        lock.unlock(); // 记得释放锁
    }
} else {
    // 获取锁失败,处理逻辑
}

4、获取锁的同时设置超时时间

if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
    try {
        // 执行需要加锁的代码块
    } finally {
        lock.unlock(); // 记得释放锁
    }
} else {
    // 获取锁失败,处理逻辑
}

三、ReentrantLock的优缺点

ReentrantLock是Java中的一种锁机制,相比于synchronized关键字,它有如下的优缺点:

1. 优点:

1.1、可重入性:

同一个线程可以多次获得同一把锁,避免了死锁等问题

1.2、公平性:

支持公平锁和非公平锁,可以根据情况选择

1.3、可中断性:

支持线程中断,即使线程已经获得了锁,也可以响应中断

1.4、高度灵活:

支持多种锁操作,如tryLock()、tryLock(long time, TimeUnit unit)等,可以根据情况选择

1.5、高性能:

相比于synchronized关键字,ReentrantLock的性能更好,特别是在高并发的情况下。

2. 缺点:

2.1、使用复杂:

相比于synchronized关键字使用简单,ReentrantLock的使用更加复杂,需要手动获取锁和释放锁

2.2、可能出现死锁:

如果使用不当,仍然可能出现死锁等问题

2.3、不支持隐式锁:

相比于synchronized关键字,ReentrantLock不支持隐式锁,需要手动获取锁和释放锁。

四、源码分析

1. 构造方法

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  1. 由构造方法可知,ReentrantLock默认为非公平锁。
  2. 当然也可以根据传入参数fair设置公平非公平。

2. lock方法

当我们在用ReentrantLock独占锁的时候,如果不指定公平非公平,那默认是非公平的

public void lock() {
    sync.lock();
}

这里以非公平为例:
非公平
当我们在外部调用lock()方法的时候会进入ReentrantLock内部的加锁lock方法,其中sync是ReentrantLock的内部类,sync直接继承了AbstractQueuedSynchronizer(AQS)。
AQS之ReentrantLock详解_第2张图片

2.1. NonfairSync#lock

代码调用lock会来到ReentrantLock 内部类NonfairSync的lock()方法,如下:
AQS之ReentrantLock详解_第3张图片

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

lock 内部,一上来直接CAS操作AQS内部的一个state变量,尝试从 0 修改为1
如果修改成功,则获取锁。

2.2 AQS#acquire

否则 else 调用 AQS 内部的 acquire (模板)方法:

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

1.acquire方法内部会调用tryAcquire方法,点击去会来到AQS内部的tryAcquire,这个方法没有在内部实现,是为了让子类(ReentrantLock )去实现,模板方法模式的体现。
2. 这里又会回到ReentrantLock 的内部类NonfairSync的tryAcquire方法:

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

2.3 NonfairSync#nonfairTryAcquire

接着调用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;
}

该方法大致逻辑:
1. 获取AQS内部的state变量,如果等于0说明没有线程获取锁,继续CAS尝试获取锁,如果成功返回true。(如果是公平锁,这里需要判断队列里是否有阻塞的线程等着,入队前)
2. 如果不等于0,则判断当前线程current是否等于已经获取锁的线程,如果等于则+1,返回true,这里就是重入锁的逻辑。
3. 如果CAS即没有获取锁,同时也不是重入锁,则返回false。
到这里,返回false,会回到最初调用tryAcquire(AQS)方法的地方:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //如果acquireQueued返回true,说明阻塞线程在等待的过程中被中断,因为线程被唤醒没有获取到锁需要继续阻塞,导致parkAndCheckInterrupt内部调用了Thread.interrupted()方法给中断标志清除了,所以这里需要把中断标志重新恢复过来
        selfInterrupt();
}

这里是竞争共享资源失败需要入队等待唤醒的逻辑,首先调用addWaiter(Node.EXCLUSIVE)方法将当前线程封装成一个Node结点。
addWaiter方法的代码如下:

2.4 AQS#addWaiter

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    //将pred指针指向队列尾结点
    if (pred != null) {
        //如果不等于null,说明队列有等待线程
        node.prev = pred;
        //修正当前node结点的前驱指针,指向队列的尾结点
        //CAS尝试将当前结点置为尾结点
        if (compareAndSetTail(pred, node)) {
            //修正队列尾结点的后继指针,指向当前结点
            pred.next = node;
            //当前结点入队成功则返回
            return node;
        }
    }
    //代码走到这里说明,队列里没有阻塞的线程
    enq(node);
    return node;
}

addWaiter方法内部涉及到enq(创建队列,队里的初始化操作)

2.5 AQS#addWaiter#enq

private Node enq(final Node node) {
    //这里是CAS+自旋机制,保证当前结点一定要入队成功,因为存在并发的情况
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //这里是给队列进行初始化,构造头结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //这里是重点,线程入队步骤,如果这几步调换是会存在安全问题的,这3步顺序不能改变
            node.prev = t;
            //1.将当前node结点的前驱指针,指向队列的尾结点
            if (compareAndSetTail(t, node)) {
                //2. CAS尝试将当前结点置为尾结点
                //3. 修正队列尾结点的后继指针,指向当前结点
                t.next = node;
                return t;
            }
        }
    }
}

enq方法:for循环+CAS机制,保证结点一定要入队成功。
注意入队三步骤:(尾插法)
1.将当前node结点的前驱指针,指向队列的尾结点
2. CAS尝试将当前结点置为尾结点
3. 修正队列尾结点的后继指针,指向当前结点

addWaiter方法结束回到acquireQueued方法处:

2.6 AQS#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);
                //释放原来的头结点,断点指针,让gc来进行回收 
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //到这里说明 当前结点的前驱结点不是头结点 或者 前驱结点是头结点但是没有拿到锁
            //说明前驱结点是SIGNAL状态,那就调用park阻塞
            //判断是不是需要park,并且梳理一下指针的指向关系
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //记录阻塞线程在等待获取锁的过程中有没有被中断
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


//设置头结点
private void setHead(Node node) {
    //这里因为只有一个结点会进来,所以不需要CAS
    head = node;
    node.thread = null;
    node.prev = null;
}

acquireQueued方法 :大致逻辑是阻塞线程入队后获取前驱结点

  1. 如果前驱结点是头结点 出于性能考虑,会再次尝试获取锁
  2. 如果抢到锁了,将当前结点设置成头结点
  3. 判断shouldParkAfterFailedAcquire是不是需要park
  4. 需要park则调用parkAndCheckInterrupt方法
  5. 到这里说明 当前结点的前驱结点不是头结点 或者 前驱结点是头结点但是没有拿到锁
  6. 执行shouldParkAfterFailedAcquire方法:
    将当前结点的前驱结点的waitStatus状态设置为SIGNAL(表示后续结点需要被唤醒)

2.7 AQS#shouldParkAfterFailedAcquire

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        //前驱结点状态是 CANCELLED 状态需要被取消
        //意思就是没有必要在 CANCELLED 结点后面等着了,前驱结点不会进行通知当前结点,那就往前面找,直到不大于0,也就是不是取消 CANCELLED 状态的
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

最后来到parkAndCheckInterrupt方法:

2.8 AQS#parkAndCheckInterrupt

private final boolean parkAndCheckInterrupt() {
    //将当前线程进行阻塞,让出CPU执行权
    LockSupport.park(this);
    //返回中断标志位,并重置为false
    //如果有其他线程调用interrupt方法将其唤醒,当前线程如果获取不到锁,仍然需要阻塞,所以这里需要通过interrupted方法清除中断标志,如果不清除线程会一直在这里自旋,耗费CPU资源
    return Thread.interrupted();
}

调用LockSupport.park(this),将当前线程挂起,进行阻塞。

2.9 扩展点:阻塞为啥要用LockSupport.park(this),wait、sleep不行么

  1. 调用wait需要释放锁,而释放锁的前提是当前线程得持有锁,没拿到锁就不能释放 wait需要和notify配合使用
  2. 而且notify不能指定唤醒的线程

2.10. lock方法流程图:

AQS之ReentrantLock详解_第4张图片

3. unlock方法

当我们在外部调用lock.unlock()方法的时候,代码如下:

 public void unlock() {
        sync.release(1);
 }

可以看到方法里会通过sync内部类调用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;
}

而release方法内部又会调用子类的tryRelease方法(ReentrantLock):

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

protected final void setState(int newState) {
    state = newState;
}

tryRelease方法大致的执行过程:

  1. 获取AQS内部的state变量,并将其-1
  2. 为了代码的健壮性判断当前解锁线程是不是获取锁的线程(防御性编程)
  3. 如果等于0,将exclusiveOwnerThread属性置为null
  4. 修改state变量值(注意这里没有CAS,因为独占锁只有一个线程能获取到锁)
    执行完tryRelease方法,会判断如果队列的头结点不等于null并且waitStatus不等于0,就到了unparkSuccessor方法:
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);
}

该方法大致过程如下:

  1. 恢复头结点head的waitStatus 状态 compareAndSetWaitStatus(node, ws, 0)
  2. 获取头结点的下一个结点Node s = node.next 如果等于null 或者 waitStatus大于0
  3. 从tail结点开始往前遍历,找到最前边的waitStatus 小于等于0的结点
  4. 找到了对应的node结点,调用LockSupport类的unpark方法唤醒线程的阻塞。

3.1. unlock方法流程图:

AQS之ReentrantLock详解_第5张图片

你可能感兴趣的:(《并发编程》专栏,java)