深刻理解 ReentrantLock

ReentrantLock

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

另外,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 中已经默认实现:ui

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,也就是该锁是否处于空闲状态,若是是的话则尝试获取锁,设置成功将当前线程设置为持有锁的线程。spa

不然的话,就判断当前线程是否为持有锁的线程,若是是的话,则增长同步状态值,获取到锁,这里也就验证了锁的可重入,再获取了锁以后,能够继续获取锁,只需增长同步状态值便可。线程

FairSync

FairSynclock() 方法的实现以下:code

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,简单方便。