Java ReentrantLock 源码阅读笔记(上)

Java ReentrantLock 源码阅读笔记(上)

Java 中的 ReentrantLockSynchronized 的性能在稍微新一点的虚拟机上的性能没有太大的区别,但是 ReentrantLock 的功能更加丰富,在实际编程中只要能够实现你的业务逻辑,我认为用哪个都没有什么区别。Synchronized 锁的实现是在虚拟机中实现的,ReentrantLock 中的绝大部分代码是用 Java 实现的,本篇文章内容就是来理解 ReentrantLock 的实现,当理解了 ReentrantLock 的实现后,其实 Synchronized 也就好理解了,我认为他们之间有太多的相似处。

获取锁

当线程需要获取锁的时候需要调用 ReentrantLock#lock() 方法:

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

这个 sync 变量有两个实现,分别是 FairSyncNoFairSync,他们分别表示公平锁和不公平锁,默认是使用不公平锁,关于公平锁和不公平锁我们后面讨论,这两个对象他们都是继承于 AbstractQueuedSynchronizer,也就是很多人口中的 AQSReentrantLock 的线程安全的实现都是基于它,其实我们阅读的代码大部分也是它。

这里我们看到获取锁的时候调用了 AbstractQueuedSynchronizer#acquire() 方法,同时传入了 1 作为参数。这里我还要再啰嗦下可重入锁和不可重入锁:可重入锁是当前线程获取锁后还可以再次获取锁,不过在释放锁的时候需要再调用同样次数的释放锁方法才可以完全释放锁;而不可重入锁在调用过获取锁方法后,就不能再次获取锁了,即使是同一个线程,需要释放上次的锁后才可以继续获取锁。ReentrantLock 是一种可重入锁,他的内部有一个状态来描述获取锁的次数,当没有获取锁时这个状态就是 0,调用一次获取锁的状态就会把这个状态加 1(也就是上面的 acquire 方法传入的参数),调用一次释放锁就会把这个状态减 1。

先来看看 AbstractQueuedSynchronizer#acquire() 方法的实现:

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

这里有三个重要的方法:
tryAcquire() 方法是一个抽象方法,等下我们看看 NoFairSync 的实现,他表示当前是否能够获取锁。
addWaiter() 方法是创建一个关于当前线程的 Node 并添加到队列尾部,这个队列就是等待锁的队列。
acquireQueued() 方法阻塞当前线程,直到当前线程获取到锁。

简单总结一下就是通过 tryAcquire() 方法判断是否能够获取锁,如果不能够获取通过 addWaiter() 方法将当前线程添加到等待队列的尾部,然后通过 acquireQueued() 方法来阻塞当前线程,直到当前线程获取到锁。

我们先来看看 NoFairSync#tryAcquire() 的实现:


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

@ReservedStackAccess
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) 
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    
    return false;
}

获取锁有三种情况:

  1. 当前没有锁:直接修改 state 为 1,并将当前线程设置为 owner,获取锁成功。
  2. 当前有锁但是锁是当前线程持有的(重入情况):将 state 加 1,获取锁成功。
  3. 其他线程持有锁,获取锁失败。

ReentrantLock 中大量使用了 CAS 的方式修改值,通过这样来保证值的修改是线程安全的,或者说在多线程编程中都有大量的使用,而且很多的 CPU 也有专门的 CAS 指令,大家不用太担心它的性能,我们也可以试着用 CAS 的方式来保证值的修改的安全,在 Java 中最简单的使用 CAS 的方式就是使用各种 Atomic 原子类。(又说废话了😂)

当获取锁失败后就会通过 AbstractQueuedSynchronizer#addWaiter() 在锁的等待队列的尾部添加当前线程的节点。

private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}

修改队列也是用到了 CAS 的方式,当队列没有初始化,会创建一个空的 Node 同时赋值给 headtail

