ReentrantLock源码分析

2022-09-21 18:15:50 浏览数 (1)

1、ReentrantLock介绍

ReentrantLock就是一个互斥锁。类比sync。套路都类似,只不过sync是基于对象头和类实现的,ReentrantLock基于AQS实现的。

代码语言:javascript复制
static ReentrantLock lock = new ReentrantLock();
publicstaticvoidmain(String[] args){
    lock.lock();  //  sync(){//  ---  业务要求原子性
    lock.unlock();  //  }
}

2、AQS分析

AQS是JUC包下的一个基类,就是JUC包下提供的一个抽象类AbstractQueuedSynchronizer

AQS中有两个核心内容:

  • state属性:state基于volatile修饰,并且采用CAS的方式去修改,这样state就保证了原子性,可见性,有序性。 同一时间点,只有会有一个线程成功的修改state。修改过程就是将state从旧值修改为新值
  • 双向链表:线程在竞争资源时,可能会出现没有拿到资源,线程不能一直CAS,因为频繁的CAS会造成浪费CPU资源,线程需要挂起。挂起就需要考虑线程存在哪。 线程会存储在Node对象中,并且没有获取到资源的线程可能或有多个,多个Node就会组成一个双向链表。

ReentrantLock加锁流程(一)(重要)

3、ReentrantLock源码剖析-lock方法

代码语言:javascript复制
// 非公平锁finalvoidlock(){
    // 直接CAS尝试将state从0改为1,如果成功,返回trueif (compareAndSetState(0, 1))
        // 获取锁资源成功,直接将exclusiveOwnerThread设置为当前线程,代表当前线程持有着锁资源
        setExclusiveOwnerThread(Thread.currentThread());
    else// 修改state失败,执行acquire方法
        acquire(1);
}

// 公平锁finalvoidlock(){
    // 执行acquire方法
    acquire(1);
}

4、ReentrantLock源码剖析-acquire方法

代码语言:javascript复制
publicfinalvoidacquire(int arg){
    // tryAcquire(公平&非公平),再次尝试拿锁资源。 如果拿到返回true,没拿到返回falseif (!tryAcquire(arg) &&
        // 没拿到锁资源,执行addWaiter以及acquireQueued// addWaiter:拿锁失败,将当前线程封装为Node对象,并且添加到AQS双向链表的末尾// acquireQueued:acquireQueued中会根据情况尝试再次执行tryAcquire,如果多次尝试依然拿不到,挂起线程(WAITING)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
}

6、ReentrantLock源码剖析-tryAcquire方法

代码语言:javascript复制
// 非公平锁实现finalbooleannonfairTryAcquire(int acquires){
    // 获取当前线程final Thread current = Thread.currentThread();
    // 拿到stateint c = getState();
    // 判断为0,为0就CASif (c == 0) {
        // 非公平锁直接CAS尝试将state从0改为1if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            // 代表锁资源拿到~~returntrue;
        }
    }
    // 有线程持有当前锁,查看是否是锁重入操作elseif (current == getExclusiveOwnerThread()) {
        // 如果是当前线程持有锁资源,进到else if,执行锁重入流程// 对state   1int nextc = c   acquires;
        // 判断锁重入是否达到上限if (nextc < 0) 
            // 凉凉~thrownew Error("Maximum lock count exceeded");
        // 如果没有达到重入上线,修改state即可
        setState(nextc);
        returntrue;
    }
    // 没拿到锁,也不是锁重入,凉凉~returnfalse;
}

// 公平锁实现protectedfinalbooleantryAcquire(int acquires){
    final Thread current = Thread.currentThread();
    int c = getState();
    // state为0时,公平锁的操作if (c == 0) {
        // 查看AQS双向链表中是否有排队的Node// 如果没有排队的Node,直接抢锁// 如果有排队的Node,查看当前线程是不是优先级最高的Node的线程if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            returntrue;
        }
    }
    // 锁重入操作。。elseif (current == getExclusiveOwnerThread()) {
        int nextc = c   acquires;
        if (nextc < 0)
            thrownew Error("Maximum lock count exceeded");
        setState(nextc);
        returntrue;
    }
    returnfalse;
}


publicfinalbooleanhasQueuedPredecessors(){
    // 拿到头和尾
    Node t = tail; 
    Node h = head;
    Node s;
    // 如果头尾不相等,证明node个数 > 1return h != t && 
        // 有Node在排队// 拿到head的next,赋值给s,s就是优先级最高的节点// 排在第一位的Node中的线程是不是当前线程?
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

7、ReentrantLock源码剖析-addWaiter方法

代码语言:javascript复制
// addWaiter将当前thread封装为Node,并扔到AQS的双向链表中// mode:代表互斥。private Node addWaiter(Node mode){
    // 将当前thread封装为Node
    Node node = new Node(Thread.currentThread(), mode);
    // 拿到tail
    Node pred = tail;
    // 如果tail不为null,说明双向链表中有Node节点if (pred != null) {
        // 将当前Node扔到AQS双向链表中,
        node.prev = pred;
        // 修改tail指向时,是CAS操作if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果出现并发或者tail还没有初始化,执行enq方法
    enq(node);
    return node;
}

