重学多线程(九)—— 再谈锁机制

前言

《重学多线程(三)—— 锁》一文比较详细地介绍了 Java 中的所机制,博主最近在工作中使用锁时,经常思考几个问题——公平锁和非公平锁如何具体实现、ReentrantLock 默认情况下是否公平、共享锁和排它锁具体如何实现,现在正好有时间重新梳理一下这块内容。

锁的基础知识

我们日常开发过程中提到的锁,一般指的是实现了 java.util.concurrent.locks 包中 Lock 和 ReadWriteLock 两个接口的实现类,狭义的角度来说就是上述包中 ReentrantLock 和 ReentrantReadWriteLock 两个类,本文也是基于这两个具体类展开叙述。

而锁的具体实现依靠的就是 AQS (AbstractQueuedSynchronizer)队列同步器,一般通过继承同步器并实现它的抽象方法的方式来管理同步状态。

公平锁与非公平锁

我们先来看一下ReentrantLock的构造方法,

    public ReentrantLock() {
        sync = new NonfairSync();
    }

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

看了源码之后,发现以前对于 ReentrantLock 的认识有点想当然了,默认情况下,ReentrantLock 居然是非公平的,那为什么是非公平的呢?

正常情况下,一个线程释放锁然后唤醒等待队列中的其他线程,被唤醒的线程需要经历请求锁、获得锁、线程执行这一系列步骤,如果这段时间内,另一个新的线程获得、使用以及释放了这个锁,那么既不影响原先被唤醒的线程,同时吞吐又得到了提升,岂不爽哉!

但仔细分析之后可以看出,上述原因建立在“插队”线程需要“快进快出”这个基础之上,如果该过程时间过长,不但吞吐不能得到提升,还会使得部分线程出现因长时间不能获得锁造成的“饥饿”现象。

简单来说,公平锁和非公平锁的区别就在于新来的线程能否允许“插队”。那么内部具体怎么实现的呢,我们一起看一下源码。

//ReentrantLock 的 lock 方法
public void lock() {
    //委托给了 sync 对象
    sync.lock();
}

ReentrantLock 将 lock 方法的具体实现委托给了 sync 这个对象的 lock 方法。sync 对象根据 ReentrantLock 的构造方法参数分为 FairSync 和 NonfairSync 两种具体类,我们先看 FairSync 的 lock方法。

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

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

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

我们再来看一下 NonfairSync 的 lock 内部实现过程。

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

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

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

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        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;
}

两相对比,可以看出差别来了,对于公平锁而言,每当线程去请求锁时,先要看一下是否有线程在排队等待(公平锁的 tryAcquire 方法中增加了 hasQueuedPredecessors() 方法的判断),有的话那就老老实实得去排队等待,严格按照FIFO的顺序,而非公平锁而言,请求线程更像是一个“投机分子”、需要见缝插针般去“碰运气”,当然如果靠投机无法获取锁,那也就需要老老实实排队等待。

共享锁和排它锁

共享锁和排它锁更多的是一种理论模型,本文以ReentrantReadWriteLock(可重入读写锁)为例,阐述相关内容,实际开发过程中,共享锁和排它锁其实已经退化为我们常说的读锁和写锁。ReentrantReadWriteLock 实现了 ReadWriteLock 接口,通过readLock()方法和writeLock()方法获取读锁和写锁。

先来看一下 ReentrantReadWriteLock 的构造方法,

public ReentrantReadWriteLock() {
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

static final class FairSync extends Sync {

    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

static final class NonfairSync extends Sync {
    final boolean writerShouldBlock() {
        return false; 
    }

    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}

默认情况下,ReentrantReadWriteLock 也是非公平的,但是 FairSync 和 NonfairSync 的区别在于 writerShouldBlock 和 readerShouldBlock 两个方法的具体实现,这两个方法的作用在于不同模式下当前读线程或者写线程是否被阻塞,具体逻辑,后文再述。

我们再来看一下读锁的获取过程,

public void lock() {
    sync.acquireShared(1);
}

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    //排他锁重入判断
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        } else if (readerShouldBlock()) {
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

公平模式下,readerShouldBlock 方法和 writerShouldBlock 方法取决于当前等待队列是否有排队线程,非公平模式下,readerShouldBlock 方法取决于等待队列中头结点的下一个节点是否为独占模式,而writerShouldBlock 方法直接返回false。

读锁的获取过程大体过程如下:
- 当前的写锁是否未被占有(AQS state变量低16位为0) 或者当前线程是否为写锁占有的线程;
- readerShouldBlock()方法返回false ;
- 当前读锁占有量小于最大值(2^16 -1) ;
- 成功通过CAS操作将读锁占有量+1(AQS的state高16位同步加1)

再来看一下写锁的获取过程,

public void lock() {
    sync.acquire(1);
}

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

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

相对而言,写锁的获取过程简单多了,大体过程如下:

  • 检查读锁是否未被占用(AQS state高16位为0) ,写锁是否未被占用(state低16位为0)或者占用写锁的线程是否为当前线程;
  • writerShouldBlock()方法返回false,即不阻塞写线程;
  • 当前写锁占有量小于最大值(2^16 -1),否则抛出Error(“Maximum lock count exceeded”) ;
  • 通过CAS竞争将写锁状态+1(将state低16位同步+1)

总结

本文针对博主日常开发中遇到的问题做了比较详细的解释,“纸上得来终觉浅,绝知此事要躬行”,只有通过理论和实践相结合,才能提升自身的技术水平。

猜你喜欢

转载自blog.csdn.net/tjreal/article/details/80317406
今日推荐