并发包源码解读——ReentrantLock

1、构造器

ReentrantLock可以构造两种锁:公平锁和非公平锁

我们可以先复习一下概念

1.1、非公平锁

每个等待中的线程都有相等的机率抢占到锁(不排队,可以插队,所以不公平)

优点:吞吐量高,因为在锁被其他资源抢占时,公平锁需要将每个新来的线程依次挂起放入等待队列,而非公平锁有机率使新来的线程直接获取到锁,性能的差异就在于新线程的挂起和唤醒

缺点:在等待队列中的线程有可能长时间获取不到锁,处于饥饿状态

默认构建非公平锁

public ReentrantLock() {
    sync = new NonfairSync();
}

1.2、公平锁

先进入等待队列的线程先获取锁

优缺点与非公平锁相反,吞吐量差点,但是线程不会出现饥饿

传参构造非公平锁

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

至于两者具体的实现差别在哪,往下看就知道了

2、非公平锁实现逻辑

我们以最简单的demo开始:

主线程获取锁对象,然后创建一个子线程再尝试获取锁对象,如果主线程不解锁,那子线程就会一直处于park状态

public static void main(String[] args) throws InterruptedException {
    ReentrantLock reentrantLock = new ReentrantLock();
    reentrantLock.lock();
    Thread.sleep(1000);
    new Thread(() -> reentrantLock.lock()).start();
}

2.1、非公平锁构造器

NonfairSync这个类继承了Sync,Sync又继承了AbstractQueuedSynchronizer

AQS这个类很著名也很重要,里面有很多成员变量和方法与CLH队列的实现有关,所以可以认为一个锁对象对应一个AQS,在下面的实现逻辑中会串进去讲

2.2、非公平锁获取锁

可以看到,线程在第一次获取非公平锁的时候,什么也不顾及直接先尝试CAS获取锁,对比公平锁会直接调用acquire,后面讲

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
2.2.1、compareAndSetState——获取锁

加锁的时候,会先进到compareAndSetState方法中,这个方法就是给锁对象加锁,原理是尝试使用CAS改变锁对象的状态,方法在AQS中实现,是unsafe中对应方法的封装。unsafe里都是native方法,跟java的底层c库有关,就先不拓展讲了(主要我也没看过)

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

这个stateOffset是个静态变量,放在static代码块中,在new第一个锁的时候就已经初始化,可以理解为存储state字段的内存地址与AQS起始地址的偏移量。同时还初始化了其他偏移量,都是底层用来寻址的。这些静态变量并不影响AQS对象的内部状态。

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

static {
    try {
        stateOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        headOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
        tailOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
        waitStatusOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("waitStatus"));
        nextOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("next"));

    } catch (Exception ex) { throw new Error(ex); }
}

下面是AQS的前3个成员变量,可以看见state之前有两个对象,64位JVM虚拟机用8字节存储一个对象引用,所以你debug的时候可以看到stateOffset初始化之后值就是16

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;

2.2.2、setExclusiveOwnerThread——设置拥有锁的线程

加锁成功会执行setExclusiveOwnerThread,把有锁的线程缓存起来

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

2.2.3、acquire——尝试重入锁和排队

2.2.3.1、重入锁

可重入锁的概念就是,同一个线程可以在不释放锁的情况下,多次获取同一把锁,下面来看逻辑

加锁失败后会进入acquire方法

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

进入tryAcquire方法,它是nonfairTryAcquire的封装

首先它会再尝试获取一次锁(之前CAS已经失败了,现在又试一次,两次获取锁之间的代码只有Thread.currentThread(),不知道是不是为了极限性能,提高非公平性),成功了就返回“获取成功”

失败了就判断持有锁的线程是不是自己,是自己就给state加1,表示加锁的次数,这就是可重入锁的实现逻辑

如果拥有锁的线程都不是自己的,就返回“获取失败”

对比公平锁的实现,少了一步hasQueuedPredecessors判断队列中是否有等待的线程,公平锁的具体实现我们也后面再讲

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
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;
}

当这次尝试获取锁成功了,方法就结束了,获取失败了会怎么样呢

2.2.3.2、排队

会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法

首先来看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;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

反之进入enq方法,这个方法是用来初始化队列首尾节点的,跟LinkedList结构有点像

其中compareAndSetHead和compareAndSetTail也都是调用unsafe方法,传AQS对象和对应参数的偏移量,套路相同

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

从addWaiter出来之后,返回了当前加入队列的节点,就是想要获取锁的该线程所对应的节点,当作参数进入acquireQueued方法中

acquireQueued这个方法是真正让线程进入阻塞状态的方法

在进入for循环后,会进入shouldParkAfterFailedAcquire方法检查前一个节点的waitStatus并置为-1,表示该节点(其实原意应该是前一个节点后面的节点,意思都一样)应该park,然后在第二次循环时执行parkAndCheckInterrupt

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;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

parkAndCheckInterrupt是LockSupport.park的封装

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

线程停就停在了UNSAFE.park这个方法,等上一个获取锁的线程释放锁,这个线程就能从park中解脱出来,继续往下走

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

线程从park中解脱之后,会回到acquireQueued方法,在下一次for循环中进入(p == head && tryAcquire(arg))这个条件,尝试获取锁,获取到了就返回,没获取到就继续之前几步。其中会判断线程的interrupted中断状态,进行自我中断

(这里也补充一点,因为在线程进入队列之后就不会被中断,外部调用该线程的中断方法,该线程也不会立刻响应,只会给该线程做一个“需要中断”的标记并唤醒该线程,并让该线程从UNSAFE.park中解脱出来,然后再次进入acquireQueued的for循环中,等该线程获取到锁之后再自己调用中断)

2.2.3、非公平锁释放锁

释放锁调用unlock方法,是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)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先调用tryRelease方法释放锁

首先该线程肯定得是锁的拥有者,然后锁状态-1并更新,如果-1之后锁状态为0说没有重入锁了,就把线程拥有者置空

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

回到release方法,如果头节点的waitStatus不是0,说明还有线程在park状态,需要被唤醒,执行unparkSuccessor方法

可以看到,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);
}

释放锁或唤醒之后,各个线程就会继续执行他们后面的逻辑,非公平锁的部分就告一段落了

3、公平锁实现逻辑

那我们主要说与非公平锁的实现差异

3.1、构造器

传true构造公平锁

ReentrantLock reentrantLock = new ReentrantLock(true);

3.2、获取锁

直接调用acquire方法,而非公平锁会先尝试CAS获取一次锁

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

tryAcquire的实现不同

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        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;
}

在尝试获取锁之前,公平锁比非公平锁多了hasQueuedPredecessors,用来检查队列中是否有park线程,保证先来的线程先获取锁,实现线程获取锁的公平性

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

其余地方与非公平锁调用的都是相同的方法

以上就是ReetrantLock源码的解读,讲的不清楚的或者有问题的可以留言,互相讨论互相进步,同时欢迎点赞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章