private Node enq(final Node node){
    // 死循环for (;;) {
        Node t = tail;
        // 如果tail为null,先初始化一个伪的head节点if (t == null) { 
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 你看上面代码
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

8、ReentrantLock源码剖析-acquireQueued方法

代码语言:javascript复制
// 准备挂起线程// node: 当前线程Node// arg: 1finalbooleanacquireQueued(final Node node, int arg){
    // 拿锁失败标识:失败了么?是boolean failed = true;
    for (;;) {
        // 拿到当前节点的上一个节点final Node p = node.predecessor();
        // true:当前Node的位置是head.nextif (p == head && 
                // 如果是head的next,再次执行tryAcquire
                tryAcquire(arg)) {
            // 拿锁成功!当前Node变为head,并且设置thread和prev为null// 拿到锁的变伪节点了
            setHead(node);
            p.next = null; // help GC
            failed = false;
            returnfalse;
        }
        // 没有拿到锁资源,或者无法执行tryAcquire// 查看能否挂起线程,如果可以,返回true// 如果prev节点是1,代表取消,此时需要往前找到有效节点// 如果prev节点状态不是-1,需要先改为-1// 如果prev节点状态是-1,当前节点可以挂起// 为什么这么做?希望可以多执行几次tryAcquire,避免线程的挂起,因为线程挂起和唤醒需要用户态和内核态的切换,消耗性能if (shouldParkAfterFailedAcquire(p, node) &&
                // 挂起线程,执行Unsafe类中的park方法,挂起线程
                parkAndCheckInterrupt())
    }
}

// 查看当前线程能否挂起// 每一个Node都会有一个状态,封装在waitStatus属性中// 状态分为5个,只关注前三个/*
1:代表节点取消,不排了
0:代表刚来排队,初始化状态
-1:当前节点的next节点挂起了(park了,线程WAITING)
-2:Condition,await或者signal了
-3:为了解决JDK1.5的BUG,BUG是在Semaphore产生的。
*/privatestaticbooleanshouldParkAfterFailedAcquire(Node pred, Node node){
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        returntrue;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    returnfalse;
}

9、ReentrantLock源码剖析-release方法

代码语言:javascript复制
publicfinalbooleanrelease(int arg){
    // 对state - 1,如果state减到0,代表锁资源完全释放干净if (tryRelease(arg)) {
        Node h = head;
        // 准备唤醒线程~~if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        returntrue;
    }
    returnfalse;
}

AQS为什么不用单向链表不可以吗?

因为排队的Node会出现取消操作,取消操作需要将当前节点的上一个节点的next执行当前节点的下一个节点,为了找到当前节点的上一个节点,需要从头遍历,成本太高了。

释放后,是拿链表中第一个?

释放的是head.next。就是第二个。

node对象 数据结构是咋样的?

Node对象里面就存储了几个属性,Node本身组成了一个双向链表,有prev,next。

为什么ReentrantLock中,head指向的Node是伪结点?

因为第一个节点是伪节点,伪节点不绑定任何线程,只有head.next后面的才绑定线程

head.next后面的才绑定线程?

因为在执行release方法时,需要判断是否需要唤醒线程。通过head节点的状态来判断后续节点是否需要被唤醒,如果head节点的状态是-1,我才需要执行唤醒后面挂起的线程。

ConcurrentHashMap在查询数据时,针对并发情况(有线程在写数据),是如何查询的?

查询数组上的数据,并发没问题

查询链表上的数据,并发也没问题

但是查询红黑树的上的数据就有问题了,因为如果存在写数据,红黑树会为了保证平衡,出现左旋和右旋操作,导致红黑树结构变化,查询就不能直接查询红黑树,他会保留一个双向链表,这是会查询双向链表

ConcurrentHashMap的size方法没有加锁,如何保证数据不出问题?(ConcurrentHashMap的addCount如何实现的)

ConcurrentHashMap中采用了类似LongAdder方式记录元素个数,内部除了基本的baseCount之后,还会有一个CounterCell[]存储着元素个数,执行size方法值需要将baseCount拿到,并且遍历CounterCell数据拿到全部数据即可。

线程池中执行addWorker时,为什么会添加一个任务为null的非核心线程?

因为执行这个方法前,会判断阻塞队列有任务,但是没有工作线程,这就会导致阻塞队列中的任务没有工作线程可以处理,一直卡在这个位置,导致任务阻塞了,所以会添加一个空任务的非核心线程处理阻塞队里的任务

0 人点赞