AnthonyZero's Bolg

Java并发-重入锁ReentrantLock

概述

重入锁ReentrantLock,顾名思义就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。(即当前线程再次获取该锁不会被阻塞)

Java关键字synchronized隐式支持重入性,实现的原理是通过编译后加上不同的机器指令来实现。而 ReentrantLock 就是一个普通的类,它是基于 AQS(AbstractQueuedSynchronizer)来实现的

synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API

锁类型

ReentrantLock 实现 Lock 接口,同时内部类 Sync 是 AQS 的子类;而 Sync 又有两个子类 NonfairSync 和 FairSync 分别对应非公平和公平锁两种策略。

Alt text

ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型:

//默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

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

实现重进入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特征的实现需要解决以下两个问题:

  1. 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。
  2. 锁的最终释放: 线程重复n次获取了锁,随后在第n次释放了该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

非公平锁

ReentrantLock默认构造函数为非公平锁,非公平锁是指在竞争获取锁的过程中,有可能后来者居上

static final class NonfairSync extends Sync {
    final void lock() {
        // CAS 设置 state 值为 1
        if (compareAndSetState(0, 1))
            //CAS 成功则说明获取到锁, 此时将当前线程设置为独占模式下锁对象的持有者
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // CAS 失败 可能是同一线程再次获取锁 也可能是不同线程获取锁
            acquire(1); //调用AQS acquire方法 -> 调用子类覆写的tryAcquire方法
    }

    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)) {
            // CAS 成功则说明获取到锁, 此时将当前线程设置为独占模式下锁对象的持有者
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 说明同一线程再次获取锁
        // state 加 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}  

通过判断当前线程是否为获取锁的线程来决定操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功

公平锁

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO先进先出

对于上面非公平锁而言,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }

    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()判断,即加入了同步队列中当前节点是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败,因此需要等待前驱线程获取了锁并释放之后才能继续获取锁。

公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

释放锁

公平锁的释放与非公平锁的释放操作一致

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

如果该锁被获取了n次,那么前n-1次tryRelease方法返回了false,而只有同步状态完全释放了才能返回true。可以看到该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时将占用线程设置为null,并返回true表示释放成功

总结

  • ReentrantLock跟synchronized一样都是互斥同步,在同一个时刻只允许一个线程获取锁。(独占式)
  • synchronized隐式支持重入性,是非公平锁。而ReentrantLock除了支持重入性,还能支持公平锁和非公平锁(默认)
  • 公平锁每次获取到锁是同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
  • 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。