AQS系列

AQS系列文章目录

第一章 AQS学习之ReentrantLock源码解析



前言

  通过源码解析 , 了解ReentrantLock的实现过程 . 从实际运用入手 , 一步一步跟踪源码 , 看每一步在源码中是怎么实现的 .


一、AQS是什么?

AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。

  • CLH队列
    在这里插入图片描述

  • 节点入队列过程

    • 初始状态
      在这里插入图片描述

    • 第一个节点入队
      在这里插入图片描述

    • 第二个节点入队
      在这里插入图片描述

二、使用步骤

1.先看一波ReentrantLock实际运用中是怎么操作的

代码如下(示例):

public class TestAqs {

	//1.定义一个ReentrantLock
    private final Lock lock = new ReentrantLock();
    private int num;

    public int addNum(int n) {
    	//2.上锁 此行代码必须在第一行
        lock.lock();
        try {
        	//3.需要自行同步的代码块
            num += n;
        } finally {
        	//4.释放锁
            lock.unlock();
        }
        return num;
    }
}

2.通过跟踪代码观察上锁的过程

此处的上锁(自旋+CAS)区别于synchronized
首先从lock()方法跟踪:

public class ReentrantLock implements Lock, java.io.Serializable {
	//无参构造器
	public ReentrantLock() {
        sync = new NonfairSync();
    }
	
	//1.如果锁目前未被其他线程占用则立即返回
	//2.如果这个锁被当前线程持有,则把持有的数量(state)加1,然后立即返回
	//3.如果锁被其他线程持有, 则当前线程不会再被线程调度器调用而进入休眠状态,直到获取取到锁,并把数量(state)加1
	public void lock() {
        sync.lock();
    }
   }

  下面看下NonfairSync.lock方法代码 :

final void lock() {
	//尝试直接CAS操作获取锁, 如果失败则通过acquire方法获取
	if (compareAndSetState(0, 1))
		//如果获取到锁则设置锁被当前线程持有
    	setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

AbstractQueuedSynchronizer.acquire()代码 :

//1.尝试获取锁
//2.不成功的话, 就给当前线程创建一个Node节点, 并把Node节点入队列中排队
//3.入队成功则进行自我中断
 public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
	    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	    //线程中断
	    selfInterrupt();
 }

尝试获取锁tryAcquire()代码 :

 protected final boolean tryAcquire(int acquires) {
     return nonfairTryAcquire(acquires);
 }

 //非公平方式获取锁
 final boolean nonfairTryAcquire(int acquires) {
 	//当前线程
    final Thread current = Thread.currentThread();
    //获取当前资源数 , state = 0说明锁未被占用 , 反之说明锁已经被占用
    int c = getState();
    //1.c == 0说明锁未被占用 , 尝试通过CAS占用锁 成功则设置锁被当前线程占用并直接返回
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //2.如果锁是被当前线程占用的, 则把state的值加上入参(acquires) 此处说明这是一把可重入锁
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //3.其他情况说明当前线程当前不能获取到锁, 直接返回fasle
    return false;
}

