初识AQS-ReentrantLock加锁过程

Java的内置锁一直备受争议,在JDK1.6之前,synchronized这个重量级锁其性能一直较为低下:虽然在java1.6开始,进行了大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的:虽然synchronized提供了便捷的隐式获取锁和释放锁的机制(基于JVM机制),但是它却缺少了获取锁和释放锁的可操作性,可中断超时获取锁,且它在独占式的高并发环境下性能大打折扣:

常见概念:

自旋:什么是自旋,自旋就是一个通过循环不断尝试的过程。
AQS队列:结构是使用双向链表设计的

ReentrantLock源码分析之上锁:🔒

AQS(AbstractQueuedSynchronizer)类:具体部分看源码!

private transient volatile Node head; // 队首
private transient volatile Node tail; // 队尾
private volatile int state; // 锁状态 0表示未上锁,加锁成功则 + 1

AQS队列展示:

在这里插入图片描述
如上图可以看出:

  1. AQS队列中每一个元素都是一个Node,其中Node节点包括了Thread、prev和next几个属性。其中Thread代表哪一个线程、prev代表前驱节点、next代表后置节点。
  2. 从图中可以看出AQS队列的第一个Node Thread为Null,后面介绍为什么为NUll。
  3. 从图中可以看出AQS是使用的双向链表,这个在阅读源码的时候会知道原因,在源码中AQS新入队一个Node需要修改AQS队列中前一个Node的ws从0变成-1,同时在唤醒新的Node时又需要唤醒下一个节点。所以我认为这里使用双向链表是为了更加方便,提升性能。
  4. 其中head永远指向队列的head也就是Thread为null的位置,tail指向队列的尾部。

Node类设计:

static final class Node{
	volatile int waitStatus; // ws状态 0表示未休眠,-1表示已经休眠
	volatile Thread thread; // 当前线程
	volatile Node prev; // 前驱指针
	volatile Node next; // 后置指针
}

上锁的过程:🔒

锁对象:这里的锁对象就是指ReentrantLock的实例化对象。
未加锁状态:未加锁状态就是没有线程持有该对象,计数器为0;
计数器:在Lock对象中存在state属性,用来记录上锁的次数,比如未上锁,则state的值为0,大于0则表示有其他线程持有该锁,小于0则会抛出异常。
waitState:就是一个状态,初始值为0,可以让一个第一个如队的线程自旋两次,减少直接park损失性能。ws默认为0 是有必要的。
tail:队列的尾部;head队列的头部;t1第一个线程;t2第二个线程;t3第三个线程。
节点:上面介绍的Node就是AQS队列中的节点,里面封装了线程,所以从某种意义上说Node就是一个线程。

下面主要解释一下公平锁的源码:

