Java ReentrantLock 原理

Java ReentrantLock 原理

ReentrantLock是Java5引入的可重入锁,Lock的实现类,相比synchronized它提供更精细的同步操作,高竞争场景表现好

主要有如下几个特点:

  • 可以设置公平性,设置后会倾向于将锁赋予等待时间最久的线程,减少线程饥渴
  • 具备尝试非阻塞地获取锁,且可选超时
  • 可以判断是否有线程或某个特定线程,在排队等待获取锁
  • 可以响应中断请求,获取到锁的线程能够响应中断
  • 提供条件变量(Condition)来控制线程,通过它的signal/await方法实现线程唤醒与等待

使用方式

ReentrantLock只适用于代码块,以线程作为同步单位,需要显式进行获取与释放锁

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
  // do something
} finally {
   lock.unlock();
}

有两点要注意:

  • 获取锁操作不建议在try内,避免在获取锁时抛出异常导致其他已获得锁的线程无故被释放锁
  • 释放锁操作必须在finally中,避免在获取锁后的同步代码中抛出异常导致死锁

AQS

ReentrantLock是基于AQS(AbstractQueuedSynchronizer)实现的,AQS是Java并发包中,实现各种同步结构和部分其他组成单元(如线程池中的Worker)的基础,它将基础的同步相关操作抽象了出来

AQS内部数据和方法,分为以下几类:

  • state状态,使用volatile修饰的int型变量,0表示未加锁状态,1表示已加锁状态

    private volatile int state;
    
  • 等待队列,基于双链表的FIFO队列,与waitStatus配合实现多线程间竞争和等待

    static final class Node {
        volatile int waitStatus;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        ...
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        ...
    }
    private transient volatile Node head;
    private transient volatile Node tail;
    
  • 各种基于CAS的基础操作方法,如CAS操作state状态或等待队列节点,以及各种用于同步的基础功能方法

    protected final boolean compareAndSetState(int expect, int update) {...}
    public final void acquire(int arg)
    public final boolean release(int arg)
    ...
    

对于CAS实现方式不熟悉的可以参考Java AtomicInteger 原理

源码分析

内部结构

ReentrantLock中有个重要的成员sync,通过继承AQS这个抽象类然后重写相关方法来实现

private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {...}

ReentrantLock有两个构造方法,无参构造方法默认创建非公平锁(NonfairSync),有参构造方法传入true则会创建公平锁(FairSync)
公平锁与非公平锁通过继承Sync类后重写相关方法来实现

static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

获取锁

首先来分析下获取锁的lock方法,通过多态性会调用FairSync或NonfairSync内重写的lock方法

public void lock() {
    sync.lock();
}
static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1)) // 直接用CAS修改状态位,争抢锁
            setExclusiveOwnerThread(Thread.currentThread()); // 争抢成功设置当前线程独占锁
        else
            acquire(1);
    }
}
static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
}

可以看到非公平锁会直接在这里开始争抢锁,在compareAndSetState中会尝试使用CAS操作将state状态从未加锁(0)置为加锁(1),若CAS操作失败表示锁已被其他线程持有,则与公平锁一样执行acquire

acquire是AQS提供的基类方法,作用是通过tryAcquire尝试争抢锁,争抢失败就把线程加入等待队列,加入排队竞争阶段

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

AQS的tryAcquire是个空方法(只抛异常),真正实现是在NonfairSync与FairSync中,两者相比,非公平锁在无人占有锁时,并不会检查队列中是否有线程在等待

// 非公平锁tryAcquire实现
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();// 获取当前AQS内部状态量
    if (c == 0) {// 0表示无人占有,则直接用CAS修改状态位
        if (compareAndSetState(0, acquires)) {// 不检查排队情况,直接争抢
            setExclusiveOwnerThread(current);// 争抢成功设置当前线程独占锁
            return true;
        }
    }//即使状态不是0,也可能当前线程是锁持有者,因为这是再入锁
    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;
}
// 公平锁tryAcquire实现,省略相同的内容
protected final boolean tryAcquire(int acquires) {
    ...
    if (c == 0) {
        if (!hasQueuedPredecessors() &&// 队列中有线程排队,则放弃这次争抢锁的机会
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    ...
}

addWaiter会把线程包装成一个独占式的节点对象,并通过CAS操作将节点放入队列

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {// 队列已创建则把节点放入队尾
        node.prev = pred;// 当前节点的前节点指向尾节点
        if (compareAndSetTail(pred, node)) {// 使用CAS把原尾节点替换为当前节点,使当前节点成为新的尾节点
            pred.next = node;// 原尾节点的后节点指向当前节点
            return node;
        }
    }
    enq(node);// 队列未创建或节点入队失败
    return node;
}
private Node enq(final Node node) {
    for (;;) {// 死循环CAS入队,直到成功退出循环
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))// 将头节点对象地址与null比较,相同则替换为new Node()
                tail = head;// 队列创建成功
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

入队成功后进入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; // 前节点出队
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&// 判断是否当前节点是否要堵塞
                parkAndCheckInterrupt())// 当前线程会被堵塞在此处
                interrupted = true;// 被中断唤醒过就会被标记为interrupted
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire中会将前节点的状态更新为SIGNAL,表示告知前节点在释放锁时要唤醒它的后节点,否则前节点若是取消状态的话将被移出队列

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)// 前节点是SIGNAL状态表示当前节点可被堵塞了
        return true;
    if (ws > 0) {// 前节点是取消状态表示将被移除
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);// 不停循环将状态是取消的前节点移出队列
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// 将前节点状态置为SIGNAL
    }
    return false;
}

线程会在parkAndCheckInterrupt中被堵塞,堵塞使用的是LockSupport.park,会被其他线程使用unpark唤醒,如果线程被中断也会退出堵塞状态

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

释放锁

然后来分析下释放锁的unlock方法,公平锁与非公平锁释放锁的逻辑一样,都会调用AQS的release方法

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {//释放锁
        Node h = head;
        if (h != null && h.waitStatus != 0)//头节点等待状态不为0则表示有后节点等待它唤醒
            unparkSuccessor(h);//唤醒后节点
        return true;
    }
    return false;
}

在tryRelease中释放锁,如果锁重入了则重入次数-1

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;// 由于是重入锁,每次释放锁-1
    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);//头节点等待状态置为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);//唤醒节点
}

参考

Java核心技术面试精讲
https://juejin.im/post/5c95df97e51d4551d06d8e8e#heading-5
https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/
https://www.cnblogs.com/waterystone/p/4920797.html

你可能感兴趣的:(Java,面试相关)