我们再继续看看 AbstractQueuedSynchronizer#acquireQueued() 方法是如何阻塞线程的,这个方法要注意理解:

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        
        for (;;) {
            
            final Node p = node.predecessor();
            
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; 
                return interrupted;
            }
            
            if (shouldParkAfterFailedAcquire(p, node))
                
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        
        
        return true;
    if (ws > 0) {
        
        
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        
        
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}


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

上面的方法可能没有那么好理解,可以先看看我对代码的注释,我这里举一个例子:假如有一个线程已经获取到锁,第二个线程去请求锁这个情况。

第二个线程去请求锁的时候,首先通过 tryAcquire() 方法去尝试获取锁,会返回 false;然后通过 addWaiter() 方法去添加一个尾部的节点,这个时候队列还没有初始化会添加一个空的节点同时指向 headtail;再然后调用 acquireQueued() 方法去等待锁的释放,这个方法是一个死循环,第一次循环首先判断前一个节点是否是 head,我们这种情况下就是 head,所以还会再次通过 tryAcquire() 方法去尝试获取锁,我们假定这次获取锁失败,然后进入 shouldParkAfterFailedAcquire() 方法去判断是否需要阻塞当前线程,由于 head 节点默认的 waitStatus 是 0,所以会被修改成 SIGNAL,同时进入下次循环,我们假定下次循环还是获取锁失败,然后又进入 shouldParkAfterFailedAcquire() 方法,这次由于 head 的状态被上次修改成 SIGNAL 了,所以会返回 true,然后会进入 parkAndCheckInterrupt() 方法完成当前线程的阻塞。我们假如过了一段时间第一个线程已经释放锁了,这时会释放当前线程的阻塞,然后进入第三次循环,我们假定这次获取锁成功,当前线程的 Node 就会被修改成 head,然后返回 lock() 方法,最终获取锁成功。

释放锁

我们直接看 unlock() 方法:

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

我们继续看 AbstractQueuedSynchronizer#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;
}

同样的 tryRelease() 是一个抽象方法,我们来看看 ReentrantLock 中的实现:

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

其实就是和加锁相反的过程,当你理解了加锁的过程,上面的方法理解起来非常简单。

前面我们说到我们的 headwaitStatus 会被修改成 SIGNAL,然后我们会进入 unparkSuccessor() 去恢复被阻塞的线程。

private void unparkSuccessor(Node node) {
    
     
    int ws = node.waitStatus;
    
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);

    
     
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
       
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        
        LockSupport.unpark(s.thread);
}

上面代码比较简单,我也写注释了,看起来应该比较轻松。

公平锁和不公平锁

我在前面分析了不公平锁的 tryAcquire() 方法的实现,我们再来看看公平锁的实现:

@ReservedStackAccess
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() 方法来判断队列中是否有等待的线程:

public final boolean hasQueuedPredecessors() {
    Node h, s;
    if ((h = head) != null) {
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null; 
            for (Node p = tail; p != h && p != null; p = p.prev) {
                if (p.waitStatus <= 0)
                    s = p;
            }
        }
        if (s != null && s.thread != Thread.currentThread())
            return true;
    }
    return false;
}

我们先想象一下不公平锁不添加队列的判断有什么问题,如果等待队列中有 10 个线程正在等待,第 11 个线程请求锁的时机和锁拥有的线程释放锁的时机相同,那么第 11 个线程就会和等待队列中的线程通过 CAS 的方式去竞争锁,如果第 11 个线程运气比较好它就会竞争过其他等待的 11 个线程而获取锁。人家都在排队,但是第 11 个线程却不用排队就可以获得锁,这显然是不公平的,所以怎么解决这种不公平现象呢?就是在获取锁的时候判断下是否有其他线程在等待,如果有,也得乖乖排队,这就显得公平了。

最后

本来想一次写完 ReentrantLock 源码阅读的文章,这样你看得累,我写得也累,所以 Condition 留到下篇再写,希望本篇文章对你有帮助。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22369,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?