AQS源码解析 5.Condition条件队列 await() signal() 核心方法

news2025/8/17 6:23:05

AQS源码解析—Condition条件队列 await() & signal() 核心方法

简介

在 Condition 条件队列中使用的也是 AQS 中的 Node 结构,它并没有使用 prev 和 next 属性,而使用的是 nextWaiter 去实现了一个单向链表的结构:

Node nextWaiter;

流程图

条件队列流程图

image-20221120144615535

await() 流程图

image-20221027130055328

await() 节点出队重新构造入队流程图

在这里插入图片描述

signal() 流程图

在这里插入图片描述

signal() 节点迁移示意图

在这里插入图片描述

Condition 接口

  • 下面将 AQS 的队列简称为 同步队列,Condition 的队列(单向链表)简称为 等待队列
    • 同步队列就是阻塞队列,等待队列就是条件队列。
public interface Condition {
    
    // 阻塞 可以响应中断(抛出中断异常)
    void await() throws InterruptedException;

	// 阻塞 不响应中断
    void awaitUninterruptibly();
	
    // 线程超时等待,可以指定挂起时间,时间单位为纳秒
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    // 线程超时等待,可以指定挂起时间,时间单位可以自行设置
    boolean await(long time, TimeUnit unit) throws InterruptedException;
	
    // 线程等待,直到收到被唤醒、或收到中断信号、或到了指定的截止时间日期自动唤醒
    boolean awaitUntil(Date deadline) throws InterruptedException;
	
    // 唤醒条件队列中第一个可以被唤醒的节点(将等待队列的节点迁移到同步队列中)
    void signal();
	
    // 唤醒条件队列中的所有节点全部迁移到同步队列中
    void signalAll();
}

