并发(三):ReentrantLock类

上文问题:为什么在DCL单例模式下,加了synchronized锁,代码块退出后,还要禁止指令重排序? 难道不是在持有锁的线程内,等重排序完之后,才会释放锁吗?

在DCL单例模式下, 使用volatile是保证指令重排序,创建对象分为三步:第一,给对象分配内存空间,第二给对象初始化,第三变量与内存空间建立链接,其中第二第三步不存在数据依赖,固可以重排序,当线程2判断if(instance==null),这个等号是比较内存地址值的,判断当前变量的内存地址值是不是为null,这里就涉及了字节码知识,当store指令执行时,会保证将线程内部的信息给写回主内存中,固线程1将内存地址和变量建联后,写回了主内存中,线程二在判断时恰好不为null,但对象还未初始化,线程二在get的时候可能会报错。

详细的原文地址并发(三):ReentrantLock类
在这里插入图片描述

大纲内容

  • 基础概念

  • ReentrantLock源码分析

  • lock()

  • unlock()

  • 条件等待队列

  • 生产者-消费者Demo

  • 问题1:为什么在unparkSuccessor()方法中,如果线程B是无效的,代码是从尾向前查找有效节点呢?

基础概念

ReentrantLock是实现了AQS框架,跟synchronized关键字一样,是保证在并发场景下,同一时间只能有一个线程访问临界资源,保证了线程不安全性问题,ReentrantLock是手动加锁和手动释放锁,有一个阻塞队列和多个条件等待队列组成,支持加锁的公平性。

AQS框架的特点:一个同步阻塞队列和多个条件等待队列。Synchronized的特点:一个同步阻塞队列和一个条件等待队列。

AQS在内部维护了被volatile修饰的state变量,通过getState(),setState(),compareAndSetState()三个方法来控制state的改变。
来看看AQS中最重要的几个字段

private volatile int state;

当state=0时,跟synchronized底层的Monitor对象中的count字段一样,标识当前锁处于空闲状态,其他线程可以尝试来获取锁。
当state=1时,表明有线程已经持有锁,其他线程只能阻塞在同步阻塞队列中等待。ReentrantLock锁是支持可重入的。

private transient Thread exclusiveOwnerThread;

跟synchronized中的owner字段一样,表示当前哪个线程持有该锁的使用权

总结:当state=0时,表示当前锁处于空闲状态,当线程A使用lock()成功时,会调用tryAcquire()把该锁的state进行自增+1,其他线程去lock()都会被队列给阻塞住,同时入同步阻塞队列,只有当线程线程A释放完锁,并且state=0时,其他线程才能尝试去获取锁。

来看看AQS中Node中最重要的字段,

//锁的状态
volatile int waitStatus;
//当前锁的引用,即这个锁是谁
volatile Thread thread;

waitStatus=-3,表示当前节点处于共享模式下,前继结点会唤醒后续的所有结点。
waitStatus=-2,表示当前节点位于条件等待队列上,当持有锁的线程调用Condition中的waite()方法时,当前线程会从同步阻塞队列移动至条件等待队列中,同时释放锁,等待获取同步锁。
waitStatus=-1,最重要的,当节点状态为-1时,说明当前线程节点有能力去唤醒后续的一个节点。 waitStatus=0,默认值,未发生锁争取
watStatus=1时,表示当前节点争夺锁资源取消了,在中断/超时的情况下,会将节点的状态修改为1,处于该状态的节点后续都不会再发生变化。

总结:当wait为负数时,是有效等待状态,为正数,说明线程节点处于无效状态。

ReentrantLock源码分析 Lock()

假设场景:A,B,C三个线程尝试去抢ReentrantLock锁,锁是如何控制互斥性的。

ReentrantLock lock = new ReentrantLock();
lock.tryLock();

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

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

默认是使用的非公平锁,如果想使用公平锁,则构造函数中传入True即可。
当线程A调用lock()方法时, 使用CAS算法来设置state的值,当设置成功时,则说明线程A获取锁成功,同时拥有锁使用权的线程设置成当前线程A,

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

核心:此时线程B和线程C尝试CAS算法修改state的值设置失败,固会走到acquire(1)中。

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

对于线程B而言,首先会尝试调用tryAcquire()方法,目的是为了想当一次舔狗,(因为每个线程持有锁的时间并不会很长),想看看线程A有没有释放完,如果线程B当舔狗成功了,导致拿到了线程A释放的锁,返回true,后续的判断逻辑也不会走。

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(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;
}

线程B当了一次舔狗,会进行两层判断:
1:首先拿到state的最新值,判断是否为0,如果为0,说明线程A已经释放了锁,线程B调用cas算法来修改state的值,如果修改成功,则把锁的使用权设置成当前线程B,返回true,说明线程B获取锁成功。
2:ReentrantLock是支持可重入的,如果state最新值不为0,判断当前持有锁的线程是不是当前线程,如果是的,则state自增+1,同时返回true。3:如果判断都不是,说明线程B获取锁失败了,返回false,固才会走后面的判断

小知识点:if(A&&B),如果A为false,if后面的B判断是不会走的,如果A为true,后续的B判断才会执行。if(A||B),如果A为true,B都不用判断,如果A为flase,B才会判断。
锁的使用权线程和当前锁的线程相比较,跟synchronized中的偏向锁拿MarkWord中的线程Id和当前锁的线程Id相比较,异曲同工之妙。

