ReentrantLock-非公平锁和公平锁

123 阅读10分钟

非公平锁和公平锁的区别?

非公平锁和公平锁是在多线程编程中的两种不同的锁获取策略,它们的主要区别在于线程获取锁的顺序。

  1. 非公平锁

    • 性能优先:非公平锁不考虑线程请求锁的顺序,它允许任何等待锁的线程随机竞争锁。这意味着一个刚释放锁的线程再次获取锁的可能性更高,因为它们无需等待,这可以提高性能。

    • 不保证公平性:非公平锁不保证锁的公平性,即不保证等待时间最长的线程会首先获取锁。这可能导致某些线程长时间等待,被称为"饥饿"问题。

    • 默认行为:在Java中,ReentrantLock的默认行为是创建非公平锁。也就是说,如果不明确指定是公平锁还是非公平锁,那么默认情况下会使用非公平锁。

  2. 公平锁

    • 按顺序获取锁:公平锁会按照线程请求锁的顺序来分配锁,即先到先得,确保线程获取锁的顺序与它们请求锁的顺序一致。这可以避免某些线程长时间等待,确保锁的分配更公平。

    • 保证公平性:公平锁会保证等待时间最长的线程会首先获取锁,不会发生"饥饿"问题。但这可能会降低锁的性能,因为它需要维护一个等待队列以管理线程的顺序。

    • 显式创建:在Java中,如果你想创建一个公平锁,需要在创建ReentrantLock对象时将构造函数参数设置为true,例如:ReentrantLock fairLock = new ReentrantLock(true);

总结起来,非公平锁追求更高的性能,但不保证线程获取锁的公平性,可能导致某些线程长时间等待。而公平锁会保证线程获取锁的公平性,但可能会降低性能,因为需要维护等待队列以按顺序分配锁。选择使用哪种锁取决于你的应用程序需求,如果公平性很重要,可以选择使用公平锁,否则可以使用默认的非公平锁。


说白了,就是非公平,就性能高。公平,就性能低。

默认是非公平,因为要性能高。

默认是非公平锁-源码分析

java.util.concurrent.locks.ReentrantLock#tryLock()

/**
 * Acquires the lock only if it is not held by another thread at the time
 * of invocation.
 *
 * <p>Acquires the lock if it is not held by another thread and
 * returns immediately with the value {@code true}, setting the
 * lock hold count to one. Even when this lock has been set to use a
 * fair ordering policy, a call to {@code tryLock()} <em>will</em>
 * immediately acquire the lock if it is available, whether or not
 * other threads are currently waiting for the lock.
 * This &quot;barging&quot; behavior can be useful in certain
 * circumstances, even though it breaks fairness. If you want to honor
 * the fairness setting for this lock, then use
 * {@link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) }
 * which is almost equivalent (it also detects interruption).
 *
 * <p>If the current thread already holds this lock then the hold
 * count is incremented by one and the method returns {@code true}.
 *
 * <p>If the lock is held by another thread then this method will return
 * immediately with the value {@code false}.
 *
 * @return {@code true} if the lock was free and was acquired by the
 *         current thread, or the lock was already held by the current
 *         thread; and {@code false} otherwise
 */
public boolean tryLock() {
    return sync.nonfairTryAcquire(1); //默认是非公平锁
}

java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
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) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //获取锁失败
    return false;
}

基于cas实现。

要么成功,要么失败。不会阻塞。而非公平锁,如果没有获取到锁,是会阻塞的。

所以有两个最大的区别,第一,是否排队,第二,是否阻塞。

ReentrantLock公平锁怎么使用?

demo

ReentrantLock是Java中用于实现可重入锁的类,而"公平锁"是ReentrantLock的一种模式,它会按照线程请求锁的顺序来分配锁,即先到先得,确保线程获取锁的顺序与它们请求锁的顺序一致。以下是使用ReentrantLock来创建公平锁的步骤:

  1. 导入相关的包:

首先,确保你的Java文件导入了ReentrantLock类所在的包:

import java.util.concurrent.locks.ReentrantLock;
  1. 创建ReentrantLock对象:

在你的代码中,创建一个ReentrantLock对象,这将用于锁定资源。你可以选择创建一个公平锁或非公平锁,这里我们讨论的是公平锁:

ReentrantLock fairLock = new ReentrantLock(true); // 创建一个公平锁
  1. 使用锁保护临界区:

在需要保护的临界区域,使用ReentrantLock对象来获取和释放锁。在进入临界区之前,使用lock()方法获取锁,在退出临界区时使用unlock()方法释放锁。公平锁会确保等待时间最长的线程会优先获取锁。

