java之Lock原理解析

在 java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、 ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异。

1.AbstractQueuedSynchronizer

1.AQS主要的作用就是维护一个CLH线程阻塞队列(数据结构为双向链表,通过内部类Node实现,每个Node通过prednext记录前后节点,并且还有个当前node的waitStatus
2.CLH是一个FIFO队列,AQS中有记录队首(head)和队尾(tail)的字段,当向队列插入一个Node的时候,插入到tail后面,如果出队列的时候,修改head。
3.每次竞争锁的时候,从队列中取出head进行竞争(但不一定保证会取得锁,比如非公平锁)。

2.ReentrantLock

1.Sync
ReentrantLock中有个抽象内部类Sync继承了AbstractQueuedSynchronizer, 而Sync又有2个实现子类FairSyncNonfairSync,而lock和和unlock方法主要是依靠Sync的具体实例来实现的。

2.1 lock()

以NonfairSync为例子
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

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

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

compareAndSetState(0, 1)主要是通过CAS的模式,如果当前没有竞争,则直接或者锁,否则,尝试获取(也就是acquire(1)方法)

然后我们再看看 acquire ( 1 )方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

首先通过tryAcquire()方法尝试获取,如果不能的话,则通过AddWaiter()方法,用当前线程生成一个Node放入队尾,而acquireQueued()则是一种自旋锁的实现方式。最后把当前线程interrupt。
AQS的实现精妙之一就是把tryAcquire延迟到子类去实现。因为公平锁和非公平锁的实现方式肯定是不一样。

非公平锁的tryAcquire()的函数代码是通过 nonfairTryAcquire()方法
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;
}
AQS中有个state字段,当没有线程竞争锁的即初始化的为0,主要目的是为了重入锁的实现。每次线程获取一个锁时候++,释放的时候--。
c==0 表示没有线程竞争该锁,如果c!=0并且当前线程就是获取锁的线程,只用修改state就行了。
acquireQueued()方法
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())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
看似就是一个无限循环,直到获得锁为止。其实 shouldParkAfterFailedAcquire ()方法中,通过前一个Node的waitStatus来判断是否应该把当前线程阻塞,阻塞是通过 parkAndCheckInterrupt()中的 LockSupport . park ( )实现
2.2 unlock
unlock中也是通过release方法实现
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
tryRelease和tryAcquire一样,也是延迟到子类(Sync)实现的。
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;
}
c==0的时候,才能成功释放锁,所以多次锁定就需要多次释放才能解锁。

释放锁之后,就会唤醒队列的一个node中的线程
private void unparkSuccessor(Node node) {
   
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

这 段代码的意思在于找出第一个可以unpark的线程,一般说来head.next == head,Head就是第一个线程,但Head.next可能被取消或被置为null,因此比较稳妥的办法是从后往前找第一个可用线程。貌似回溯会导致性 能降低,其实这个发生的几率很小,所以不会有性能影响






你可能感兴趣的:(java,java,lock)