美文网首页Java并发编程
Java并发编程 - 深入剖析ReentrantLock之公平锁

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

作者: HRocky | 来源:发表于2019-03-14 17:56 被阅读6次

    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 。

    • true || (不用判断) : 不是&&前面的条件满足表示非空队列了吗,那为什么还要判断h.next是否为null,因为可能出现代表线程的节点正在入队列,还没来得对h.next设置。
    • false || true :此时s代表head节点的下一个节点,这个节点代表的线程不是当前线程。

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

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

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

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

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

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

    非公平锁vs公平锁

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

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

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

    • 减少性能开销
      当线程请求锁而锁被其他线程占用的时候,线程会被挂起,之后会被重新唤醒。这里就涉及到线程的上下文切换问题,我们知道线程的上下文切换是需要操作系统调度的,如果大量线程出现被挂起然后又被唤醒,而且每个线程持有锁的时间比较小的话,那么就会带来巨大的性能开销。解决这种损耗的其中一个办法就是避免线程被挂起和唤醒,如果允许新来的线程可以跟之前队列中被唤醒线程争夺锁,而且争夺成功,那么它就不用入队列并被挂起,这样就减少了需要上下文切换的带来的性能损耗,而非公平锁就是这样处理的。允许"闯入式"得抢夺锁。
    • 提供系统处理的并发数(提高吞吐量)
      我们知道线程被唤醒之后,它并不是马上就运行的,也就是说挂起的线程重新开始与它真正开始运行,两者之间在竞争激烈的情况下会产生严重的延迟(需CPU调度)。如果新来的线程能利用好这个时间,在这个时间内加锁并解锁,那么就提供了系统处理并发的能力,单位时间内就能多多地执行线程。这样如果是公平地模式,锁被释放了而又只能队列中被唤醒的线程处理,白白地就浪费了唤醒到真正执行的这段时间,使用非公平获取锁方式就能很好地利用这段时间。

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

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

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

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

    在synchronized和ReentrantLock之间进行选择

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

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

    相关文章

      网友评论

        本文标题:Java并发编程 - 深入剖析ReentrantLock之公平锁

        本文链接:https://www.haomeiwen.com/subject/kngxmqtx.html