ReentrantLock源码分析


ReentrantLock源码分析

Author:郑金维

一、ReentrantLock介绍

ReentrantLock,synchronized

synchronized同步锁,锁升级,但是synchronized只有升级,没有降级,锁竞争激烈,不推荐synchronized

ReentrantLock同步锁,内部是基于CAS和AQS实现的,在锁竞争比较激烈时,推荐使用ReentrantLock

ReentrantLock基本使用(可重入锁):

获取锁:ReentrantLock对象.lock();

释放锁:ReentrantLock对象.unlock();

二、CAS、AQS扫盲

2.1 CAS(乐观锁实现方式)

Compare And Swap,是针对一个值进行修改(原子性)

在Java中提供了Unsafe类实现了CAS的操作。

ABA问题:添加版本号。

CAS自旋次数过多:

  • synchronized基于自适应自旋锁解决。
  • LongAdder基于Cell[],在CAS实现失败,将数据扔到Cell[]中,后期再添加

劣势:CAS只能保证修改一个值的时候,是原子性的。CAS无法直接锁住一段代码。

2.2 AQS

AQS是什么:就是Java中的一个类,名字:AbstractQueuedSynchronizer

AQS是JUC中的一个基础类(并发基础类),很多其他类都是基于AQS实现的:

  • ReentrantLock
  • CountDownLatch
  • 信号量
  • 阻塞队列~
  • 线程池的Worker类

AQS中维护着一个非常重要的属性:state

还维护着一个双向队列:Node类

image.png

三、加锁源码分析

3.1 加锁流程

image.png

如果在线程B被唤醒之后:

  • 非公平锁,线程B会和其他线程一起竞争锁资源
  • 公平锁,其他线程需要先到AQS排队

3.2 lock源码

实现方式就是在ReentrantLock的有参构建中追加,默认false为非公平,true为公平

非公平锁:

final void lock() {
	// 先抢一波锁。
    if (compareAndSetState(0, 1))
		// 如果成功的将state从0改为1,代表拿到锁资源,那就将exclusiveOwnerThread设置为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
		// 没抢到,执行acquire方法
        acquire(1);
}

公平锁

final void lock() {
	// 执行acquire方法
    acquire(1);
}

ReentrantLock竞争锁资源就是以CAS的方式,尝试将state从0改为1,修改成功,获取锁资源成功

3.3 acquire源码

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

非公平锁:

// 非公平锁!
final boolean nonfairTryAcquire(int acquires) {
    // 拿到当前线程~
    final Thread current = Thread.currentThread();
    // 获取state
    int c = getState();
    // 如果state为0,就再次以CAS的方式尝试拿到锁资源
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 当前是锁重入操作!将state+1,即为重入ok,同时释放锁也需要释放多次,释放锁是对state - 1
    else if (current == getExclusiveOwnerThread()) {
        // state + 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow      2进制的符号位,加了1,代表是负数。代表超过锁重入线程
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        // 锁重入成功,返回true
        return true;
    }
    return false;
}

公平锁跟上述代码基本一致,有一个区别,当判断c == 0成功后

  • 公平锁需要先尝试排队执行hasQueuedPredecessors()方法,如果需要排队,就排队去
  • 如果不需要,那就直接CAS获取锁资源
3.3.2 addWaiter方法

将当前没有获取到锁资源的线程,封装为Node,开始排队

// 将当前没有获取到锁资源的线程,封装为Node,开始排队
private Node addWaiter(Node mode) {
    // 将当前线程封装为Node
    Node node = new Node(Thread.currentThread(), mode);
    // 获取了tail
    Node pred = tail;
    // 如果tail不为null,说明队列中有节点!
    if (pred != null) {
        // 当前Node的上一个是之前的tail节点
        node.prev = pred;
        // 将tail指向当前节点
        if (compareAndSetTail(pred, node)) {
            // 将pred的next节点,指向当前节点
            pred.next = node;
            return node;
        }
    }
    // 证明tail指向null,说明队列没Node
    enq(node);
    // 返回当前节点
    return node;
}
private Node enq(final Node node) {
    // 死循环
    for (;;) {
        // 重新获取tail
        Node t = tail;
        if (t == null) { 
            // tail还是指向null(第一个来排队的)
            // 设置一个空节点作为head和tail
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // tail不是null,说明有并发操作,并且有人排到了我的前面
            node.prev = t;
            // 跟之前操作一样,插入到队列尾巴
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
3.3.3 acquireQueued方法

如果当前节点的上一个节点是head,尝试获取锁资源,如果不是方法会将当前线程进行挂起。

final boolean acquireQueued(final Node node, int arg) {
    // 声明变量,
    boolean failed = true;
    for (;;) {
        // 拿到当前节点的prev节点,命名为p
        final Node p = node.predecessor();
        // p是head头节点,当前节点就再次尝试获取锁资源
        if (p == head && tryAcquire(arg)) {
            // 当前节点获取锁资源成功,设置当前节点为head,之前head等待GC回收
            setHead(node);
            p.next = null;
            failed = false;
            return interrupted;
        }
        // 查看是否需要挂起当前线程,如果可以挂起
        if (shouldParkAfterFailedAcquire(p, node) &&
            // 挂起线程,执行了LockSupport.park()挂起线程
            parkAndCheckInterrupt())
    }
}
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

waitStatus:new的Node默认值为0
-1: 正常
1:凉凉了
-2,-3:不正常,但是可以改~~~
// 判断当前线程的上一个节点的状态是不是-1
// 如果是-1,当前线程可以阻塞
// 如果是1,就往前找,找状态为-1的
// 如果不是-1,也不是1,那就上一个节点状态设置为-1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == -)
        return true;
    if (ws == 1) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

四、释放锁源码分析(白给的)

将state - 1,如果减完之后,state是0,unpark唤醒正在排队,并且离head最近的线程

public void unlock() {
    // 公平和非公平都走当前方法
    sync.release(1);
}
public final boolean release(int arg) {
    // 尝试释放
    if (tryRelease(arg)) {
        // 锁释放成功
        // 拿到head
        Node h = head;
        // 排队的队列中,有head,并且头的状态正常!!!
        if (h != null && h.waitStatus != 0)
            // 唤醒挂起的线程!
            unparkSuccessor(h);
        return true;
    }
    // 释放失败,还得释放几次~~~~
    return false;
}
// 针对可重入锁的操作。
protected final boolean tryRelease(int releases) {
    // 拿state的值进行-1后的值,赋值给c
    int c = getState() - releases;
    // 如果释放锁的线程不是拥有锁的线程,抛出异常。
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 标识
    boolean free = false;
    if (c == 0) {
        // 如果state-1之后为0,代表释放成功
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

// 唤醒队列中排队的线程
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 获取head节点的next
    Node s = node.next;
    // 如果下一个为null,或者下一个节点的状态为取消
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后往前,找到节点状态正常的,并且离head最先的节点。(思考问题:为什么从后往前找节点)
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果离head最近的节点,不为null
    if (s != null)
        // 唤醒线程!
        LockSupport.unpark(s.thread);
}

(思考问题:释放阻塞线程时,为什么从后往前找状态正常的节点)

posted @ 2022-05-07 21:41  追云逐梦  阅读(67)  评论(0编辑  收藏  举报