Back
Featured image of post [并发编程]ReentrantLock篇

[并发编程]ReentrantLock篇

基本介绍

对于ReentrantLock(重入锁),是常用的Lock接口的一个实现,最主要的是了解他的重入性和公平锁/非公平锁,还有用他于synchronized机械能对比,下面进行具体介绍。

重入性实现原理

重入性有2个基本特点:

  1. 在线程获取锁的时候,如果锁已经存在且锁还是当前线程的,那可以直接再次获取成功
  2. 由于锁可以被同一线程获取n次,在释放时同样要释放n次才能把锁完全释放开。

许多同步组件都是通过重写AQS的方法来实现自己的同步功能的,下面以ReentrantLock的非公平锁的源码来解析其重入的实现。核心方法nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果该锁未被任何线程占有,该锁能被当前线程获取
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //2.若被占有,检查占有线程是否是当前线程
    else if (current == getExclusiveOwnerThread()) {
        // 3. 再次获取,计数加一
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

首先是判断当前是否存在锁,如果不存在,那自然可以获取。

如果存在,而且占有锁的还是当前这个线程,那就可以重入,同步状态(锁的计数器)+1。

对于释放,也是同样的道理,核心方法tryRelease

protected final boolean tryRelease(int releases) {
    //1. 同步状态减1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        //2. 只有当同步状态为0时,锁成功被释放,返回true
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 3. 锁未被完全释放,返回false
    setState(c);
    return free;
}

用一个变量free来表示锁是否被完全释放,只有当同步状态(锁的计数器)为0时才会设置为true,否则还是上锁的状态。

公平锁和非公平锁

设置公平性

ReentrantLock支持公平锁和非公平锁,默认是非公平的(无参构造方法),也可以用一个带有boolean值的构造方法来指定公平性。

// 默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
// true为公平锁,false为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平性的意思就是,如果是公平锁,那么线程获取锁的顺序就是按它们请求时的顺序,维护来一个队列进行排队,FIFO;如果是非公平锁,则没有进行排队,线程发起请求时就立刻可以去尝试获取锁。

源码解析

上面的nonfairTryAcquire 就是非公平锁的源码来,下面我们看一下公平锁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;
  }
}

其实大部分逻辑和非公平锁是相似的,不同在于公平锁在判断到当前资源的同步状态为0后,还多判断来一次hasQueuedPredecessors() ,这个方法用于判断当前节点在同步队列中是否有前驱节点,也就是当前线程是否位于同步队列的队头(判断时取反)。如果是才能进行下面的逻辑来获取资源的锁,否则根据公平性是会获取失败的。

对比

  1. 公平锁每次都只能让位于同步队列中的第一个线程进行获取资源,保证请求资源时间上的绝对顺序;而非公平锁可能产生刚刚释放锁的线程又立刻拿到锁了,导致其他线程迟迟拿不到,造成了“饥饿”现象。
  2. 公平锁为了保证时间上的顺序,往往后涉及频繁的上下文切换,造成比较大的开销。而非公平锁则会降低一定的上下文切换,提高了性能,所以ReentrantLock默认是非公平的,保证系统更大的吞吐量。

参考

GitHub - CL0610/Java-concurrency: Java并发知识点总结

comments powered by Disqus
一辈子热爱技术
Built with Hugo
Theme Stack designed by Jimmy
gopher