假设上一步尝试获取锁tryAcquire()没有成功 , 则要1.给当前线程创建一个Node节点, 将创建的Node节点入队列排队(addWaiter())) 2.为节点找到一个安全的停靠点(acquireQueued()) 看这两个方法的代码:

 //1.为当前线程创建Node节点 2.将Node节点入队列
 private Node addWaiter(Node mode) {
 	 //创建节点,并设置独占模式
     Node node = new Node(Thread.currentThread(), mode);
     //尝试将当前Node节点快速入队 判断当前队列的队尾是不是null值
     Node pred = tail;
     if (pred != null) {
     	 //如果不是null值,则设置当前Node节点的前节点是当前等待队列的队尾
         node.prev = pred;
         //尝试把当前Node节点置为队尾
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     //如果快速入队没成功, 则通过enq()方法入队
     enq(node);
     return node;
 }

 //通过自旋+CAS操作讲当前Node节点入队等待
 private Node enq(final Node node) {
     //自旋
     for (;;) {
     	 //获取当前等待队列的尾节点
         Node t = tail;
         //如果当前等待队列的队尾为null, 则给设置一个空节点队尾
         if (t == null) { // Must initialize
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else {
         	 //如果队尾不为空,则设置当前节点(要插入的节点)的前节点为当前等待队列的尾节点
             node.prev = t;
             //CAS操作将当前节点(要插入的节点)设置为队尾 成功则设置之前的队尾节点的下一个等待节点为当前节点(要插入的节点), 返回; 不成功的会一直自旋, 直到插入队尾成功
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
 }

 //1.尝试获取资源
 //2.不成功的话,则给当前节点找一个合适的位置等待,并告诉前置节点执行完了要来唤醒我
 //3.找到安全的停靠点后调用park()方法挂起当前线程
 //4.返回当前线程是否被中断过
 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;
             }
             //寻找安全点停靠 2.告诉前节点执行完要通知我
             if (shouldParkAfterFailedAcquire(p, node) &&
             	 //挂起线程 返回当前线程是否被中断过
                 parkAndCheckInterrupt())
                 interrupted = true;
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }
 //经过此方法会给当前节点找到一个安全的停靠点,并设置前节点的状态为-1(告诉前节点执行完唤醒我)
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
     int ws = pred.waitStatus;
     //判断当前节点的前置节点的状态,如果为-1,则说明它执行完之后会通知后节点,则当前节点就可以安全停靠,直接返回
     if (ws == Node.SIGNAL)
         return true;
     //如果前节点的状态大于0,则说明前面的节点已经取消了, 那就从前节点开始向前查找直到查询到状态小于0的节点为止
     if (ws > 0) {
         do {
             node.prev = pred = pred.prev;
         } while (pred.waitStatus > 0);
         pred.next = node;
     } else {
         //如果前节点的状态小于等于0,则就把前节点的状态设置为-1(告诉前节点执行完唤醒我)
         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
     }
     return false;
 }
 //挂起当前线程,返回当前线程是否被中断过
 private final boolean parkAndCheckInterrupt() {
 	 //挂起
     LockSupport.park(this);
     //返回当前线程是否被中断过,此方法会清除中断标记
     return Thread.interrupted();
 }

3.通过跟踪代码观察释放锁的过程

首先从unlock()方法跟踪:

 //释放锁
 public void unlock() {
     sync.release(1);
 }

AbstractQueuedSynchronizer.release()代码:

 //1.释放锁 2.唤醒下一节点
 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) {
 	 //当前线程持有state数量减去要释放的数量
     int c = getState() - releases;
     //如果当前持有锁的线程不是当前线程,则抛出异常
     if (Thread.currentThread() != getExclusiveOwnerThread())
         throw new IllegalMonitorStateException();
     boolean free = false;
     //如果当前state数量-要释放的数量为0,则说明当前线程可以释放锁了
     if (c == 0) {
         free = true;
         //设置当前持有锁的线程为空,即释放锁
         setExclusiveOwnerThread(null);
     }
     //设置state数量
     setState(c);
     return free;
 }

为什么此处要判断c == 0 ?
因为这是一个可重入的锁, 当前线程持有锁, 但是state的数量不一定为1, 有可能是2.3.4…, 所以要判断c == 0的时候当前线程才可以真正的把锁释放掉.

AbstractQueuedSynchronizer.unparkSuccessor()代码:

	//入参为当前的首节点head
    private void unparkSuccessor(Node node) {
        //获取当前首节点的状态
        int ws = node.waitStatus;
        //如果当前首节点的状态小于0, 则设置为0.
        if (ws < 0)
            //清空节点状态
            compareAndSetWaitStatus(node, ws, 0);
        //获取入参节点的下一个节点
        Node s = node.next;
        //判断下一节点是否为空或状态大于0(大于0为取消状态)
        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);
    }

问题讨论

  • 在释放锁的时候, 取head节点的下一节点, 如果下一节点的状态小于0则直接唤醒, 否则从等待队列的尾部向前查找状态小于等于0的非head节点.
    那么问题来了, 假如等待队列的长度为10, 我从后往前找第三个是可用的, 然后我把这个节点唤醒, 那从后往前的三个节点和首节点(head)的节点怎么办呢? 如果有可用的节点谁来唤醒?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值