有图解有案例,我终于把 Condition 的原理讲透彻了
哈喽大家好,我是阿Q!
20张图图解ReentrantLock加锁解锁原理文章一发,便引发了大家激烈的讨论,更有小伙伴前来弹窗:平时加解锁都是直接使用Synchronized关键字来实现的,简单好用,为啥还要引用ReentrantLock呢?
为了解决小伙伴的疑问,我们来对两者做个简单的比较吧:
相同点
两者都是“可重入锁”,即当前线程获取到锁对象之后,如果想继续获取锁对象还是可以继续获取的,只不过锁对象的计数器进行“+1”操作就可以了。
不同点
ReentrantLock是基于API实现的,Synchronized是依赖于JVM实现的;ReentrantLock可以响应中断,Synchronized是不可以的;ReentrantLock可以指定是公平锁还是非公平锁,而Synchronized只能是非公平锁;ReentrantLock的lock是同步非阻塞,采用的是乐观并发策略,Synchronized是同步阻塞的,使用的是悲观并发策略;ReentrantLock借助Condition可以实现多路选择通知,Synchronized通过wait()和notify()/notifyAll()方法可以实现等待/通知机制(单路通知);
综上所述,ReentrantLock还是有区别于Synchronized的使用场景的,今天我们就来聊一聊它的多路选择通知功能。
实战
没有实战的“纸上谈兵”都是扯淡,今天我们反其道而行,先抛出实战Demo。
场景描述
加油站为了吸引更多的车主前来加油,在加油站投放了自动洗车机来为加油的汽车提供免费洗车服务。我们规定汽车必须按照“加油->洗车->驶离”的流程来加油,等前一辆汽车驶离之后才允许下一辆车进来加油。
代码实现
首先创建锁对象并生成三个Condition
/*** 控制线程唤醒的标志*/
private int flag = 1;/*** 创建锁对象*/
private Lock lock = new ReentrantLock();/*** 等待队列* c1对应加油* c2对应洗车* c3对应开车*/
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
然后声明加油、清洗、驶离的方法,并规定加完油之后去洗车并驶离加油站
/*** 汽车加油*/
public void fuelUp(int num) {lock.lock();try {while (flag!=1){c1.await();}System.out.println("第"+num+"辆车开始加油");flag = 2;c2.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}/*** 汽车清洗*/
public void carWash(int num) {lock.lock();try {while (flag!=2){c2.await();}System.out.println("第"+num+"辆车开始清洗");flag = 3;c3.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}
}/*** 驶离*/
public void drive(int num) {lock.lock();try {while (flag!=3){c3.await();}System.out.println("第"+num+"辆车已经驶离加油站");flag = 1;c1.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}
}
其中
await为等待方法,signal为唤醒方法。
最后我们来定义main方法,模拟一下3辆车同时到达加油站的场景
public static void main(String[] args) {CarOperation carOperation = new CarOperation();//汽车加油new Thread(()->{for (int i = 1; i < 4; i++) {carOperation.fuelUp(i);}},"fuelUp").start();//汽车清洗new Thread(()->{for (int i = 1; i < 4; i++) {carOperation.carWash(i);}},"carRepair").start();//驶离new Thread(()->{for (int i = 1; i < 4; i++) {carOperation.drive(i);}},"drive").start();
}
使用是不是很丝滑?为了加深大家对Condition的理解,接下来我们用图解的方式分析一波Condition的原理~
图解
大家都看到了,上边的案例都是围绕Condition来操作的,那什么是Condition呢?Condition是一个接口,里边定义了线程等待和唤醒的方法。

代码中调用的lock.newCondition()实际调用的是Sync类中的newCondition方法,而ConditionObject就是Condition的实现类。
final ConditionObject newCondition() {return new ConditionObject();
}
我们发现它处于AQS的内部,没法直接实例化,所以需要配合ReentrantLock来使用。
ConditionObject

ConditionObject内部维护了一个基于Node的FIFO单向队列,我们把它称为等待队列。firstWaiter指向首节点,lastWaiter指向尾节点,Node中的nextWaiter指向队列中的下一个元素,并且等待队列中节点的waitStatus都是-2。
了解了ConditionObject的数据结构之后,我们就从源码角度来图解一下ReentrantLock的等待/唤醒机制。
await
首先找到AQS类中await的源码
public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();//将当前线程封装成node加入等待队列尾部Node node = addConditionWaiter();int savedState = fullyRelease(node);int interruptMode = 0;//检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待直到检测到此节点在同步队列上while (!isOnSyncQueue(node)) {//当node处于等待队列时,挂起当前线程。LockSupport.park(this);//如果发生了中断,则跳出循环,结束等待if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}//被唤醒后该节点一定会在AQS队列上,//之前分析过acquireQueued方法获取不到锁会继续阻塞//获取到了锁,中断过返回true,未中断过返回false//获取到锁存在中断并且不是中断唤醒的线程将中断模式设置为重新中断if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null) // clean up if cancelled//清除条件队列中所有状态不为 CONDITION 的结点unlinkCancelledWaiters();if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}
如果线程中断,清除中断标记并抛出异常。
查看addConditionWaiter
该方法的作用是将当前线程封装成node加入等待队列尾部
private Node addConditionWaiter() {Node t = lastWaiter;if (t != null && t.waitStatus != Node.CONDITION) {//将不处于等待状态的结点从等待队列中移除unlinkCancelledWaiters();t = lastWaiter;}Node node = new Node(Thread.currentThread(), Node.CONDITION);//尾节点为空if (t == null)//将首节点指向nodefirstWaiter = node;else//将尾节点的nextWaiter指向node节点t.nextWaiter = node;//尾节点指向nodelastWaiter = node;return node;
}
首先将t指向尾节点,如果尾节点不为空并且它的waitStatus!=-2,则将不处于等待状态的结点从等待队列中移除,并且将t指向新的尾节点。
将当前线程封装成waitStatus为-2的节点追加到等待队列尾部。
如果尾节点为空,则队列为空,将首尾节点都指向当前节点。

如果尾节点不为空,证明队列中有其他节点,则将当前尾节点的nextWaiter指向当前节点,将当前节点置为尾节点。

接着我们来查看下unlinkCancelledWaiters()方法——将不处于等待状态的结点从等待队列中移除。
private void unlinkCancelledWaiters() {Node t = firstWaiter;//trail是t的前驱结点Node trail = null;while (t != null) {//next为t的后继结点Node next = t.nextWaiter;//如果t节点的waitStatus不为-2即失效节点if (t.waitStatus != Node.CONDITION) {t.nextWaiter = null;//如果t的前驱节点为空,则将首节点指向nextif (trail == null)firstWaiter = next;else//t的前驱结点不为空,将前驱节点的后继指针指向nexttrail.nextWaiter = next;//如果next为null,则将尾节点指向t的前驱节点if (next == null)lastWaiter = trail;}elsetrail = t;t = next;}
}
t为当前节点,trail为t的前驱节点,next为t的后继节点。
while方法会从首节点顺着等待队列往后寻找waitStatus!=-2的节点,将当前节点的nextWaiter置为空。
如果当前节点的前驱节点为空,代表当前节点为首节点,则将next设置为首节点;

如果不为空,则将前驱节点的nextWaiter指向后继节点。

如果后继节点为空,则直接将前驱节点设置为尾节点。

查看fullyRelease
从名字也差不多能明白该方法的作用是彻底释放锁资源。
final int fullyRelease(Node node) {//释放锁失败为true,释放锁成功为falseboolean failed = true;try {//获取当前锁的stateint savedState = getState();//释放锁成功的话if (release(savedState)) {failed = false;return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)//释放锁失败的话将节点状态置为取消node.waitStatus = Node.CANCELLED;}
}
最重要的就是release方法,而我们上文中已经讲过了,release执行成功的话,当前线程已经释放了锁资源。
查看isOnSyncQueue
判断当前线程所在的Node是否在同步队列中(同步队列即AQS队列)。在这里有必要给大家看一下同步队列与等待队列的关系图了。

final boolean isOnSyncQueue(Node node) {if (node.waitStatus == Node.CONDITION || node.prev == null)return false;if (node.next != null) return true;//node节点的next为nullreturn findNodeFromTail(node);
}
如果当前节点的waitStatus=-2,说明它在等待队列中,返回false;如果当前节点有前驱节点,则证明它在AQS队列中,但是前驱节点为空,说明它是头节点,而头节点是不参与锁竞争的,也返回false。
如果当前节点既不在等待队列中,又不是AQS中的头结点且存在next节点,说明它存在于AQS中,直接返回true。
接着往下看,如果当前节点的next为空,该节点可能是tail节点,也可能是该节点的next还未赋值,所以需要从后往前遍历节点。
private boolean findNodeFromTail(Node node) {Node t = tail;for (;;) {//先用尾节点来判断,然后用队列中的节点依次来判断if (t == node)return true;//节点为空,说明找到头也不在AQS队列中,返回falseif (t == null)return false;t = t.prev;}
}
在遍历过程中,如果队列中有节点等于当前节点,返回true;如果找到头节点也没找到,则返回false。
我们回到await的while循环处,如果返回false,说明该节点不在同步队列中,进入循环中挂起该线程。
知识点补充
阿Q的理解是线程被唤醒会存在两种情况:一种是调用signal/signalAll唤醒线程;一种是通过线程中断信号,唤醒线程并抛出中断异常。
查看checkInterruptWhileWaiting(难点)
该方法的作用是判断当前线程是否发生过中断,如果未发生中断返回0,如果发生了中断返回1或者-1。
private int checkInterruptWhileWaiting(Node node) {return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :0;
}
我们来看看transferAfterCancelledWait方法是如果区分1和-1的
final boolean transferAfterCancelledWait(Node node) {//cas尝试将node的waitStatus设置为0if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {//将node节点由等待队列加入AQS队列enq(node);return true;}//cas失败后,看看队列是不是已经在AQS队列中,如果不在,则通过yield方法给其它线程让路while (!isOnSyncQueue(node))Thread.yield();//如果已经在AQS队列中,则返回falsereturn false;
}
那什么情况下cas操作会成功?什么情况下又会失败呢?
当线程接收到中断信号时会被唤醒,此时node的waitStatus=-2,所以会cas成功,同时会将node从等待队列转移到AQS队列中。
当线程先通过signal唤醒后接收到中断信号,由于signal已经将node的waitStatus设置为-2了,所以此时会cas失败。
举例
大家可以用下边的例子在transferAfterCancelledWait中打断点测试一下,相信就明了了。
public class CarOperation {//创建一个重入锁private Lock lock = new ReentrantLock();//声明等待队列Condition c1 = lock.newCondition();/** 等待操作*/public void await() {lock.lock();try {System.out.println("开始阻塞");c1.await();System.out.println("唤醒之后继续执行");} catch (InterruptedException e) {System.out.println("唤醒但是抛出异常了");e.printStackTrace();} finally {lock.unlock();}}/** 唤醒操作*/public void signal() {lock.lock();try {c1.signal();System.out.println("唤醒了。。。。。。。。。。。。。。");} finally {lock.unlock();}}
}
中断测试
public static void main(String[] args) {CarOperation carOperation = new CarOperation();Thread t1 = new Thread(()->{//等待,挂起线程carOperation.await();});t1.start();try {//模拟其它线程抢占资源执行过程Thread.sleep(10000);//发出线程中断信号t1.interrupt();} catch (InterruptedException exception) {exception.printStackTrace();}
}

先唤醒后中断测试
public static void main(String[] args) {CarOperation carOperation = new CarOperation();Thread t1 = new Thread(()->{carOperation.await();});t1.start();try {Thread.sleep(10000);//先唤醒线程carOperation.signal();//后中断t1.interrupt();} catch (InterruptedException exception) {exception.printStackTrace();}
}

查看reportInterruptAfterWait
//要么抛出异常,要么重新中断。
private void reportInterruptAfterWait(int interruptMode)throws InterruptedException {if (interruptMode == THROW_IE)throw new InterruptedException();else if (interruptMode == REINTERRUPT)selfInterrupt();
}
以上就是await的全部内容了,我们先来做个简单的总结。
总结
- 将当前线程封装成
node加入等待队列尾部; - 彻底释放锁资源,也就是将它的同步队列节点从同步队列队首移除;
- 如果当前节点不在同步队列中,挂起当前线程;
- 自旋,直到该线程被中断或者被唤醒移动到同步队列中;
- 阻塞当前节点,直到它获取到锁资源;
如果你哪个地方存在疑问可以小窗阿Q!
signal
接下来我们再来捋一捋唤醒的过程
public final void signal() {//当前线程是否是锁的持有者,不是的话抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)//具体的唤醒过程doSignal(first);
}private void doSignal(Node first) {do {//获取头结点的下一个节点并赋值为头结点if ( (firstWaiter = first.nextWaiter) == null)lastWaiter = null;//将之前的头节点置为空first.nextWaiter = null;//将头结点从等待队列转移到AQS队列中,如果转移失败,则寻找下一个节点继续转移} while (!transferForSignal(first) &&(first = firstWaiter) != null);
}
首先将等待队列的头结点从等待队列中取出来

然后执行transferForSignal方法进行转移
final boolean transferForSignal(Node node) {//将node的waitStatus设置为0,如果设置失败说明node的节点已经不在等待队列中了,返回falseif (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;//将node从等待队列转移到AQS队列,并返回node的前驱节点Node p = enq(node);//获取node前驱节点的状态int ws = p.waitStatus;//如果该节点是取消状态或者将其设置为唤醒状态失败(说明本身已经是唤醒状态了),所以可以去唤醒node节点所在的线程if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))//唤醒当前节点LockSupport.unpark(node.thread);return true;
}
将等待队列的头结点从等待队列转移到AQS队列中,如果转移失败,说明该节点已被取消,直接返回false,然后将first指向新的头结点重新进行转移。如果转移成功则根据前驱节点的状态判断是否直接唤醒当前线程。

怎么样?唤醒的逻辑是不是超级简单?我们也按例做个简单的总结。
总结
从等待队列的队首开始,尝试对队首节点执行唤醒操作,如果节点已经被取消了,就尝试唤醒下一个节点。
对首节点执行唤醒操作时,首先将节点转移到同步队列,如果前驱节点的状态为取消状态或设置前驱节点的状态为唤醒状态失败,那么就立即唤醒当前节点对应的线程,否则不执行唤醒操作。
以上就是今天的全部内容了,我们下期再见。跪求一键三连,更文很累的,不要白嫖我,需要一点正反馈。点击名片与我联系,希望在这个冷漠的城市里,让我们互相温暖。
相关文章:
有图解有案例,我终于把 Condition 的原理讲透彻了
哈喽大家好,我是阿Q! 20张图图解ReentrantLock加锁解锁原理文章一发,便引发了大家激烈的讨论,更有小伙伴前来弹窗:平时加解锁都是直接使用Synchronized关键字来实现的,简单好用,为啥还要引用Re…...
Linux之找回root密码
文章目录前言一、启动系统二、进入编辑界面三、修改密码前言 当我们使用root用户登陆Linux时,忘记了登陆密码,改怎样修改登陆密码呢,接下来将介绍如何修改root密码 一、启动系统 首先,启动系统,进入开机界面&#x…...
stack_queue | priority_queue | 仿函数
文章目录1. stack 的使用2. stack的模拟实现3. queue的使用4. queue的模拟实现5. deque ——双端队列deque优缺点6. priority_queue ——优先级队列1. priority_queue的使用2. priority_queue的模拟实现push——插入pop ——删除top —— 堆顶仿函数问题完整代码实现1. stack 的…...
第十四届蓝桥杯三月真题刷题训练——第 14 天
目录 第 1 题:组队 题目描述 运行限制 代码: 第 2 题:不同子串 题目描述 运行限制 代码: 思路: 第 3 题:等差数列 题目描述 输入描述 输出描述 输入输出样例 运行限制 代码: 思…...
【Hadoop-yarn-01】大白话讲讲资源调度器YARN,原来这么好理解
YARN作为Hadoop集群的御用调度器,在整个集群的资源管理上立下了汗马功劳。今天我们用大白话聊聊YARN存在意义。 有了机器就有了资源,有了资源就有了调度。举2个很鲜活的场景: 在单台机器上,你开了3个程序,分别是A、B…...
技术掉:PDF显示,使用pdf.js
PDF 显示 场景: 其实直接显示 pdf 可以用 iframe 标签,但产品觉得浏览器自带的 pdf 预览太丑了,而且无法去除那些操作栏。 解决方案:使用 pdf.js 进行显示 第一步:引入 pdf.js 去官网下载稳定版的 pdf.js 文件 然后…...
有关pytorch的一些总结
Tensor 含义 张量(Tensor):是一个多维数组,它是标量、向量、矩阵的高维拓展。 创建 非随机创建 1.用数组创建 将数组转化为tensor np.ones([a,b]) 全为1 #首先导入PyTorch import torch#数组创建 import numpy as np anp.arr…...
基础IO【Linux】
文章目录:文件相关知识C语言文件IOstdin & stdout & stderr系统文件 IOopenclosewriteread文件描述符文件描述符的分配规则重定向dup2系统调用FILEFILE中的文件描述符FILE中的缓冲区理解文件相关知识 文件 文件内容 文件属性(每一个已经存在的…...
Vue3——自定义封装上传图片样式
自定义封装上传图片样式 一、首先需要新建一个自组建完善基础的结构,我这里起名为ImgUpload.vue <el-upload name"file" :show-file-list"false" accept".png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.bmp,.BMP" :multiple"…...
ChatGLM-6B (介绍以及本地部署)
中文ChatGPT平替——ChatGLM-6BChatGLM-6B简介官方实例本地部署1.下载代码2.通过conda创建虚拟环境3.修改代码4.模型量化5.详细代码调用示例ChatGLM-6B 简介 ChatGLM-6B 是一个开源的、支持中英双语问答的对话语言模型,基于 General Language Model (GLM) 架构&…...
react的基础使用
react中为什么使用jsxReact 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。react认为将业务代码和数据以及事件等等 需要和UI高度耦合…...
letcode 4.寻找两个正序数组的中位数(官方题解笔记)
题目描述 给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (mn)) 。 1.二分查找 1.1思路 时间复杂度:O(log(mn)) 空间复杂度:O(1) 给定…...
【面试题系列】K8S常见面试题
目录 序言 问题 1. 简单说一下k8s集群内外网络如何互通的吧 2.描述一下pod的创建过程 3. 描述一下k8s pod的终止过程 4.Kubernetes 中的自动伸缩有哪些方式? 5.Kubernetes 中的故障检测有哪些方式? 6.Kubernetes 中的资源调度有哪些方式ÿ…...
字符函数和字符串函数(上)-C语言详解
CSDN的各位友友们你们好,今天千泽为大家带来的是C语言中字符函数和字符串函数的详解,掌握了这些内容能够让我们更加灵活的运用字符串,接下来让我们一起走进今天的内容吧!写这篇文章需要在cplusplus.com上大量截图,十分不易!如果对您有帮助的话希望能够得到您的支持和帮助,我会持…...
全连接神经网络
目录 1.全连接神经网络简介 2.MLP分类模型 2.1 数据准备与探索 2.2 搭建网络并可视化 2.3 使用未预处理的数据训练模型 2.4 使用预处理后的数据进行模型训练 3. MLP回归模型 3.1 数据准备 3.2 搭建回归预测网络 1.全连接神经网络简介 全连接神经网络(Multi-Layer Percep…...
深度学习目标检测ui界面-交通标志检测识别
深度学习目标检测ui界面-交通标志检测识别 为了将算法封装起来,博主尝试了实验pyqt5的上位机界面进行封装,其中遇到了一些坑举给大家避开。这里加载的训练模型参考之前写的博客: 自动驾驶目标检测项目实战(一)—基于深度学习框架yolov的交通…...
ubuntu不同版本的源(换源)(镜像源)(lsb_release -c命令,显示当前系统的发行版代号(Codename))
文章目录查看unbuntu版本名(lsb_release -c命令)各个版本源代号(仅供参考,具体代号用上面命令查)各版本软件源Ubuntu20.10阿里源:清华源:Ubuntu20.04阿里源:清华源:Ubunt…...
linux入门---程序翻译的过程
我们在vs编译器中写的代码按下ctrl f5就可以直接运行起来,并且会将运行的结果显示到显示器上,这里看上去只有一个步骤但实际上这里会存在很多的细节,比如说生成结果在这里插入代码片之前我们的代码会经过预处理,编译,汇…...
springboot复习(黑马)
学习目标基于SpringBoot框架的程序开发步骤熟练使用SpringBoot配置信息修改服务器配置基于SpringBoot的完成SSM整合项目开发一、SpringBoot简介1. 入门案例问题导入SpringMVC的HelloWord程序大家还记得吗?SpringBoot是由Pivotal团队提供的全新框架,其设计…...
C++指针详解
旧文更新:两三年的旧文了,一直放在电脑里,现在直接传上CSDN 一、指针的概念 1.1 指针 程序运行时每个变量都会有一块内存空间,变量的值就存放在这块空间中。程序可以通过变量名直接访问这块空间内的数据,这种访问方…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...
【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...
ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
C++.OpenGL (20/64)混合(Blending)
混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...
