ReentrantLock从结构底层到原理一篇讲清

ReentrantLock

1.是什么

Reetrantlock是一个可以重复获得锁的一个互斥锁,它的加锁与解锁都是需要手动执行,也可以多次加锁,同时它还可以指定是由公平锁还是非公平锁实现。

2.内部实现

img

构造方法:

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

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

由构造方法可知,reentrantlock是由公平锁和非公平锁实现的,而公平锁和非公平锁都是继承实现AQS(AbstractQueuedSynchronizer)的模板方法和底层原理得以实现

AbstractQueuedSynchronizer(AQS)

//节点元素node的构成:
volatile Node prev; //指向前一个结点的指针
volatile Node next; //指向后一个结点的指针
volatile Thread thread; //当前结点代表的线程
volatile int waitStatus; //等待状态

//AQS其他的数据结构
private volatile int state;//Reentratlock中表示锁被同一个线程重入的次数
private transient Thread exclusiveOwnerThread;//标识锁是由哪个线程拥有

img

这里的头指针的头结点表示当前获得这个锁的线程,后面的节点表示想要获得这个锁的线程。这是一个先进先出的同步队列,获取锁失败的线程将构造自己的节点并追加到队列的尾部,并且阻塞自己,而当线程释放锁之后,也将尝试唤醒后续节点中处于阻塞状态的线程。

AQS的底层结构式lock实现的重要基础,对于公平锁和非公平锁来说,只是方法执行逻辑不同。

非公平锁NonfairSync

继承Sync,实现了lock和tryAcquire方法

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

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

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


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

lock方法是默认去用cas的方式去更新state和当前持有线程,如果AQS中state为0,也就是队列为空,那么设置state和当前线程就可以,如果不成功,就加入队列。

Reentratlock非公平锁的加锁流程

img

1.1,尝试获取锁首先获取AQS中state的值,如果为0,表示当前锁没有被任何线程持有,以cas的方式设置state的值,以获取锁,并设置exclusiveOwnerThread标识这个锁由哪个线程拥有

1.2 如果state值不为0,则看当前锁的持有线程是否是自己,如果是,就可以重入了,对state加1 就可以了,获取锁也成功,如果持有线程不是自己,返回失败,

2,线程获取锁失败以后,就会为当前线程创建一个新的节点,判断tail尾指针是否为空,如果为空,构建新的节点同时设置为首尾指针,如果不是也就是队列不为空,那么就要以尾插方式放入队列尾部。

//先将新建节点前置节点置为尾节点,然后以cas的方式更新tail指针,最后新建节点入队
node.prev = t;
   if (compareAndSetTail(t, node)) {
        t.next = node;
        return t;
   }

3,将新建的节点入队后,进入一个死循环,只有新建节点的前节点是头节点并且自己获取锁成功以后,将头结点设置为自己以后才能跳出循环,否者的话就要,并将前置节点置为SIGNAL阻塞(调用LockSupport方法)当前线程。

4,随着队列不断的出队,后面的节点的前节点总会是头结点,而自己也会获得锁,那么他也会被唤醒。

ReentratLock的解锁过程

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
        if (tryRelease(arg)) {//将state减1,如果减1后为0,返回true
            Node h = head;
    //防止队列为空,唤醒前节点必须是阻塞的,而节点阻塞前要设置前节点为SIGNAL
            if (h != null && h.waitStatus != 0)
               //唤醒线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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;
            //从为节点开始遍历,找到离头节点最近且节点状态为SIGNAL的节点
        }
        if (s != null)
            //使用LockSupport唤醒线程
            LockSupport.unpark(s.thread);
    }

img

当前线程将state值减1,减少为0后,就表示线程释放了锁,就要唤醒后继节点,使得他可以获得锁,而这个后继节点的状态waitStatus小于0时才可以被唤醒

公平锁FairSync

Reentratlock公平锁的加锁流程

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

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
  //相比较而言,公平锁的流程在cas更新state前多了一个判断队列中是否有更高优先级的节点
                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;
        }

相比较非公平锁来说首先

1,在lock方法中少了一步直接cas更新state的方法

2,在tryAcquire方法中加入了hasQueuedPredecessor判断

hasQueuedPredecessor判断如下

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

这里的判断判断了两种情况

1,头节点的后续节点为空

2,AQS的持有锁线程不是当前线程

为第一种要判断呢??因为当一个节点要加入队列时有三步:

  1. 待插入节点pre指针指向尾节点
  2. cas方式更新AQS尾指针
  3. 原尾节点的next指向新插入节点

第一个判断是为了排除前两步执行完成,但是第三步没完成的情况,也就是队列中除了头节点外,还有后续的第二个节点,它应该第二个获得锁,这种情况下,当前线程应该入队,等待锁。

第二个判断就是队列中有第二个节点且AQS持有锁的线程为其他线程

所以这个判断就是为了判断队列中是否有第二个节点,这个节点获取锁的优先级最高,应该让他先获得锁。

为什么要对第二节点做判断

由于底层实现为同步队列,队列也就是先进先出,越早进入队列的线程应该越先获得锁,这样才是公平合理的,但是在非公平模式下,我们知道它有两次cas方式去更新state值和更新AQS持有线程

  1. 在lock方法中首先就尝试去cas更新state值,更新成功的话,那么就获得锁成功
  2. 在nonfairTryAcquire方法中也有对state值cas方式去更新值的判断

那么这两次判断真的能获得锁吗?

在释放锁的流程中,线程将state更新为0,然后去唤醒后续节点,就是(LockSupport.unpark(s.thread))找到符合条件的节点解锁。

这就是两步,那么在多线程条件下,第一步执行完之后,此时恰好另外一个线程进入,看到state值为0,那么就可直接获得锁,更新完state和AQS持有线程后,就获得了锁。而这时刚刚解锁完的节点去唤醒的节点获取state值时发现不为0,继续阻塞等待,这也就造成了不公平。等的时间长不如进来的时间巧。从流程来说就是如下

img

那么在公平锁模式下,只要有第二节点的存在,后面进行的线程就必须要加入队列,而不能直接去cas的方式更新state值,对于多线程来说就是按加入时间长短来获取锁,也就实现了公平。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章