Concurrent包(二):深入ReentrantLock

ReentrantLock简介

ReentrantLock表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数。如果当前线程 t1 通过调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数 就行了。synchronized 和 ReentrantLock 都是可重入锁。

重入锁的目的

比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。

ReentrantLock的源码分析

ReentrantLock.lock(),这个是 reentrantLock 获取锁的入口。

/**
 * Acquires the lock.
 *
 * <p>Acquires the lock if it is not held by another thread and returns
 * immediately, setting the lock hold count to one.
 *
 * <p>If the current thread already holds the lock then the hold
 * count is incremented by one and the method returns immediately.
 *
 * <p>If the lock is held by another thread then the
 * current thread becomes disabled for thread scheduling
 * purposes and lies dormant until the lock has been acquired,
 * at which time the lock hold count is set to one.
 */
public void lock() {
    sync.lock();
}

注释翻译:

如果没有线程持有这把锁锁,则当前线程获取到锁,并设置线程的重入次数记为1。

如果是当前线程已经持有这把锁,则重入次数加1并返回。

如果是其他线程持有这把锁,则当前线程将因线程调度而被禁用,并处于休眠状态,直到获得锁,此时锁保持计数设置为1。

sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。

sync 有两个具体的实现类:

  • NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他 线程等待,新线程都有机会抢占锁。

  • FailSync:表示所有线程严格按照 FIFO 来获取锁。

非公平锁和公平锁的区别在于,在非公平锁中,在获取锁的时候,不管有没有线程排队,先用 CAS 抢占一下。若CAS 成功,就表示成功获得了锁;CAS 失败,调用 acquire(1)走锁竞争逻辑。

以非公平锁为例,来看看 lock 中的实现:

final void lock() {
    if (compareAndSetState(0, 1)) // 不管有没有线程排队,先用 CAS 抢占一下
        setExclusiveOwnerThread(Thread.currentThread()); // CAS 成功,就表示成功获得了锁
    else
        acquire(1); // CAS 失败,调用 acquire(1)走锁竞争逻辑
}

此处的CAS:

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

compareAndSwapInt:如果当前内存中的 state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返 回 false。

state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入 锁的实现来说,表示一个同步状态。它有两个含义的表示

  • 当 state=0 时,表示无锁状态
  • 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为 ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增, 比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0 其他线程才有资格获得锁

unsafe类:Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包 括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、 Hadoop、Kafka 等;Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、 线程的挂起和恢复、CAS、线程同步、内存屏障。而 CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象, 第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第 四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false;

再来看acquire(1)方法:

acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此 时继续 acquire(1)操作。

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

在tryAcquire方法中就是重入性的体现了。

重入性的实现原理

ReentrantLock的核心其实是AQS的独占锁,而其重入性体现在:

  • 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
  • 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

获取锁

NonfairSync.tryAcquire方法:

这个方法重写了AQS的tryAcquire,作用是尝试获取锁,如果成功返回 true,不成功返回 false。

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

直接看nonfairTryAcquire(acquires)

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

释放锁

Sync.tryAcquire方法:

这个方法在sync中而不是NonfairSync:

其源码:

protected final boolean tryRelease(int releases) {
  // 同步状态减1,getState()获取的是线程的重入次数
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
      // 只有当同步状态为0时,锁成功被释放,返回true
        free = true;
        setExclusiveOwnerThread(null);
    }
  // 锁未被完全释放,设置state为减去后的数量,返回false
    setState(c);
    return free;
}

公平锁和非公平锁

锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序 就应该符合请求的绝对时间顺序,也就是 FIFO。 在上面分析的例子来说,只要 CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点有两个:

FairSync.lock:

final void lock() {
    acquire(1);
}

直接acquire(1),并没有先CAS试一下。

FairSync.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()方法:用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁

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

参考:

并发编程

《java并发编程的艺术》

文章已创建 17

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部