java并发-----浅析ReentrantLock加锁,解锁过程,公平锁非公平锁,AQS入门,CLH同步队列

前言

为什么需要去了解AQS,AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件

本文所有源码基于JDK9
目的:掌握大概的流程/框架
适用人群:初学者想了解些源码的
不适合:想深入了解的

ReentrantLock-非公平锁

我们在实际中一定会用到ReentrantLock的lock操作,那么它的实现究竟是怎样的?我们以重入锁作为切入点。

1、构建锁,获得锁对象

//锁的声明
private final Sync sync;
// 构造锁,默认非公平
public ReentrantLock() {
sync = new NonfairSync();
}

2、lock方法,调用sync这个锁对象

    public void lock() {
        sync.acquire(1);
    }
    // 来自AQS
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

关键性的四个方法:

1、tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。

2、addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。

3、acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。

4、selfInterrupt:产生一个中断。

tryAcquire

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

加锁,是通过NonfairSync的这个方法实现的,但是NofairSync并没有它的实际代码。真正实现的是它的父类Sync。

nonfairTryAcquire(int 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;
        }

getState(),这个方法在AQS中,用户获取锁的状态值。

    /**
     * Returns the current value of synchronization state.
     * This operation has memory semantics of a {@code volatile} read.
     * @return current state value
     */
    protected final int getState() {
        return state;
    }

下面来解释一下state在独占锁的作用是什么,结合下nonfairTryAcquire(int acquires)。

  • 首先第一个if条件,state = 0 的时候代表着,当前锁没有被某个线程占用,然后通过CAS操作设置线程的状态为值1,并且把当前线程设置为独占锁的拥有者。
  • 第二个if,当state!=0的时候,代表这个锁已经被别的线程占着了,就判断,这个是不是这个锁的拥有者,如果是的话,锁state就+1,这就巧妙地设置了重入锁的。每次+1的机制

CLH同步队列

在介绍addWaiter前,先来看一下CLH同步队列,就看着大佬们在说CLH但是少有人说他是啥,我解释一下啊

The wait queue is a variant of a “CLH” (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks

1、名称:CLH 由三个人名字组成 (Craig, Landin, and Hagersten)
2、基本数据结构:基于FIFO双端链表
3、用途:用于等待资源释放的队列。也就是等待锁释放的队列

注意:由于笔者是初学者,觉得CLH队列中的等待状态转换略微复杂,故意跳过。只看它们的方法,并没有特别深入

addWaiter

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }

与JDK8不同的地方是,addWaiter直接使用自旋(无限循环)去完成入队的操作,而不是调用enq,实现的内容和enq差不多。下面给出enq的代码

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(Node node) {
        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return oldTail;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }

注意addWaiter的返回值,是新加入的节点下面有用

acquireQueued

    final boolean acquireQueued(final Node node, int arg) {
        try {
            // 中断标识
            boolean interrupted = false;
            // 自旋
            for (;;) {
                final Node p = node.predecessor();
                // 如果当前线程节点的前驱节点是头结点,并且尝试获得锁成功
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                // 如果获取锁失败了,就进入挂起。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

关于挂起和唤醒,就先不看了,主要还是理解下流程。过多的细节会拖慢新手的学习之路,一定要记住这个!!

RenentrantLock-公平锁

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

公平锁和非公平锁在于第一个判断条件中的,hasQueuedPredecessors()这个方法。

    public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        // 头节点不是尾节点
        // 第一个节点不为空
        // 当前节点是头节点
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

从源码我们可以验证,公平和非公平的标准是否是按照队列的顺序进行锁的获取的原理。

小结:
到这里,加锁基本上就是结束了,我们忽略了挂起和唤醒这种复杂的操作。加锁的操作中,阻塞队列是由AQS使用CLH进行维护的。ReentrantLock,的同步操作主要还是依赖于AQS。

释放锁

    public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
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;
}

调用Sync的tryRelease,减小state值,然后uppark进行唤醒下一个节点PS,笔者忽略了唤醒的操作。

总结

本文针对和我一样刚入门不久的新手,从整体上以重入锁的加锁和解锁,去了解了一些AQS的CLH队列的基本内容,未涉及深层次,顺便看了下公平和非公平的实现。算作是一种了解把,个人感觉太细节的东西,新手看了也没用。还是从宏观上把握把握,会用,然后了解点源码,方便以后再来学习。

猜你喜欢

转载自blog.csdn.net/qq_41376740/article/details/80669851