Java并发编程

Java并发编程 - 深入剖析ReentrantLock之公平锁

2019-03-14  本文已影响6人  HRocky

Java并发编程 - 深入剖析ReentrantLock之非公平锁重要点(第3篇)

前面3篇文章我们讲得是ReentrantLock作为非公平锁使用,在这个类中我们可以看到,除了无参构造方法外,还有一个有参的构造方法:

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

当fair设置为true是,sync被初始化为为FairSync,从名字看这是个公平的同步器,那么这个公平同步器又是怎么回事?

首先我们来比较一下FairSync和NonFairSync的lock方法:

NonFairSync

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

FairSync

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

通过上面的比较可以看到FairSync没有快速获取锁的逻辑,每次都是通过acquire方法的调用获取。

AbstractQueuedSynchronizer.java

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

现在来看看FairSync中tryAcquire的实现:

FairSync

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

与ReentrantLock作为非公平锁模式调用nonfairTryAcquire相比,多了下面的代码:

!hasQueuedPredecessors() 

这个方法的调用有什么作用?

看下这个方法:

AbstractQueuedSynchronizer.java

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

API解释如下:

Queries whether any threads have been waiting to acquire longer than the current thread. 

查询等待队列中是否有比当前线程等待获取锁的时间更长的线程存在。

这里&&之后的条件使用 || 来连接的,&&前面的条件满足了表示非空队列,
那么&&后面的条件组合有两个:true || (不用判断) 和 false || true 。

回到tryAcquire,与之前非公平的获取方式不同的是,每次线程获取锁不仅仅只看能否将state设置为1,还要判断当前队列中是否有比它等待锁时间更长的线程存在,如果有的话,那么它无法获取锁,而是入队列。

在我们之前的非公平锁分析中,我们说过队列中被唤醒的线程重新请求获取锁,这时候有新来的线程会与它争抢,导致它获取不到锁,只能再次重试获取锁。而我们这里的模式就不会出现这样的情况,新来的只能入队列。

先来先得,体现了公平性,这就是公平锁的由来。

公平模式:排队打饭,同学们都有素质,自动遵守排在队头的人先打饭,新来的自动排到队尾的规则。

非公平模式:排队打饭,已排队的人都有素质,但是这时候来了一个没素质的,跟排在队头的人争抢先打饭,于是他们打了一架,排在队头的同学打赢了,先打饭,新来的同学灰溜溜到队尾了;排在队头的同学打输了,新来的同学先打饭,扬长而去,排在队头的同学继续打饭(期望不会又来新的没素质的同学)。

这篇文章我们可以说只是简要得谈了一下公平锁,因为它的内部执行流程除了我们上面说的之外,其他的几乎跟非公平锁是一样的,所以就没必要再次深入源码进行分析。

非公平锁vs公平锁

通过前面的文章,我们讲解了非公平锁和公平锁,比较了一下它们的不同点。

ReentrantLock默认采用非公平的加锁方式。那么说明非公平锁应该是我们实际使用中比较用得多,也是推荐的方式,使用非公平锁会带来什么好处呢?

这里摘抄和整理《Java编程实践》中说明。

上面说了非公平锁的好处,那么是不是什么情况下都使用非公平锁呢?

不是的,当线程持有锁的时间相对较长或者说请求锁的平均时间较长的情况下,那么使用公平锁比较好。

因为我们上面说的是要利用队列中的等待线程唤醒但是还没获取锁的这段时间。但是在上面说的持有锁或请求锁时间相对较长,那么这种在线程正在唤醒中,还没有得到锁这种情况不太容易出现。

锁被占用的时间较长,新来的线程大多数情况下也是请求不到的,那么还是直接入队列比较好,避免了多个新来的线程的无用竞争。

在synchronized和ReentrantLock之间进行选择

这里摘抄自《Java并发编程实践》

在内部锁不能满足使用时,ReentrantLock才被作为更高级的工具。当你需要以下高级特性时,才应该使用:可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized。

上一篇 下一篇

猜你喜欢

热点阅读