ReentrantLock源码解析

ReentrantLock源码解析

1.AQS

AQS本质就是java中的一个类AbstractQueuedSynchronizer

在这个类中,有几个属性和一个双向队列(CLH队列)

AQS就是并发包下的一个基础类

CountDownLatch ReentrantLock Semaphore都是以AQS为基础实现的

//AQS中的节点Node,是AQS的内部类
class Node{
//是一个排它锁的标识
static final Node EXCLUSIVE = null
//如果带有这个标识,证明是失效了
static final int CANCELLED = 1
//如果具有这个标识,说明后继节点需要被唤醒
static final int SIGNAL = -1
//NODE对象存储标识的地方
volatile int waitStatus
//上一个节点是谁
volatile Node prev
//下一个节点是谁
volatile Node next
//返回前驱节点,如果前驱为null,则抛出空指针异常
final Node predecessor() throws NullPointorException{
  Node p = prev;
  if(p == null){
    throw new NullpointerExcption()
 		}else{
    return p;
  	}
	}
}
//AQS中指向头结点
private transient volatile Node head;
//指向尾结点
private transient volatile Node tail;
//AQS中一个核心的属性,只要state为0,就可以获取一个锁资源,不等于0时,说明以及有线程占用了锁资源
private volatile int state;

2.加锁

lock()是ReentrantLock加锁的实现方法

在这个方法中,lock()方法调用了sync对象的lock()方法

image-20220924183450311

而sync就是一个继承了AQS的Sync类的对象

image-20220924183551669

在Sync这个类当中,lock这个抽象方法有个实现,分别对应了公平锁以及非公平锁的实现,是一个互斥锁也是一个可重入锁

image-20220924184815242

2.1从lock进入

public void lock(){
  //sync分为了公平锁和非公平锁,在默认情况下,是以非公平锁的方法实现的
  sync.lock();
}

2.2从非公平锁进入