fairLock.lock(); // 获取锁
try {
    // 执行需要同步的代码块
} finally {
    fairLock.unlock(); // 释放锁
}
  1. 处理异常情况:

在使用ReentrantLock时,务必在finally块中释放锁,以确保即使在代码块中发生异常,锁也能被正确释放。

这就是使用ReentrantLock来创建公平锁的基本步骤。请注意,公平锁会增加线程等待时间,因为它会按照请求的顺序分配锁,这可能导致一些线程等待较长时间。如果不需要强制公平性,也可以创建一个非公平锁,只需将ReentrantLock的构造函数参数设置为false即可。

源码分析

入口:获取锁

java.util.concurrent.locks.ReentrantLock#lock

/**
 * 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(); //获取锁
}

java.util.concurrent.locks.ReentrantLock.FairSync#lock

final void lock() {
    acquire(1); //获取锁
}

基于公平锁类,获取锁。


底层是基于AQS

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
public final void acquire(int arg) {
    //获取锁
    if (!tryAcquire(arg) && //获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //如果没有获取到锁,就排队
        selfInterrupt();
}

核心流程:是否获取到锁?如果获取到锁,那么直接返回。如果没有,那么排队。


如果获取到锁

java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire

/**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) { //基于cas,获取锁
                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;
    }
}

如果没有获取到锁,排队

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
final boolean acquireQueued(final Node node, int arg) { //排队
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) //阻塞:parkAndCheckInterrupt
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

小结

底层是基于AQS,更底层是基于cas。说白了,就是AQS封装了一下cas。也就是说,为什么要有AQS?封装了cas,使得cas更好用,比如说,这里的公平锁功能,就是封装功能之一。

总结

公平,基于排队。不公平,基于CAS,要么成功,要么失败。

cas不会阻塞。

ReentrantLock-非公平锁,也不会阻塞。要么成功,要么失败。

ReentrantLock-公平锁,会阻塞。成功,就直接返回。没成功,就排队——排队的意思,就是阻塞等待被唤醒,而且是按先进先出的顺序被唤醒。

所以,单纯的cas,永远都不会阻塞。但是和其他结合,比如公平锁-排队,那么这个时候就会阻塞。

官方文档-ReentrantLock


public class ReentrantLock
extends Object
implements Lock, Serializable

A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.
可重入互斥 Lock 与使用 synchronized 方法和语句访问的隐式监视器锁具有相同的基本行为和语义,但具有扩展功能。

ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it. A thread invoking lock will return, successfully acquiring the lock, when the lock is not owned by another thread. The method will return immediately if the current thread already owns the lock. This can be checked using methods isHeldByCurrentThread(), and getHoldCount().
ReentrantLock 由最后成功锁定但尚未解锁的线程拥有。当锁不属于另一个线程时,调用 lock 的线程将返回并成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。这可以使用方法 isHeldByCurrentThread() 和 getHoldCount() 进行检查。

The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation. Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock. Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.
此类的构造函数接受可选的公平参数。当设置 true 时,在争用情况下,锁倾向于授予等待时间最长的线程访问权限。否则该锁不保证任何特定的访问顺序。使用由许多线程访问的公平锁的程序可能会显示比使用默认设置的程序更低的总体吞吐量(即更慢;通常慢得多),但在获取锁的时间上具有较小的差异并保证不会出现饥饿。但请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得公平锁,而其他活动线程没有进展并且当前没有持有该锁。另请注意,不定时的 tryLock() 方法不遵守公平设置。如果锁可用,即使其他线程正在等待,也会成功。

It is recommended practice to always immediately follow a call to lock with a try block, most typically in a before/after construction such as:
建议的做法是始终在对 lock 的调用之后立即使用 try 块,最典型的是在 before/after 构造中,例如:

 
 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

In addition to implementing the Lock interface, this class defines a number of public and protected methods for inspecting the state of the lock. Some of these methods are only useful for instrumentation and monitoring.
除了实现 Lock 接口之外,此类还定义了许多 public 和 protected 方法来检查锁的状态。其中一些方法仅对仪器和监控有用。

Serialization of this class behaves in the same way as built-in locks: a deserialized lock is in the unlocked state, regardless of its state when serialized.
此类的序列化行为与内置锁的行为方式相同:反序列化的锁处于解锁状态,无论其序列化时的状态如何。

This lock supports a maximum of 2147483647 recursive locks by the same thread. Attempts to exceed this limit result in Error throws from locking methods.
该锁最多支持同一线程有2147483647个递归锁。尝试超过此限制会导致锁定方法抛出 Error 异常。