ConditionObject 实现类

	/*
	 * Condition 接口的实现类
	 * 此类是AQS中的一个内部类,等待队列使用的节点也是AQS的内部类 Node
	 * 只不过与AQS的队列不同的是,等待队列中是单向链表。AQS的队列是双向链表。
	 * 注意:普通内部类是可以访问到类外部的域(后面要做节点的迁移)
     */
	public class ConditionObject implements Condition, java.io.Serializable {
		private static final long serialVersionUID = 1173984872572414699L;
        
    	// 条件队列的第一个node节点(头节点)
        private transient Node firstWaiter;
        
    	// 条件队列的最后一个node节点(尾节点)
        private transient Node lastWaiter;
    	
    	// lock.newCondition() 最终调用的还是这个构造器
        // Lock => ReentrantLock => Sync => new Condition() => new ConditionObject()
   		public ConditionObject() { }

await() 方法

        public final void await() throws InterruptedException {
            // 判断当前线程是否是中断状态,如果是则直接抛出中断异常
            if (Thread.interrupted())
                throw new InterruptedException();
            /*
             * addConditionWaiter()将当前线程封装成Node节点加入条件队列中
             * 并返回封装完成的Node节点(类似AQS中的addWaiter方法)
             */
            Node node = addConditionWaiter();
            /*
             * fullyRelease() 完全释放当前线程的锁(state置为0)
             * 因为这里要挂起等待被唤醒,所以必须先完全释放锁
             */
            int savedState = fullyRelease(node);
            /*
             *  0 表示在Condition队列挂起期间没有接收过中断信号
             * -1 表示在Condition队列挂起期间接收到了中断信号(THROW_IE)
             *  1 表示在Condition队列挂起期间接未接收到中断信号,但是迁移到“阻塞(同步)队列AQS的队列”之后,接收过中断信号(REINTERRUPT)
             */
            int interruptMode = 0;
            /*
             * isOnSyncQueue(node) 
             * true表示当前节点已经迁移到了同步队列中
             * false表示当前节点还在等待队列中如果当前节点还在等待队列中,则需要继续挂起。
             */
            while (!isOnSyncQueue(node)) {
                // 当前节点还在等待队列中,继续被挂起
                LockSupport.park(this);
                 /*
                 * 这里就是唤醒后判断是否是被中断唤醒的,然后执行响应的中断逻辑。
                 * 
                 * checkInterruptWhileWaiting()就算在等待队列挂起期间,
                 * 线程发生了中断,对应的node也会被迁移到同步队列(参考transferAfterCancelledWait)
                 * 如果当前node被中断过,也直接break。
                 * 
                 * 什么时候会被唤醒?
                 * 1.常规:外部线程获取锁后,调用了signal()方法,转移条件队列的头节点,到同步队列,当这个节点获取锁后,会被唤醒
                 * 2.转移到同步队列后,发现同步队列的前驱节点状态是取消状态,此时会直接唤醒当前节点
                 * 3.当前节点挂起期间,被外部线程使用中断唤醒..
                 */
                // 如果不为0,说明发生过中断了,也会被迁移到阻塞队列中,直接break。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            
            /*
             * 到这里 说明当前node已经被迁移到了同步队列中
             */
            
            /*
             * 条件1:acquireQueued(node, savedState)就是在同步队列中竞争锁的逻辑 savedState作用:恢复加锁状态 需要把之前释放锁的状态设置回去
             *   返回true -> 表示在同步队列中被外部线程中断唤醒过
             *   返回false -> 表示在同步队列中没有被外部线程中断唤醒过
             * 条件2:interruptMode != THROW_IE 成立,表示node在等待队列未发生过中断
             *   两个条件同时成立,表示node在同步队列发生过中断。
             *   所以将interruptMode设置为REINTERRUPT(在同步队列发生过中断)
             */
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 考虑下 node.nextWaiter != null,条件什么时候成立呢?
            // 其实是node在条件队列内时如果被外部线程中断唤醒时,会加入到阻塞队列
            // 但是并未设置 nextWaiter = null,这里需要做一个清理工作。
            if (node.nextWaiter != null) // clean up if cancelled
                // 清理等待队列内取消状态的节点
                unlinkCancelledWaiters();
            /*
             * 条件成立:说明挂起期间发生过中断(1.条件队列的挂起 2.条件队列之外的挂起)
             */
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter()

        /*
         * 将当前线程封装成一个Node加入到等待队列的末尾。
         * (会将等待队列中所有处于取消状态的节点全部出队)
         * 调用await方法的线程 都是 持锁状态的,也就是说 addConditionWaiter 这里不存在并发!
         */
        private Node addConditionWaiter() {
            // 指向队尾
            Node t = lastWaiter;
            /*
             * 条件1:t != null 成立:说明当前条件队列中,已经有node元素了
             * 条件2:node 在条件队列中时,它的状态是CONDITION(-2)
             *       t.waitStatus != Node.CONDITION 成立:说明当前node发生中断了..处于取消状态CANCELLED(1)
             * 此时当前线程在队列中做一次清除处理,将所有处于取消状态的node全部干掉。
             */ 
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 清理条件队列中所有取消状态CANCELLED(1)的节点
                unlinkCancelledWaiters();
                // 更新t指向最新的尾结点(lastWaiter可能会更新 因为可能处于取消状态CANCELLED(1))
                t = lastWaiter;
            }
            // 将当前线程封装为一个Node,Node状态(waitStatus)为Condition(-2)
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // 下面就是入队的过程(这里只有加锁的线程可以进来,所以不需要加锁)
            // 条件成立:说明条件队列中没有任何元素,当前线程是第一个进入队列的元素
            if (t == null)
                // 让firstWaiter 指向当前node
                firstWaiter = node;
            //说明当前条件队列已经有其它node了 做追加操作
            else
                // 让nextWaiter 指向当前追加的node
                t.nextWaiter = node;
            // 更新队尾引用指向 当前node
            lastWaiter = node;
            // 最终返回构造的节点
            return node;
        }

fullyRelease()

        /*
         * 完全释放锁
         */
		final int fullyRelease(Node node) {
            // 完全释放锁是否成功,当failed失败时,说明当前线程是未持有锁调用 await方法的线程(错误写法)
        	// 假设失败,在finally代码块中 会将刚刚加入到 条件队列的 当前线程对应的node状态 修改为 取消状态
        	// 后继线程就会将 取消状态的节点给清理出去了
            boolean failed = true;
            try {
                // 获取当前的state的值
                int savedState = getState();
                /*
                 * 全部释放掉state,变为初始状态0 这里正常情况下都会成功。
                 * (release方法见AQS源码解析系列博客)
                 */
                if (release(savedState)) {
                    // 失败标记设置为false,不会再走下面finally中 失败并设置节点状态为取消的逻辑
                    failed = false;
                    /*
                     * 返回当前线程释放的state值
                	 * 为什么要返回savedState?
                     * 这里最终要返回加锁前的状态,因为执行到这时,构造出来的Node节点还在等待队列中,
                     * 当迁移到同步队列后,再次被唤醒,还需要进行获取锁释放锁的逻辑,所以这里需要记录下初始锁的状态并返回。
                     */
                    return savedState;
                } else {
                    throw new IllegalMonitorStateException();
                }
            } finally {
                if (failed)
                    node.waitStatus = Node.CANCELLED;
            }
        }

isOnSyncQueue()

        /*
         * 判断当前Node是否在同步队列中
         */
        final boolean isOnSyncQueue(Node node) {
            /*
             * 条件1:node.waitStatus == Node.Condition(-2)条件成立,
             * 说明当前Node一定在等待队列,因为signal方法迁移节点到同步队列前,会将node的状态设置为0
             *  
             * 条件2:前置条件:node.waitStatus != Node.Condition
             *   1.此时node.waitStatus 可能 = 0(表示当前节点已经被signal了 在不在阻塞队列另说 可能在迁移过程中)
             *   2.此时node.waitStatus 可能 = 1(取消) (当前线程是未持有锁调用await方法,最终会将node的状态改为取消状态)
             *  
             * node.waitStatuas == 0 为什么还要判断 node.prev == null?
             * 因为signal是先修改状态 再迁移
             * 因为等待队列的Node是单向链表,node.prev == null 说明一定是在等待队列中
             */
            if (node.waitStatus == Node.CONDITION || node.prev == null)
                // 直接return false
                return false;
            /*
             * 执行到这里,会是哪种情况?
             * node.waitStatus != Condition 且 node.prev != null 
             * 可以排除 node.waitStatus == 1 取消状态
             * 为什么可以排除?因为signal不会把取消状态的node迁移走
             *  
             * 设置prev引用的逻辑 是 迁移到同步队列设置的(enq()自旋入队)
             * 入队的逻辑:
             * 1.设置node.prev = tail; 
             * 2.CAS设置当前node为tail,成功才算真正进入到同步队列
             * 3.pred.next = node;
             * 可以推算出,就算prev不为null,也不能说明当前node已经成功入队到同步队列了(CAS成功才算)
             */

             /*
              * next不为null,说明当前节点已经成功入队到同步队列了,且当前节点后面已经有其他node了
              */
            if (node.next != null) 
                return true;
            /*
             * 执行到这里,说明当前节点的状态为 node.witStatus = 0 && node.prev != null
             * findNodeFromTail:从同步队列的尾巴开始向前遍历查找node,查找成功返回ture,失败返回false
             * 当前node有可能在signal过程中,正在迁移中..还未完成
             */
            return findNodeFromTail(node);
        }

	// 在阻塞队列中从后向前遍历查找元素
	private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

checkInterruptWhileWaiting()

    /*
     * 检查线程是在哪里被中断的 等待/同步(正在迁移过程中)
     */    
	private int checkInterruptWhileWaiting(Node node) {
            /*
             * Thread.interrupted():返回当前线程中断标记位,并且重置当前线程标记位为false
             * transferAfterCancelledWait(node):如果当前线程被中断过,才会执行该方法,
             * 被中断过就去判断是在哪里中被中断的,最终返回-1/1(-1表示在等待队列内,1表示在等待队列外(同步队列/迁移过程中))
             * 没有被中断过直接返回0
             */
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

transferAfterCancelledWait()

          /*
           * @return true  表示node是在等待队列中唤醒的
           *		 false 表示node不是在等待队列中唤醒的(同步队列/在迁移过程中)
           */
          final boolean transferAfterCancelledWait(Node node) {
            /*
             * 条件成立:说明当前的node一定是在等待队列中被中断唤醒的
             * CAS期望值为-2,修改为0,signal迁移节点到同步队列时,会将节点的状态修改为0
             */
            if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { 
                // 在等待队列中 被中断唤醒的node也会被迁移到同步队列中
                enq(node);
                // 返回 true 表示node是在等待队列中被中断的
                return true;
            }
            /*
             * 执行到这里有几种情况?
             * 1.当前node已经被外部线程调用signal 方法将其迁移到 同步队列了
             * 2.当前node正在被外部线程调用signal 方法将其迁移到同步队列 进行中..
             * (因为将node从等待队列迁移到同步队列时会先将状态改为0)
             */
            while (!isOnSyncQueue(node))
                // 如果当前线程状态被修改为0,但还未进入同步队列中,则短暂的释放CPU,再去抢占。
                Thread.yield();
            // 返回false这里表示node是在同步队列中被中断的(这里可能有些许歧义,可能此时还未进入同步队列,但该node一定不在等待队列中)
            return false;
        }

unlinkCancelledWaiters()

        /*
         * 将等待队列中的所有取消状态的node全部出队
         */
        private void unlinkCancelledWaiters() {
            // 循环当前节点,从链表的第一个节点开始向后迭代
            Node t = firstWaiter;
            // 当前链表上一个正常状态的node节点
            Node trail = null;
            // 遍历
            while (t != null) {
                Node next = t.nextWaiter;
                // 当前节点t状态不正常(为取消状态)
                if (t.waitStatus != Node.CONDITION) {
                    // 直接将nextWaiter置为null
                    t.nextWaiter = null;
                    // 条件成立:说明遍历到的节点还未碰到过任何正常节点
                    if (trail == null)
                        // 将firstWaiter指向next,即删除中间为取消状态的节点
                        firstWaiter = next;
                    // 执行到else这里,说明已经遇到过正常节点
                    else
                        // 让上一个正常节点指向 取消节点的 下一个节点,即跳过(删除)当前有问题的节点
                        trail.nextWaiter = next;
                    // 条件成立:遍历到链表的末尾了
                    if (next == null)
                        // 更新lastWaiter指向最后一个正常节点
                        lastWaiter = trail;
                }
                // 条件不成立执行到else,说明当前节点是正常节点
                else
                    trail = t; // 更新一下trail
                // 更新下一个节点
                t = next;
            }
        }

reportInterruptAfterWait()

    	/*
         * 根据interruptMode的值决定中断的处理方式
         * 1.interruptMode = THROW_IE(-1):在条件队列内发生过中断
         * 2.interruptMode = REINTERRUPT(1):在条件队列外发生过中断
         */
		private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            // 条件成立:说明在条件队列内发生过中断,此时await方法抛出中断异常
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            // 条件成立:说明在条件队列外发生的中断,此时设置当前线程的中断标记为为 true
            // 中断处理 交给业务代码处理,如果你不处理 什么事也不会发生..
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

signal() 唤醒后的逻辑

        public final void signal() {
            // 判断当前调用signal方法的线程是否是持有锁的线程
            if (!isHeldExclusively())
                // 如果不是 则直接抛异常
                throw new IllegalMonitorStateException();
            // 获取条件队列的第一个node
            Node first = firstWaiter;
            // 不为null,则将第一个节点进行迁移到同步队列的逻辑
            if (first != null)
                doSignal(first);
        }

doSignal()

   		private void doSignal(Node first) {
            // 自旋
            do {
                /*
                 * 当前节点要进行出队,所以更新firstWaiter 指向下一个结点
                 * 如果当前等待队列只有一个节点的话,需要更新lastWaiter为null
                 */
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                // 当前first节点出队,断开和下一个节点的关系
                first.nextWaiter = null;      
                /*
                 * 条件1:transferForSignal(first) true->迁移成功 false->迁移失败,取反后 迁移失败会进入条件2的逻辑:
                 * 条件2:(first = firstWaiter) != null 当前first迁移失败,则将first更新为 first.next 继续尝试迁移..
                 * 直至迁移某个节点成功,或者 条件队列为null为止。
                 * 这里表示要么迁移一个节点成功 要么 队列为空跳出循环。
                 */         
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

AQS.transferForSignal()

	/*
     * 真正迁移节点的逻辑
     * @return 返回true->表示迁移成功 返回false->表示迁移失败..
     */
	final boolean transferForSignal(Node node) {
        /*
         * CAS修改的当前Node的状态为0,因为马上node要迁移到同步队列了,
         * 成功:当前节点在条件队列中状态正常。
         * 失败:
         *   1.取消状态(线程await时 未持有锁,最终线程对应的node会设置为 取消状态)
         *   2.node对应的线程 挂起期间,被其它线程使用 中断信号 唤醒过...(就会主队进入到 阻塞队列,这时也会修改状态为0)
         * 失败返回false,尝试唤醒下一个节点
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        /*
         * enq() 自旋进入同步队列,一定会入队成功,最终返回当前node进入同步队列后的前驱节点
         * enq()方法参考AQS源码解析
         * p 就是当前节点在同步队列的的前驱节点
         */
        Node p = enq(node);
        // 获取前驱节点的waitStatus
        int ws = p.waitStatus;
        /*
         * 条件1:判断如果前驱节点的状态 > 0(取消),那么直接唤醒node。
         * 条件2:前置条件:前驱节点的node状态 <= 0,那么尝试CAS修改为-1(signal),即前驱节点释放锁后唤醒后继节点。
         * 设置失败说明前驱节点的状态突然被取消了,需要唤醒后继节点。
         * 当前驱node对应的线程是lockInterrupt入队时的node,是会响应中断的,
         * 外部线程给前驱node中断信号之后,前驱node会将状态修改为取消状态,并且执行出队逻辑。
         */
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            // 走到这里 说明前驱节点状态处于取消状态,那么直接唤醒当前node对应的线程。
            LockSupport.unpark(node.thread);
        return true;
    }

参考

  • 视频参考
    • b站_小刘讲源码付费课
  • 文章参考
    • shstart7_Condition条件队列源码解析2.await()与signal()解析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/33087.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL索引和事务

目录 1.索引 1.1 索引的作用 1.2 查看索引 1.3 创建索引 1.4 删除索引 1.5 索引背后的数据结构(重点、面试题) 2.事务 2.1 什么是事务? 2.2 事务的使用 2.2.1 回滚 2.2.2 执行 2.3 事务的原子性(事务的初心) 2.4 事务的一致性 2.5 事务的持久性 2.6 事务的隔离性…

STM32之蜂鸣器实验

本章知识点 STM32GPIO的应用 蜂鸣器的原理&#xff08;最好网上看看&#xff09; 蜂鸣器概述 蜂鸣器是一种一体化结构的电子讯响器&#xff0c;采用直流电压供电&#xff0c;广泛应用于计算机、打印机、复印机、报警器、电子玩具、汽车电子设备、电话机、定时器等电子产品中作…

CFD基本概念

1、流动控制方程 2、流体力学中的流动模型及流场解 N-S方程→&#xff08;忽略粘性与热扩散&#xff09;→Euler方程→&#xff08;无旋&#xff09;→全速位方程→&#xff08;小扰动&#xff09;→小扰动方程→&#xff08;不可压&#xff09;→labplace方程&#xff1b; 数…

【机器学习】python实现随机森林

目录 一、模型介绍 1. 集成学习 2. bagging 3. 随机森林算法 二、随机森林算法优缺点 三、代码实现 四、疑问 五、总结 本文使用mnist数据集&#xff0c;进行随机森林算法。 一、模型介绍 1. 集成学习 集成学习通过训练学习出多个估计器&#xff0c;当需要预测时通过…

[附源码]SSM计算机毕业设计流浪动物救助网站JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【百度AI_人脸识别】图片对比相似度、人脸对比登录(调摄像头)

人脸对比 此文档功能&#xff1a; 两张人脸图片相似度对比&#xff1a;比对两张图片中人脸的相似度&#xff0c;并返回相似度分值。存档一张图片与调用的摄像中的人脸进行对比。项目、资源下载&#xff1a;https://download.csdn.net/download/m0_70083523/87150842?spm1001.2…

编译原理—语法制导翻译、S属性、L属性、自上而下、自下而上计算

编译原理—语法制导翻译、S属性、L属性、自上而下、自下而上计算1.语法制导翻译1.1属性文法1.2算术表达式的计数器1.3属性的分类1.4属性依赖图继承属性的计算1.5语义规则的计算方法1.6属性计算次序2. S属性定义2.1 语法树与分析树2.2 语法树与DAG2.2.1构造表达式的语法树(DAG)2…

Android中常见的那些内存泄漏——【问题分析+方案】

1.静态Activity(Activity上下文Context)和View 静态变量Activity和View会导致内存泄漏&#xff0c;在下面代码中对Activity的Context和TextView设置为静态对象&#xff0c;从而产生内存泄漏&#xff1b; public class MemoryTestActivity extends AppCompatActivity {private…

[附源码]SSM计算机毕业设计健身健康规划系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

noexcept说明符/运算符

一、noexcept说明符 1、语法 &#xff08;1&#xff09;noexcept 与 noexcept(true) 相同 &#xff08;2&#xff09;noexcept&#xff08;表达式&#xff09; 如果 表达式 求值为 true&#xff0c;那么声明函数不会抛出任何异常。 &#xff08;3&#xff09;throw() //c1…

Ubuntu配置FTP服务

参考目录1.安装FTP服务器软件2.配置FTP服务3.Ubuntud登录ftp服务器4.windows下通过cuteFTPlianjei1.安装FTP服务器软件 (1) FTP文件传送协议(File Transfer Protocol&#xff0c;简称FTP)&#xff0c;是一个用于从一台主机到另一台主机传输文件的协议。 (2&#xff09;Linux下有…

Jetpack 之 LiveData 实现事件总线

事件总线相信大家很多时候都会用到&#xff0c;那大家常用的也就是常青树 EventBus&#xff0c;以及 RxJava 流行起来的后起之秀 RxBus。它们的使用方式都差不多&#xff0c;思想也都是基于观察者模式&#xff0c;正好 LiveData 的核心思想也是观察者模式&#xff0c;因此我们完…

做Android 开发这么久,还不明白 Android Framework 知识重要性?

Framework作为Android的框架层&#xff0c;为App提供了很多API调用&#xff0c;但很多机制都是Framework包装好后直接给App用的&#xff0c;如果不懂这些机制的原理&#xff0c;就很难在这基础上进行优化。 从做Android的第一天起&#xff0c;你一定听过无数次关于Framework的…

计算机音乐-乐理知识(1)

一、节拍 节拍&#xff08;Beat/Meter&#xff09;&#xff0c;是一个衡量节奏的单位&#xff0c;在音乐中&#xff0c;有一定强弱分别的一系列拍子在每隔一定时间重复出现。如 2 / 4 、 4 / 4 、 3 / 4 拍等。节拍&#xff0c;乐曲中表示固定单位时值和强弱规律的组织形式。 …

测试员工作三年后的工资对比,没达到这个数的都属于拖后腿了

“毕业三年的薪资是职场阶段的一个分水岭。” 不知什么时候开始&#xff0c;这句话深刻的引入了所有打工人的心中&#xff0c;程序员们自然也不例外。 事实上&#xff0c;这句话说的并不无道理&#xff0c;毕业的三年&#xff0c;不仅是学生到职场人身份上的一个转变&#xf…

初阶数据结构学习记录——아홉 二叉树和堆(2)

接着上一篇 之前写过一些关于堆的代码&#xff0c;向下调整&#xff0c;向上调整算法&#xff0c;以及常用的几个函数。这一篇继续完善堆&#xff0c;难度也会有所上升。先来看上一篇文末提到的创建堆算法。 首先要有空间&#xff0c;要有数据&#xff0c;之后再形成堆。我们…

9.5 利用可执行内存挑战DEP

目录 一、实验环境 二、实验思路 三、实验代码 四、实验步骤 1、寻找memcpy函数的地址 2、查看内存中可读可写可执行的内存 3、修复EBP 4、保证memcpy的源地址位于shellcode之前 一、实验环境 操作系统&#xff1a;windows 2000 软件&#xff1a;原版OD、VC6.0 二、实…

删除的数据如何恢复?误删了文件怎么恢复

文件的误删除&#xff0c;相信大部分人都经历过。不过因为很多人删除的文件都不算是很重要&#xff0c;所以有与没有并没有太大的区别。但是一旦你删除的文件正是你最近急需的&#xff0c;删除的数据如何恢复&#xff1f;别着急&#xff0c;可以试试以下的几种方法&#xff1a;…

STM32串口详解

实验一&#xff1a;简单的利用串口接收中断回调函数实现数据的返回 关于串口调试助手&#xff0c;还应知道&#xff1a; 发送英文字符需要用一个字符即8位&#xff0c;发送汉字需要两个字符即16位&#xff0c;如上图&#xff0c;发送汉字“姜”实际是发送“BD AA”而发送英文字…

外卖项目06---套餐管理业务开发(移动端的后台代码编辑开发)

菜品展示、购物车、下单 目录 一、导入用户地址簿相关功能代码 90 1.1需求分析 90 1.2数据模型 90 1.3导入功能代码 90 二、菜品展示 91 2.1需求分析 91 2.2商品展示---代码开发---梳理交互过程 92 2.3菜品展示---代码开发---修改DishController的list方法并测试 93 2…