final void lock() {
  //通过cas的方式尝试将state从0修改为1,如果返回TRUE,代表修改成功
            if (compareAndSetState(0, 1))
              //成功的话,我们将一个属性设置为当前线程,这个属性是AQS父类所提供的
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

2.3查看acquire方法

public final void acquire(int arg) {
  //tryAcquire再次获取锁资源,如果成功,返回true
    if (!tryAcquire(arg) &&
        //获取锁资源失败后,需要将当前线程封装成一个Node追加到AQS队列中
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      //如果没有获取到锁资源,则将当前线程interrupt
        selfInterrupt();
}

这个方法也就是说,如果我们在2.2这一步没有成功拿到锁资源,我们就会调用到acquire方法,在这个acquire方法中,会再次尝试获取锁资源,也就是trylock,如果这个时候获取到了,就还是啥事没有,如果这个时候还没获取到,就会将这个线程封装成一个AQS队列中的一个Node节点放入其中。

2.4查看tryAcquire方法

AQS是一个基类,也是一个抽象类,很多JUC工具类都是基于他实现的,所以在这里trylock这个方法,是基于不同的类实现的。

image-20220924192703221

final boolean nonfairTryAcquire(int acquires) {
  //获取当前线程
            final Thread current = Thread.currentThread();
  //获取AQS的state的值是多少
            int c = getState();
  //如果state为0,代表之前占有锁的线程,已经释放了锁资源,代表我可以再次尝试获取锁资源
            if (c == 0) {
              //CAS尝试修改state值,如果修改成功,就设置这个属性为当前线程
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
  //当前占有锁资源的线程是否是当前线程,做了一个重入锁的操作
            else if (current == getExclusiveOwnerThread()) {
              //将state加1
                int nextc = c + acquires;
              //如果加1后小于0,证明超过了最大锁数,因为在二进制中,第一位为符号位,当其他位都为1时,int已经达到了最大值,
              //如果再进行加1操作,符号位会从0->1会导致我们得到的这个数逆转为负数,超出锁可重入的最大值,
              //所以在此加了一个健壮性判断
                if (nextc < 0) {// overflow
                    throw new Error("Maximum lock count exceeded");
                }
              //没问题就重新对state赋值
                setState(nextc);
              //锁可重入成功
                return true;
            }
            return false;
        }

在这里提下何为可重入锁:

可重入是多线程并发编程中一个重要的概念,在一个运行的函数或者方法因为抢占资源或者中断,导致这个函数或方法运行过程中被中断了,等到中断的程序执行结束之后,重新进入到这个函数的代码里面在运行的时候,并且运行的结果不会发生变化,这个函数或者方法就是可重入的。所谓的可重入锁就是一个线程如果抢占到了互斥锁的资源,在锁释放之前,在此竞争锁的资源不需要等待,只需要去记录重入次数,在多线程并发编程当中,绝大部分的锁都是可重入的,比如synchronized,ReentrantLock,但是也有一些不支持可重入的锁,比如jdk8当中的读写锁stampedLock,锁的可重入性是为了避免死锁的问题,因为一个已经获取锁的线程在释放锁之前再次去竞争锁的时候,相当于会出现一个自己等待自己释放的过程,所以就会导致死锁。

2.5小结

image-20220924224226503

2.6查看addWaiter(Node.EXCLUSIVE)

//执行这行代码,说明前面获取锁资源失败,放到队列中等待
private Node addWaiter(Node mode) {
  //创建node类,并且设置Thread为当前线程,设置他为排它锁
  //这个mode其实就是我们刚刚在那边传入的Node.EXCLUSIVE这个变量的值
    Node node = new Node(Thread.currentThread(), mode);
  //这个Node是由AQS提供的,所以这个tail就是整个AQS队列中的尾结点
    Node pred = tail;
  //如果tail==null,也就代表这是个空队列,没人排队
  //如果tail!=null,说明现在队列有人排队
    if (pred != null) {
      //将当前节点的上一个节点,指向原本队列的尾结点,那么,当前节点,就成为了尾结点
        node.prev = pred;
      //用CAS的方式,将以前的尾结点,换成现在的我(当前节点)
        if (compareAndSetTail(pred, node)) {
          //因为AQS是一个双向队列,所以需要将原来尾结点的下一个节点设置为当前节点
            pred.next = node;
          //返回当前节点
            return node;
        }
    }
  //如果AQS队列为空,我们需要将当前节点node传入给enq方法
    enq(node);
    return node;
}

2.7查看enq()方法

//两种情况:现在没人排队,我是第一个或者如果前面CAS失败,也会进到这个位置重新往队列尾巴放
private Node enq(final Node node) {
  //死循环
    for (;;) {
      //重新获取tail节点
        Node t = tail;
        if (t == null) { // Must initialize
          //如果tail节点为空,则没人排队,我是第一个
            if (compareAndSetHead(new Node()))//初始化一个Node作为head,而这个head没有意义,相当于定义了一个队列头元素
                tail = head;
        } else {
          //有人排队,我要往队列尾部放
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
              //最终返回的就是当前的这个node
                return t;
            }
        }
    }
}

这个方法相当于给addWaiter上了一层保险,或许前面的操作没人排队或许因为CAS失败最后都能得到处理,而且在enq这个方法中定义了一个死循环,其中的CAS操作相当于原地自旋,尽可能避免CAS失败问题;

此外,AQS在设计之初就是这样,头结点就是一个没有任何意义的节点,当后续需要释放的时候就需要去释放后续的有效节点。

2.8查看acquireQueue()方法

当前acquireQueue方法从addwaiter这个方法中获取到了最终是一个Node元素

final boolean acquireQueued(final Node node, int arg) {
  			//首先声明了一个标识,获取锁资源的标识
        boolean failed = true;
        try {
          //标识
            boolean interrupted = false;
          //死循环
            for (;;) {
              //获取当前节点的上一个节点p
                final Node p = node.predecessor();
              //如果p是head,再次尝试获取锁资源(state从0-1,锁重入操作)
                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);
        }
    }

3.释放锁

未完待续

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值