acquire方法源码分析:
 // 1、首先调用tryAcquire方法尝试获得锁
 // 2、使用addWaiter方法,将线程加入到队列中
 // 3、acquireQueued设置park阻塞
 // 4 selfInterrupt 设置自我中断,修改标志位。
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
tryAcquire方法源码分析:
/**
* * Fair version of tryAcquire.  Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取当前持有锁的状态,为0则表示未上锁,大于0表示已经获得锁
    int c = getState();
    if (c == 0) { // 没有人占用锁---->我要占用
    	// hasQueuedPredecessors:判断自己是否需要排队,比较复杂
    	// compareAndSetState:使用CAS设置state状态为1,如果成功则把线程设置成持有锁的状态。
    	// 继而返回true
        if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 有人占用锁,则判断占用锁的线程是否是当前线程
    else if (current == getExclusiveOwnerThread()) {
    	// 如果是当前线程持有锁则state + 1,这里可以看出ReentrantLock的重入锁的实现。
        int nextc = c + acquires;
        // state正常情况下是不会小于0的
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 更新state值
        setState(nextc);
        return true;
    }
    return false;
}
hasQueuedPredecessors 方法源码分析:
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

下面对hasQueuedPredecessors方法进行分析:
一、队列没有被初始化,不需要排队;直接去加锁,但是有可能会失败;因为可能有几个线程同时获得lock,并且都发现没有初始化,于是所有线程都认为不需要要排队即可以直接获得锁,所以这里要使用CAS修改state的值;这里如果t1获得锁成功,那么t2CAS就会失败,那么t2就会去初始化队列。
二、队列被初始化了,但是t2过来加锁,发现队列中的第一个就是自己;
那么这种情况下是怎么发生的呢,首先如果上一个线程已经完成会设置当前线程为head,线程就会重新调用tryAcquire尝试重新获得锁,如果获得成功直接加锁,获取失败则重新进入队列,所以这里的不需要要排队,不是真的不需要排队。

h != null 判断head不等于tail的情形:

  1. 当队列没有被初始化,那么head和tail都是null,所以这里就会返回false,那么短路与就不会执行,直接返回false就是不需要排队。!hasQueuedPredecessors()使用!做取反,所以就会执行,并且尝试获得锁。
  2. 队列被初始化了,如果队列初始化了那么h!=t则成立。
  3. 如果队列被初始化了,而且至少有一个人在排队那么自己也去排队,他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放也合情合理,好比你去买票,如果有人排队,那么你乖乖排队,但是你会去看第一个排队的人是不是你女朋友;如果是你女朋友就相当于是你自己(这里实在想不出现实世界关于重入的例子,只能用男女朋友来替代);你就叫你女朋友看看售票员有没有搞完,有没有轮到你女朋友,因为你女朋友是第一个排队的.

TODO:这里理解的不是很深,等具体深入理解后更新。

addWaiter 方法源码分析:

将一个线程加入到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;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                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()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

对于上面的入队操作,分别对t1,t2,t3,三种不同时刻的线程进行介绍:
t1正在执行 t2入队图:

  • Node node = new Node(Thread.currentThread(), mode);使用当前线程创建一个新的Node
  • Node pred = tail;设置pred节点为tail,此时由于队列没有初始化,所以head和tail都为null
  • 于是执行enq(node);方法,初始化队列。
  • Node t = tail; 创建了一个Node t,
  • if (t == null) 此时必须进行初始化,使用compareAndSetHead(new Node()) 利用CAS比较并设置机制将Head节点设置为一个新创建的节点,并将tail指向新的节点。
  • 第二次循环将Node设置为新的Tail,并将新的Tail返回。
    在这里插入图片描述
    经过队列的初始化和第一个线程入队以后,AQS队列中如上图所示。

t2 已经在队列中,t3入队图:

  • Node node = new Node(Thread.currentThread(), mode);使用当前线程创建一个新的Node
  • Node pred = tail; 设置Node的前一节点为tail,连接上一节点
  • if (pred != null) 此时不为null
  • node.prev = pred; 设置Node的前驱节点为,上一个节点
  • 如果compareAndSetTail(pred, node)成立,pred.next = node;设置上一个节点的下一个节点为当前的Node,返回Node信息。
    此时AQS队列中的节点如图所示:
    在这里插入图片描述
acquireQueued方法源码分析:
final boolean acquireQueued(final Node node, int arg) { // 这里的Node为当前线程封装的哪个Node,后面叫做nc
		// 标记很重要
        boolean failed = true;
        try {
        	// 中断标记
            boolean interrupted = false;
            for (;;) {
            	// 获得nc的上一个节点,两种情况:1、上一个节点为头部;2、上一个节点不是头部
                final Node p = node.predecessor();
                // 如果nc的上一个节点为头部,则表示nc为AQS队列中的第二个元素,是队列中第一个排队的Node
                // 如果nc为队列中的第二个元素,需要调用tryAquire获取加锁。
                // 这里只有nc为队列中第二个元素的时候,才会尝试去加锁,其他情况直接park。
                // 因为第一个排队的在这里的时候需要看持有锁的线程是否释放锁,如果释放直接获得,就不park了。
                if (p == head && tryAcquire(arg)) {
                	// 能够进入到这里面,证明上一个人已经搞完了,同时我也已经获得锁了,那么前面那个人直接出队列,我自己则是队首
                    setHead(node);
                    // 这里设置搞完事情的那个人,由于事情做完了要出队,直接使用赋值Null交由JVM负责。
                    p.next = null; // help GC
                    // 设置标示为false
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

加锁过程总结:
如果第一个线程执行,或者多个线程是交替执行的,那么不会进入到队列中,也不会初始化队列,永远与AQS无关,都是线程直接持有锁;如果发生了竞争,比如t1在持有锁的过程中,t2想来获得这把锁就会产生竞争,那么这个时候就会初始化AQS队列,在初始化AQS队列的过程中会创建一个Thread为Null的Node。在AQS队列中,head节点,也就是AQS队列中的第一个节点,永远是正在持有锁的线程。当t1执行完后会主动唤醒t2,此时t2的Thread会置为NUll,同时会给执行完的哪个Node的prev和head都设置成Null,便于GC。

这里只是记录了一些自己的学习记录,研究的没有那么透彻,有问题请大家直接指出,谢谢。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值