深入了解JUC的ReentrantLock原理

Lock (Synchronized)

在Lock接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。但是synchronized在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在

Java5以后,Lock的出现可以解决synchronized在某些场景中的短板,它比synchronized更加灵活。

ReentrantLock(重入锁)

重入锁:自己可以再次获取自己的内部的锁。比如有线程A获得了某对象的锁,此时这个时候锁还没有释放,当其再次想获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

Reentrantlock 提供了公平锁(FairSync)和非公平锁(NonFairSync)两种实现,我们最常用的就是非公平锁的实现(默认)

源码

先看非公平锁

lock.lock()

由**lock.lock()**进入源码

final void lock() {
    //抢占互斥资源,有多个线程进入到这段代码?多个线程抢占到同一把锁.所以cas为解决这一问题
    if (compareAndSetState(0, 1))//乐观锁( true / false) | 只有一个线程能够进入.
       //能够进入到这个方法 , 表示无锁状态
        setExclusiveOwnerThread(Thread.currentThread());//保存当前的线程,设置当前线程为持有的线程
    else
        //cas设置stat为1失败,代表获取的资源失败,执行AQS获取锁的模板流程,否则获取流程成功
        acquire(1);
}

进入方法后,cas判断是否有锁,有则保存当前的线程,设置当前线程为持有的线程,失败则进入acquire(1)的方法。

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

然后我们一个一个的去理解!tryAcquire(arg)、 acquireQueued(addWaiter(Node.EXCLUSIVE)、 selfInterrupt()意义

tryAcquire(arg)

tryAcquire(arg):首先A Q S的acquire函数是获取锁的流程模板,模板流程会先执行tryAcquire函数获取资源,tryAcquire函数要子类实现,NonfairSync作为子类,实现了tryAcquire函数,具体实现是调用了Sync的nonfairTryAcquire函数。

接下来,我们再看看Sync专门给NonfairSync准备的nonfairTryAcquire函数逻辑

 final boolean nonfairTryAcquire(int acquires) {
     //获取当前的线程
            final Thread current = Thread.currentThread();
     //获取当前状态
            int c = getState();
            if (c == 0) {//无锁,state==0,代表资源可获取
                //cas设置state为acquires,acquires传入的值为1
                if (compareAndSetState(0, acquires)) {
                    //cas成功,设置当前为持有锁的线程
                    setExclusiveOwnerThread(current);
                    //返回成功
                    return true;
                }
            }
     //如果state!=0,但是当前的线程是持有锁的线程,直接重入
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;//state+1
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //设置state状态,这里不需要cas,因为持有锁的线程只有一个
                setState(nextc);
                return true;
            }
            return false;
        }

可见当前的方法就是当前线程查看资源是否可获取:

  • 可获取,尝试使用C A S设置state为1,C A S成功代表获取资源成功,否则获取资源失败
  • 不可获取,判断当线程是不是持有锁的线程,如果是,state重入计数,获取资源成功,否则获取资源失败

在这里插入图片描述

acquireQueued(addWaiter(Node.EXCLUSIVE)

addWaiter 将未获得锁的线程加入到队列

acquireQueued(); 去抢占锁或者阻塞.

再回到acquire的方法,当tryAcquire方法获取锁失败以后,则会先调用addWaiter将当前线程封装成Node.

入参mode表示当前节点的状态,传递的参数是Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了AQS的独占锁功能

  1. 将当前线程封装成Node

  2. 当前链表中的tail节点是否为空,如果不为空,则通过cas操作把当前线程的node添加到AQS队列

  3. 如果为空或者cas失败,调用enq将节点添加到AQS队列

private Node addWaiter(Node mode) {
    //把当前线程封装为Node
    Node node = new Node(Thread.currentThread(), mode);
    
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;//tail是AQS中表示同比队列队尾的属性,默认
    if (pred != null) {//tail不为空的情况下,说明队列中存在节点 
        node.prev = pred;//把当前线程的Node的prev指向tail
        if (compareAndSetTail(pred, node)) {//通过cas把node加入到AQS队列,也就是设置为tail
            pred.next = node;
            return node;
        }
    }
    //尾节点不为空加入队列
    enq(node);
    return node;
}
//该函数会初始化队列(如果队列未被初始化)  
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) {  // Must initialize  初始化队列
                if (compareAndSetHead(new Node()))//将head设置为一个空node。这个空node很重要,aqs的队头一定是空节点,用来表示正在执行的那个线程,想一下当执行线程结束后只有这个空节点才能去唤醒下一个节点,假如队头节点就是等待线程,谁能去唤醒他呢
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {//cas队尾,如果还失败看到这个是死循环会一直去放,直到放到队尾为止
                    t.next = node;
                    return t;
                }
            }
        }
    }

了解了addWaiter后,再看addWaiter(Node.EXCLUSIVE)传入的参数到了acquire里面做了那些操作

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();//获得该node的前置节点
             /**
                * 如果前置节点是head,表示之前的节点就是正在运行的线程,表示是第一个排队的
(一般讲队列中第一个是正在处理的,可以想象买票的过程,第一个人是正在买票(处理中),第二个才是真正排队的人);
那么再去tryAcquire尝试获取锁,如果获取成功,说明此时前置线程已经运行结束,则将head设置为当前节点返回 **/
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; //  help GC,将前置节点移出队列,这样就没有指针指向它,可以被gc回收
                failed = false;
                return interrupted; //返回false表示不能被打断,意思是没有被挂起,也就是获得到了锁
            }
            /**shouldParkAfterFailedAcquire将前置node设置为需要被挂起,
                    注意这里的waitStatus是针对当前节点来说的,
                    即是前置node的ws指的是下一个节点的状态**/
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) //挂起线程 park()
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);//如果失败取消尝试获取锁(从上面的代码看只有进入p == head && tryAcquire(arg)这个逻辑是才会触发,这个时候前置节点正好在当前节点入队的时候执行完,当前节点正好获得锁,具体的代码以后分析)

    }
}

以上是lock.lock()方法里执行的操作,当lock.unlock执行又发生什么呢,接下来看:

在这里插入图片描述

lock.unlock
release

​ 讲了如何获取到资源,接下来就应该如何释放资源.这个方法会在独占的模式下释放指定的资源(减小state).这个语义也是reentrantLock.unlock();

 public final boolean release(int arg) {
        if (tryRelease(arg)) {//尝试释放资源
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);   //唤醒队列的下一个节点
            return true;
        }
        return false;
    }

tryRelease():释放指定量的资源,这个方法是在子类中实现的.我们以reentrantLock.unlock()为例解读资源释放的过程

 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;  // 获取 AQS 的 state,并计算 c=state-释放资源数
     // 判断当前线程是否是拿锁线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) { //独占锁模式下,state为0时表示没有线程获取锁,这时才算是当前线程完全释放锁
                free = true;
                // 将AQS保存的拿锁线程置为null
                setExclusiveOwnerThread(null);
            }
     // 重置state
            setState(c);
            return free;
        }

回到releadse看看如何唤醒后继的节点

unparkSuccessor(Node node)

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;//获得head节点的状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);//设置head节点状态为0 

        Node s = node.next;//得到head节点的下一个节点
        if (s == null || s.waitStatus > 0) {//如果下一个节点为null或者status>0表示cancelled状态. 
		//通过从尾部节点开始扫描,找到距离head最近的一个waitStatus<=0的节点
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)//next节点不为空,直接唤醒这个线程即可
            LockSupport.unpark(s.thread);
    }

附个整个流程的图如下
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值