java aqs

AQS

  1. 全称AbstractQueuedSynchronizer,即队列同步器,它就是Java的一个抽象类,他的出现是为了解决多线程竞争共享资源而引发的安全问题,细致点说AQS具备一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制aqs是用clh队列锁实现的,即将暂时获取不到锁的线程加入到队列中,队列是双向队列。

  2. AQS是一个抽象类,主要是通过继承方式使用,本身没有实现任何接口,仅仅是定义了同步状态的获取和释放的方法。

  3. ReentrantLock、ReentrantReadWriteLock、Semaphore,CountDownLatch等都是基于AQS

独占锁源码分析

此处以ReentrantLock为例

lock()

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

		//调用父类acquire,尝试获取
        final void lock() {
        	// 父类方法,1表示一次
            acquire(1);
        }

        /**
         * 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表示锁没有被任何线程获得
            int c = getState();
            if (c == 0) {
            	// 1.hasQueuedPredecessors 有没有前序结点,如果有肯定轮不到当前线程,
            	//2.compareAndSetState 设置锁计数器=1,原子操作
            	//3.setExclusiveOwnerThread 设置独占
            	// 三个操作都符合条件才算当前线程获得锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
            	// 如果当前线程已经获得锁,那么锁计数器state加1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

总结:AQS获取锁的机制就是维护一个int属性state

  • state=0 表示锁没人再用
  • state>0 表示有线程获取,state的值表示线程重入的次数

acquireQueued()

这块是核心代码

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //死循环,直到return interrupted获取锁
            for (;;) {
            	/*
            	1.这个死循环的作用是如果当前线程还轮不到获取锁,则进入队列,并且当前线程中断(后边调用了LockSupport.park())
            	2.如果当前线程节点的上一个节点是head节点,则不断尝试去获取锁
				*/
            	//获取前任节点
                final Node p = node.predecessor();
                //如果前任节点是head节点,则尝试获取锁,此时属于第二次获取锁,第一次是lock时便会尝试获取锁
                if (p == head && tryAcquire(arg)) {
                	//获取到锁后,把head节点指向当前node
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire返回true表示前序节点还在排队,所以当前节点需要去park,进到parkAndCheckInterrupt方法
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
            // 如果上面代码没有获取锁报错,需要取消获取锁的动作
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire()

//获取锁失败后应该排队
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//走进这个方法说明前边没有获取到锁,即tryAcquire()失败
		// 先拿到前序节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
        	// 前序节点还在排队呢,所以当前节点node只能挂起,安心地去排队
            // 这里说下节点得SIGNAL状态,它的意思是如果锁被释放,应该通知SIGNAL状态的节点
            return true;
         // 下面的情况,node节点不需要去park,最终返回false使上层调用方法死循环直到获取锁
        // 首先是ws>0,即waitStatus=cancelled=1(看内部类:Node)
        if (ws > 0) {
        	// 如果先序节点的状态是取消,则把异常的先序节点从队列中删除
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	// 走到这里,前序节点的waitStatus只能是0/-3,
        	// 把前序节点的waitStatus设置为-1:SIGNAL,因为啥呢?
            // 看上面if (ws == Node.SIGNAL)分支,只有前序节点waitStatus=-1,当前节点才能安心地去队列等待,
            // 否则当前node会一直自旋获取锁,这显然是不合理的
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
  1. 这里我们需要注意,当执行完LockSupport.park(this)后,此线程就被挂起了,除非当其他线程调用LockSupport.unpark唤醒当前线程或者当前线程被中断,否则后面代码是不会执行的
  2. 假设此时线程被唤醒了,我们此时是不知道线程是被unpark方法还是中断唤醒的,所以我们需要通过Thread类的interrupted方法来判断。interrupted方法会返回给我们当前线程的中断标志位,并将中断标志位复位,即置为false。如果我们是中断唤醒的,则返回true,然后会进入acquireQueued的第二个If分支中将interrupted置为true。然后再次进入for循环自旋,看是获取锁还是又被挂起。

Node.waitStatus

AQS使用FIFO双向阻塞队列来保存被阻塞的线程,实现机制是,AQS通过其内部类Node封装线程,同时Node维护prev,next,waitStatus信息来实现双向队列;
针对节点的waitStatus属性(等待状态),要补充说明一下,它的取值有以下几种:

  • CANCELLED = 1; // 取消状态,唯一个大于0的状态,表示节点获取锁超时。例如:1,2,3三个节点按顺序获取锁,结果1正在处理业务,2,3排队等待,2先来的,等久了超时了,而3没超时,等1释放锁以后,3虽然排在2后面,但是会把CANCELLED状态的2节点删掉,让后面未取消的节点顶上来;
  • SIGNAL = -1; // 等待触发状态,这个状态的节点就是锁释放后需要被通知的节点;
  • CONDITION = -2; // 当其他线程调用了condition的signal方法后,condition状态的节点会从等待队列转移到同步队列中,等待获取同步锁
  • PROPAGATE = -3; // 共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能唤醒后继的后继节点。
  • 0;新节点入队时候的默认状态。

AbstractQueuedSynchronizer 结构

在这里插入图片描述

加锁流程总结

在这里插入图片描述

独占锁线程唤醒

release

// 首先是尝试释放锁,有人问了,这释放锁还需要尝试吗?又不是获取锁,还可能获取不到
// 那确实存在释放不了的情况,什么情况呢?  那就是重入次数大于1的情况,按照重入锁的设计,重入几次就需要释放几次
public final boolean release(int arg) {
        if (tryRelease(arg)) {
         	// 锁释放成功,要唤醒后序挂起线程
            Node h = head;
            // 判断head节点状态非0,即被修改过
            if (h != null && h.waitStatus != 0)
            	// 如果h.waitStatus = 0,表示没有后续节点,能理解不?看上面第三点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

unparkSuccessor

// 注意参数是head节点,因为唤醒后续节点总是从head往后找
private void unparkSuccessor(Node node) {
        
        int ws = node.waitStatus;
        if (ws < 0)
        	// 还原head节点状态为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);
    }

共享锁源码分析

此处以ReentrantReadWriteLock为例,

acquireShared

    public final void acquireShared(int arg) {
    	//获取共享的同步状态,不同锁实现不一样
    	//<0 表示获取同步状态失败
        if (tryAcquireShared(arg) < 0)
        	//加入同步队列、挂起线程等在此处实现
            doAcquireShared(arg);
    }

与独占锁的获取不一样的是,此处将加入同步队列与挂起线程等操作放到一个方法里了。

doAcquireShared

    private void doAcquireShared(int arg) {
        //加入同步队列,此处节点是共享状态
        final Node node = addWaiter(Node.SHARED);
        //获取同步状态失败
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            	//前驱节点
                final Node p = node.predecessor();
                if (p == head) {
                	//若是前驱节点为头结点,则尝试获取同步状态
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    	//获取同步状态成功
                    	//修改头结点,并传递唤醒状态
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                        	//补中断
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //与独占锁一致
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }   

tryAcquireShared(arg) 返回值表示当前可用的资源。

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
        //propagate == 0 表示没有资源可以使用了
        Node h = head; // Record old head for check below
        //设置头结点
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //若后继节点是共享节点,则唤醒
            if (s == null || s.isShared())
                doReleaseShared();
        }

除了将头结点指向当前节点外,还需要唤醒下一个共享节点。
而独占锁不会。

读写锁(待补充)

相关概念

CLH队列

CLH队列(Craig, Landin and Hagersten queue)是一种基于链表结构的线程等待队列,由三位计算机科学家Maurice Craig、Colin Landin和C. R. Hagersten在1971年提出。该队列被广泛应用于多线程环境下的自旋锁和其他同步原语实现中。

在CLH队列模型中,当多个线程尝试获取一个资源时,竞争失败的线程不会立即进入阻塞状态,而是将自身放入一个特殊的链表(通常称为FIFO等待队列)。每个线程在队列中有一个对应的节点,节点上包含了一个标志位或称为”锁定“字段,用于表示该线程是否已经释放了锁。
当线程尝试获取资源时:

  • 如果资源可用,则线程获取资源并执行相应操作。
  • 如果资源已被其他线程占用,则线程构造一个新的节点,并将其添加到CLH队列的尾部,然后开始循环检查其前驱节点的状态(即检查前驱节点的“锁定”字段)。
  • 当前线程的前驱节点释放资源后会修改其“锁定”字段,这样当前线程就能感知到资源已释放,有机会再次尝试获取资源。

这种设计使得线程在等待资源的过程中可以继续运行(通过自旋),而不是直接进入阻塞状态等待唤醒,从而减少上下文切换带来的开销,提高了并发环境下的性能表现。同时,CLH队列能够确保线程按照先进先出(FIFO)的原则进行调度,避免了死锁和饥饿问题的发生。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值