回头看线程B调用的方法

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//第一个条件: tryAcquire(arg)返回的是false,
//固会走到下一个条件判断acquireQueued(addWait(Node.EXCLUSIVE),arg)))
//第二个条件: 分解成以下两小步解析
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

addWaiter(Node node)方法中, 参数是我们的Node节点,前面也介绍了这个Node中存在waitStatus和thread两个重要的属性。此方法的目的是:将获取锁失败的线程封装成Node节点,加入同步阻塞队列(双向链表)中等待,同时返回当前Node节点。
1:将当前线程Thread.currentThead()封装成一个Node节点,标注是互斥Node.EXCLUSIVEW状态,(AQS的节点有两类,一类是互斥,一类是共享)
2:线程B第一次进来,tail是默认值为null,所以会先执行enq(Node)方法

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

3:第一次循环,tail为null,所以会初始化一个空Node节点作为头节点,然后将tail指向头结点

第二次循环,t先指向tail节点,把线程B中的前驱节点设置为t,同时使用cas算法将node节点设置成尾节点,同时返回线程B节点\

4:当addWaiter()方法执行完后,返回的封装Node节点,调用acquireQueued(Node node,int arg)

// node节点为3中返回的节点,当前返回的是加入链表中的Node线程B,
// arg为1。
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);
    }
}

acquireQueued()目的是为了再一次当舔狗,当线程B的前驱节点若为head节点,(因为线程持有锁的时间很短,还是想再次看看线程A又没有释放锁),线程B尝试去获取锁,如果获取成功,则返回false。
假设线程A没有释放锁,会执行shouldParkAfterFailedAcquire(p, node)和parkAndCheckInterrupt()两个方法。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire(p, node)方法是判断线程B的前驱节点的waitStatus是不是为-1.如果为-1,则返回true,线程B初次进入这个方法,他的前驱节点默认是0,所以会走else中的方法,用cas算法把前驱结点的waitStatus状态改为-1.

当waitStatus为-1时,代表着当前节点能唤醒后续的第一个尾节点,其实可以把第一个空的头节点假设为获取锁的线程A,把他的waitStatus设置为-1,当他释放锁时,才能唤醒后续的第一个尾节点,这样可能好理解。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

如果设置当前节点的前驱节点的waitStatus状态为-1时,则park住当前线程B。
到这里,线程B才结束了,线程C的流程跟线程B的流程是一样的,简述一下线程C的流程1:线程C调用tryAcquire()方法,主要是判断state的状态是否为0,持有锁的线程者是否是自己(线程C),假设线程A没有释放完,固返回false,走2的流程。

这里存在一个公平锁和非公平锁的小知识点,假设线程C拿到了锁,此不是对线程B中的线程不公平,线程B辛辛苦苦入队列,反而给线程C拿到了。公平锁会先去检查队列中有没有等待的Node节点,如果有,自己乖乖先入队等着。

2:调用addWaiter(Node node)方法,将线程C封装成Node节点,加入队列中,同时返回当前线程C节点,这里就不用初始化enq(node),假设线程B还在队列中,线程C会直接用尾插法插在线程B的后面。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

3:调用acquireQueued(Node node)方法,判断线程C的前驱节点是不是头结点 ,由于线程C的前驱节点是线程B,固会直接走到shouldParkAfterFailedAcquire(p, node),尝试把线程B的waitStatus状态设置成-1.同时再第二次循环时调用parkAndCheckInterrupt()阻塞线程C。\

这里有个小细节:为什么我要提起两次循环调用这个概念,因为shouldParkAfterFailedAcquire(p,
node)中把前驱节点设置成了-1后,返回false出去的,但acquireQueued中是死循环,还是想继续当一次舔狗,但这次如果还没有获取成功,才会parkAndCheckInterrupt()阻塞线程C。最终阻塞队列中的结果

unLock()

还是以上面结果为场景,其实线程A获取锁,线程B和线程C在阻塞队列中等待

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

1:当线程A想要释放锁unlock(),底层先会调用tryRelease()方法,只有持有锁的线程才能去释放锁,否则会抛出异常,如果线程A多次重入,则tryRelease()调用多次自减1,当c==0时,设置state为0,同时设置持有锁线程为null,返回true。
2:当线程A成功释放锁,取队列中的头结点,若头节点状态不为空,会执行unparkSuccessor()方法

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

取头结点的waitStatus状态,如果为负数,说明是有效等待状态,则尝试把头节点的waitStatus设置为0,找到头结点的尾节点线程B,如果线程B节点为空或者线程B是无效等待状态(只有超时或中断,才会为无效等待状态)。则尝试找线程C,假设线程B有效,则唤醒线程B,假设线程B无效,线程C有效,则唤醒线程C,

注意在lock中的acquireQueued()方法,是个死循环,线程B只是在死循环中被park住了,此时唤醒线程B,则线程B在acquireQueue()方法中被唤醒,又可以执行死循环,然后尝试取获取锁。

问题1:为什么在unparkSuccessor()方法中,如果线程B是无效的,代码是从尾向前查找有效节点呢?

未获取锁的线程入队时,每次都是使用prev来强关联队列中已存在的节点,可以让prev前驱链保证强一致。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值