深入理解 ReentrantLock

1,463 阅读5分钟

ReentrantLock

ReentrantLock 是一种可重入锁,它指的是一个线程能够对资源重复加锁。ReentrantLocksynchronized 类似,能够保证解决线程安全问题,但是却提供了比 synchronized 更强大、灵活的机制,例如可中断式的获取锁、可定时的获取锁等。

另外,ReentrantLock 也提供了公平锁与非公平锁的选择,它们之间的区别主要就是看对锁的获取与获取锁的请求的顺序是否是一致的,选择公平锁时,等待时间最长的线程会最优先获取到锁,但是公平锁获取的效率通常比非公平锁要低。可以在构造方法中通过传参的方式来具体指定选择公平或非公平。

公平锁

ReentrantLock 中,有一个抽象内部类 Sync,它继承自 AQSReentrantLock 的大部分功能都委托给 Sync 进行实现,其内部定义了 lock() 抽象方法,默认实现了 nonfairTryAcquire() 方法,它是非公平锁的默认实现。

Sync 有两个子类:公平锁 FairSyncNonFairSync,实现了 Sync 中的 lock() 方法和 AQS 中的 tryAcquire() 方法。

NonFairSync

NonFairSynclock() 方法的实现如下:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

首先,非公平锁可以立即尝试获取锁,如果失败的话,会调用 AQS 中的 acquire 方法,其中 acquire 方法又会调用由自定义组件实现的 tryAcquire 方法:

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

nonfairTryAcquire() 方法在 Sync 中已经默认实现:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 使用 CAS 设置同步状态
        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;
}

这里,首先会判断的当前线程的状态是否为 0,也就是该锁是否处于空闲状态,如果是的话则尝试获取锁,设置成功将当前线程设置为持有锁的线程。

否则的话,就判断当前线程是否为持有锁的线程,如果是的话,则增加同步状态值,获取到锁,这里也就验证了锁的可重入,再获取了锁之后,可以继续获取锁,只需增加同步状态值即可。

FairSync

FairSynclock() 方法的实现如下:

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

公平锁只能调用 AQSacquire() 方法,再去调用由自定义组件实现的 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 方法,这个方法用来判断同步队列中是否有前驱节点。也就是当前线程前面再没有其他线程时,它才可以尝试获取锁。

释放锁

ReentrantLockunlock 方法内部调用 AQSrelease 方法释放锁,而其中又调用了自定义组件实现的 tryRelease 方法:

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

首先,判断当前线程是否是持有锁的线程,如果不是会抛出异常。如果是的话,再减去同步状态值,判断同步状态是否为 0,即锁被完全释放,其他线程可以获取同步状态了。

如果没有完全释放,则仅使用 setState 方法设置同步状态值。

指定公平性

ReentrantLock 的构造函数中可以指定公平性:

  • 默认创建一个非公平的锁
public ReentrantLock() {
    sync = new NonfairSync();
}
  • 创建一个指定公平性的锁。
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

synchronized 和 ReentrantLock 区别

这里总结一下 synchronized 和 ReentrantLock 的异同,它们之间的相同点如下:

  • 都可以用于实现线程间的同步访问;
  • 两者都是可重入锁,即一个线程能够对资源重复加锁;

其不同点如下:

  • 同步实现机制不同:
    • synchronized 通过 Java 对象关联的 Monitor 监视器实现(不考虑偏向锁、轻量级锁);
    • ReentrantLock 通过 CASAQSLockSupport 等共同实现;
  • 可见性实现机制不同:
    • synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。
    • ReentrantLock 通过 ASQvolatile 类型的 state 同步状态值保证包含共享变量的多线程内存可见性。
  • 使用方式不同:
    • synchronized 可以用于修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、同步代码块(指定的锁对象)。
    • ReentrantLock 需要显式地调用 lock 方法,并在 finally 块中释放。
  • 功能丰富程度不同:
    • synchronized 只提供最简单的加锁。
    • ReentrantLock 提供定时获取锁、可中断获取锁、Condition(提供 awaitsignal 等方法)等特性。
  • 锁类型不同:
    • synchronized 只支持非公平锁。
    • ReentrantLock 提供公平锁和非公平锁实现。但非公平锁相比于公平锁效率较高。

synchronized 优化以前,它比较重量级,其性能比 ReentrantLock 要差很多,但是自从 synchronized 引入了偏向锁、轻量级锁(自旋锁)、锁消除、锁粗化等技术后,两者的性能就相差不多了。

一般来说,仅当需要使用 ReentrantLock 提供的其他特性时,例如:可中断的、可定时的、可轮询的、公平地获取锁等,才考虑使用 ReentrantLock。否则应该使用 synchronized,简单方便。