科普文:JUC系列之多线程门闩同步器Condition的使用和源码解读
一、概述
条件锁就是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。
比如,在阻塞队列中,当队列中没有元素的时候是无法弹出一个元素的,这时候就需要阻塞在条件notEmpty上,等待其它线程往里面放入一个元素后,唤醒这个条件notEmpty,当前线程才可以继续去做“弹出一个元素”的行为。
注意,这里的条件,必须是在获取锁之后去等待,对应到ReentrantLock的条件锁,就是获取锁之后才能调用condition.await()方法。
在Java中,条件锁的实现都在AQS的ConditionObject类中,ConditionObject实现了Condition接口,所以ReentrantLock的条件锁是基于AQS实现的。
sychronized + Object.wait = Lock + Condition
二、案例
public class ReentrantLockTest {public static void main(String[] args) throws InterruptedException {// 声明一个重入锁ReentrantLock lock = new ReentrantLock();// 声明一个条件锁Condition condition = lock.newCondition();// 创建一个线程并执行,该线程就是当达到条件锁条件后,来执行后续的相关逻辑的。// 可以看作是一个消费者new Thread(() -> {try {// 获取锁lock.lock(); // 1try {System.out.println("before await"); // 2// 等待条件 // 该线程执行到这里就会进入到阻塞状态,直到达到了条件后,// 由其他线程执行signal()方法来告知该线程已经达到条件了,// 该线程就会在这里被唤醒继续向下执行condition.await(); // 3System.out.println("after await"); // 10} finally {// 释放锁lock.unlock(); // 11}} catch (InterruptedException e) {e.printStackTrace();}}).start();// 这里睡1000ms是为了让上面的线程先获取到锁Thread.sleep(1000);// main方法的当前线程来获取锁,该线程就是用来进行相关业务处理,进而达到条件锁条件的。// 可以看作是一个生产者lock.lock(); // 4try {// 这里睡2000ms代表这个线程执行业务需要的时间,// 当完成这里的2000ms之后就认为是符合条件锁条件了Thread.sleep(2000); // 5System.out.println("before signal"); // 6// 通知条件已成立condition.signal(); // 7System.out.println("after signal"); // 8} finally {// 释放锁lock.unlock(); // 9}}
}
上面的代码很简单,一个线程等待条件,另一个线程通知条件已成立,后面的数字代表代码实际运行的顺序。
由上面的例子我们也可以看出,await()和signal()方法都必须在获取锁之后释放锁之前使用;
三、源码分析
3.1 Condition接口
Condition是一个接口,从1.5的时候出现的,是用来替代Object的wait、notify。所以显而易见,Condition的await和signal肯定效率更高、安全性更好。Condition是依赖于lock实现的。并且await和signal只能在lock的保护之内使用。
package java.util.concurrent.locks;import java.util.concurrent.TimeUnit;
import java.util.Date;public interface Condition {//导致当前线程等到发信号或 interrupted 。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();
}
3.2 内部类
ConditionObject是Condition的实现类。
//AbstractQueuedSynchronizer.ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {/** First node of condition queue. */private transient Node firstWaiter;/** Last node of condition queue. */private transient Node lastWaiter;//...
}
可以看到条件锁中也维护了一个队列,为了和AQS的队列区分,我这里称为条件队列,firstWaiter是队列的头节点,lastWaiter是队列的尾节点。
ConditionObject中的条件队列和AQS中的同步队列使用的是相同的节点类型Node,但是两个队列还是有一些不同的,在后续会详细讲解。
3.3 lock.newCondition()方法
新建一个条件锁。
- ReentrantLock.newCondition()
- ReentrantLock.Sync.newCondition()
- AbstractQueuedSynchronizer.ConditionObject.ConditionObject()
public class ReentrantLock implements Lock, java.io.Serializable {private final Sync sync;// 创建条件锁public Condition newCondition() {return sync.newCondition();}/*** 抽象内部类*/abstract static class Sync extends AbstractQueuedSynchronizer {// 条件锁final ConditionObject newCondition() {return new ConditionObject();}}
}// AbstractQueuedSynchronizer.ConditionObject.ConditionObject()
public ConditionObject() { }
新建一个条件锁最后就是调用的AQS中的ConditionObject类来实例化条件锁。
3.4 condition.await()方法
condition.await()方法,表明现在要等待条件的出现,只有满足条件之后获取锁的线程才可以继续向后执行。
await()方法会新建一个节点放到条件队列中,接着完全释放锁,然后阻塞当前线程并等待条件的出现;
// AbstractQueuedSynchronizer.ConditionObject.await()
public final void await() throws InterruptedException {// 如果线程中断了,抛出异常if (Thread.interrupted())throw new InterruptedException();// 添加节点到Condition的队列中,并返回该节点Node node = addConditionWaiter();// 完全释放当前线程获取的锁// 因为锁是可重入的,所以这里要把获取的锁全部释放int savedState = fullyRelease(node);/*** 中断标志,用来标识线程是否是被中断唤醒的* interruptMode = 0:表示不是被中断唤醒的* interruptMode != 0:表示是被中断唤醒的* interruptMode = REINTERRUPT = 1:表示当前线程在其他线程调用signal()之后被中断唤醒* interruptMode = THROW_IE = -1:表示当前线程在其他线程调用signal()之前被中断唤醒*/int interruptMode = 0;// 是否在同步队列中,如果该线程节点从条件队列移出到同步队列中,// 说明当前已经满足条件,线程已经被唤醒,则跳出循环while (!isOnSyncQueue(node)) {// 阻塞当前线程LockSupport.park(this);// 上面部分是调用await()时释放自己占有的锁,并阻塞自己等待条件的出现// *************************分界线************************* //// 下面部分是条件已经出现,该线程被唤醒,尝试重新去获取锁继续向后执行// checkInterruptWhileWaiting()中会判断当前线程是否是被中断唤醒的// 返回值非0表示是被中断唤醒的,会通过break跳出while。// 因为如果是中断唤醒的,有可能实际上该线程还没有等到条件满足的时候就被唤醒了,// 这样该线程的Node节点一定没有被转移到同步队列中,所以就不可能通过循环条件来跳出循环。// 只能是我们手动调用break来跳出循环,毕竟await()方法还是要响应中断的,// 不能在其他线程已经发出中断信号后,还让线程在这个循环里自选而不响应中断。if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 尝试获取锁,注意第二个参数,这里需要获取所的数量要和该线程最初持有锁的数量相同,// 使该线程恢复到最开始的持有锁的状态/*** 我们从两个条件分别去分析* 1、acquireQueued(node, savedState)* 尝试获取锁,注意第二个参数,这里需要获取所的数量要和该线程最初持有锁的数量相同,* 使该线程恢复到最开始的持有锁的状态,* 该方法在之前ReentrantLock章节中已经讲解过了,在这个方法中线程就会不断地尝试获取锁,* 如果没获取到就会再次阻塞在这个位置,等到被唤醒后继续抢占锁,直到成功获取锁之后该方法才会返回。* 该方法的返回值是中断标记,如果该线程是被中断信号唤醒并且抢占到锁的,就会返回true,* 如果线程不是被中断唤醒的,则返回false。* * 2、interruptMode != THROW_IE* interruptMode标识的是线程在等待条件满足被阻塞的过程当中,是否是被中断唤醒的* 这里的条件interruptMode != THROW_IE表示该线程不是在其他线程调用signal()之前被中断唤醒的* 所以第一个条件是表示该线程在重新获取锁的时被阻塞,然后又被唤醒重新获取到锁的过程中,是不是被中断唤醒的* 第二个条件表示该线程被阻塞,等待条件满足的过程当中,是不是被中断唤醒的* 第一个条件为true,说明该线程是在重新获取锁的过程中接收到过中断信号,也就说是这一次中断是在该线程等待* 条件被阻塞然后又被唤醒之后才发生的,这个中断信号一定是发生在其他线程调用signal()之后* 第二个条件为true,说明该线程在其他线程调用signal之前没有接收到过中断信号* 当两个条件都为true时,就会进入到if代码块中,将interruptMode = REINTERRUPT,也就是该线程收到了中断信号,* 并且是在其他线程调用signal()之后收到的中断信号* 如果该线程实在signal之前被中断的,那么该线程的流程就到此结束了,需要从头再来获取锁,* 就不能执行后续的步骤了,也就不能进入到该if代码块中*/// 如果没获取到会再次阻塞if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 清除取消的节点if (node.nextWaiter != null) // clean up if cancelledunlinkCancelledWaiters();// 线程中断相关if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}// AbstractQueuedSynchronizer.ConditionObject.addConditionWaiter
// 为线程创建Node节点并将其添加到条件队列当中去
private Node addConditionWaiter() {Node t = lastWaiter;/*** 如果条件队列的尾节点已取消(非Node.CONDITION状态),从头节点开始清除所有已取消的节点* * 该条件成立的例子:* 1、假设有两个线程thread0、thread1,初始时,thread0在未持有锁的情况下调用AQS.CO.await(),* thread0执行到AQS.fullyRelease中时会将其对应节点的waitStatus字段设置为取消状态,* 之后持有锁的thread1调用 AQS.CO.await(),会执行到这里,这是条件队列尾节点t.waitStatus为1* * 2、假设有两个线程thread0、thread1,初始时,thread0持有锁,* 之后调用AQS.CO.await()释放锁并阻塞在LockSupport.park(this)处,* 之后外部线程中断thread0,thread0被唤醒后会执行到AQS.transferAfterCancelledWait的if处 * 将t.waitStatus设置为0,之后thread1获取到锁,执行到这里时,t.waitStatus为0*/ if (t != null && t.waitStatus != Node.CONDITION) {// 移除条件队列中所有不是Node.CONDITION状态的节点 unlinkCancelledWaiters();// 重新获取最新的尾节点,经过unlinkCancelledWaiters(),lastWaiter可能已经改变t = lastWaiter;}// 新建一个节点,它的等待状态是CONDITIONNode node = new Node(Thread.currentThread(), Node.CONDITION);// 如果尾节点为空,则把新节点赋值给头节点(相当于初始化队列)// 否则把新节点赋值给尾节点的nextWaiter指针if (t == null)firstWaiter = node;elset.nextWaiter = node;// 尾节点指向新节点lastWaiter = node;// 返回新节点return node;
}// AbstractQueuedSynchronizer.fullyRelease
// 完全释放当前线程获取的锁
final int fullyRelease(Node node) {boolean failed = true;try {// 获取状态变量的值,重复获取锁,这个值会一直累加// 所以这个值也代表着获取锁的次数int savedState = getState();// 一次性释放所有获得的锁if (release(savedState)) {failed = false;// 返回获取锁的次数return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}
}// AbstractQueuedSynchronizer.isOnSyncQueue
// 判断当前线程节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {// 如果等待状态是CONDITION,或者前一个指针为空,返回false// 说明还没有移到AQS的队列中if (node.waitStatus == Node.CONDITION || node.prev == null)return false;// 如果next指针有值,说明已经移到AQS的队列中了if (node.next != null) // If has successor, it must be on queuereturn true;// 从AQS的尾节点开始往前寻找看是否可以找到当前节点,找到了也说明已经在AQS的队列中了return findNodeFromTail(node);
}// AbstractQueuedSynchronizer.findNodeFromTail()
private boolean findNodeFromTail(Node node) {Node t = tail;for (;;) {if (t == node)return true;if (t == null)return false;t = t.prev;}
}// AbstractQueuedSynchronizer.ConditionObject.checkInterruptWhileWaiting()
// 根据节点的中断情况来返回其中断标志
private int checkInterruptWhileWaiting(Node node) {/*** 通过Thread.interrupted()来判断该线程是不是被中断,如果没有被中断则返回0* 如果被中断了,则在通过transferAfterCancelledWait(node)来判断该线程是在其他线程调用signal()前被中断,* 还是调用signal()后被中断* 返回THROW_IE表示当前线程在其他线程调用signal前被中断* 返回REINTERRUPT表示当前线程在其他线程调用signal后被中断* 具体的分界点就是node.waitStatus的值,若其值为Node.CONDITION,是signal前被中断,否则在signal后被中断*/return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}// AbstractQueuedSynchronizer.ConditionObject.transferAfterCancelledWait()
// 返回true表示该节点是在调用signal()之前被中断的,返回false表示是调用signal()之后被中断的
final boolean transferAfterCancelledWait(Node node) {/*** 如果node.waitStatus的值是Node.CONDITION,说明该节点还没有被signal()方法唤醒,* 也就是说该节点是在调用signal()前被中断的* * 例子:* 该条件为true的情形:* 假设仅有thread0,thread0持有锁后调用AQS.CO.await()被park,* 之后被外部线程中断,会执行到这里,此时条件为true* 该条件为false的情形:* 假设有两个线程thread0、thread1,thread0,thread0持有锁后调用AQS.CO.await()被park,* thread1获取到锁后调用AQS.CO.signal,之后会执行到AQS.transferForSignal的第一个if处,* 该if语句执行完后,这里node.waitStatus被修改为0,所以thread0执行这里的if语句时会失败*/if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {// 被中断后就需要重新进入同步队列抢占锁,从头再来enq(node);return true;}// 到这里说明其他线程调用了signal(),将当前线程对应的节点加入同步队列,这里自旋等待入队完成while (!isOnSyncQueue(node))// 主动让出当前线程的CPU时间片Thread.yield(); // 到这里就说明是在调用signal()之后被中断的,执行到这里的时候,这个线程节点已经进入到了同步队列中了,// 在后续的操作中就会执行acquireQueued(node, savedState)来不断尝试获取锁直到成功return false;
}// AbstractQueuedSynchronizer.ConditionObject.unlinkCancelledWaiters()
// 清除掉条件队列中所有被取消的节点;
private void unlinkCancelledWaiters() {// 获取条件队列的头节点Node t = firstWaiter;Node trail = null;/*** 1、从firstWaiter开始,将整个链表中t.waitStatus != Node.CONDITION的节点移除掉;* 2、节点移除后,将其前置节点的nextWaiter指向后置节点。*/while (t != null) {// 获取当前遍历到节点的下一个节点Node next = t.nextWaiter;if (t.waitStatus != Node.CONDITION) {/** 当前节点状态不是CONDITION:* 将当前节点的nextWaiter设置为null。* 如果trail是空,则将firstWaiter指向之前保存的t.nextWaiter,* 否则将trail.nextWaiter指向之前保存的t.nextWaiter。*/t.nextWaiter = null;if (trail == null)firstWaiter = next;elsetrail.nextWaiter = next;if (next == null)lastWaiter = trail;}elsetrail = t;t = next;}
}// AbstractQueuedSynchronizer.ConditionObject.reportInterruptAfterWait()
// 根据中断标识来进行不同的处理
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {// 如果当前线程在其他线程调用signal()之前被中断唤醒,则直接抛出异常响应中断if (interruptMode == THROW_IE)throw new InterruptedException();// 如果当前线程在其他线程调用signal()之后被中断唤醒,这种情况不会对条件锁流程造成影响,// 则设置中断标志,但不会抛出异常中止执行else if (interruptMode == REINTERRUPT)selfInterrupt();
}
这里有几个难理解的点:
-
Condition的队列和AQS的队列不完全一样;AQS的队列头节点是不存在任何值的,是一个虚节点;Condition的队列头节点是存储着实实在在的元素值的,是真实节点。
-
各种等待状态(
waitStatus)的变化;- 首先,在条件队列中,新建节点的初始等待状态是
CONDITION(-2); - 其次,移到
AQS的队列中时等待状态会更改为0(AQS队列节点的初始等待状态为0); - 然后,在
AQS的队列中如果需要阻塞,会把它上一个节点的等待状态设置为SIGNAL(-1); - 最后,不管在
Condition队列还是AQS队列中,已取消的节点的等待状态都会设置为CANCELLED(1); - 另外,后面我们在共享锁的时候还会讲到另外一种等待状态叫
PROPAGATE(-3)。
- 首先,在条件队列中,新建节点的初始等待状态是
-
相似的名称;
AQS中下一个节点是next,上一个节点是prev;Condition中下一个节点是nextWaiter,没有上一个节点。
总结一下await()方法的大致流程:
- 新建一个节点加入到条件队列中去;
- 完全释放当前线程占有的锁;
- 阻塞当前线程,并等待条件的出现;
- 条件已出现(此时节点已经移到
AQS的队列中),尝试获取锁;
也就是说await()方法内部其实是先释放锁->等待条件->再次获取锁的过程。
3.5 condition.signal()方法
condition.signal()方法通知条件已经出现。
这个方法是由获取锁的其他线程执行的,用来唤醒正在阻塞等待满足条件的线程。
signal()方法会寻找条件队列中第一个可用节点移到AQS队列中;
// AbstractQueuedSynchronizer.ConditionObject.signal
public final void signal() {// 如果不是当前线程占有着锁,调用这个方法抛出异常// 说明signal()也要在获取锁之后执行if (!isHeldExclusively())throw new IllegalMonitorStateException();// 条件队列的头节点Node first = firstWaiter;// 如果有等待条件的节点,则通知它条件已成立if (first != null)doSignal(first);
}// AbstractQueuedSynchronizer.ConditionObject.doSignal
private void doSignal(Node first) {// 从头节点开始遍历条件队列,仅唤醒第一个符合条件的线程do {// 将记录条件队列头节点的指向向后移动一位if ( (firstWaiter = first.nextWaiter) == null)// 如果移动后firstWaiter为null,说明已到队列尾部,将lastWaiter设置为nulllastWaiter = null;// 将first与其后继节点断开,相当于把头节点从队列中出队first.nextWaiter = null;// 转移节点到AQS队列中// 条件1:只要一个线程转移到AQS同步队列成功,transferForSignal返回true,就会终止该循环// 条件2:用于判断是否遍历到了队列尾部} while (!transferForSignal(first) &&(first = firstWaiter) != null);
}// AbstractQueuedSynchronizer.transferForSignal
// 将节点从条件队列移动到同步队列,返回移动是否成功
final boolean transferForSignal(Node node) {// 把节点的状态更改为0,也就是说即将移到AQS队列中// 如果失败了,说明节点已经被改成取消状态了// 返回false,通过上面的循环可知会寻找下一个可用节点if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;// 调用AQS的入队方法把节点移到AQS的队列中// 注意,这里enq()的返回值是node的上一个节点,也就是旧尾节点Node p = enq(node);// 上一个节点的等待状态int ws = p.waitStatus;// 如果上一个节点已取消了,或者更新状态为SIGNAL失败(也是说明上一个节点已经取消了)// 则直接唤醒当前节点对应的线程if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))LockSupport.unpark(node.thread);// 如果更新上一个节点的等待状态为SIGNAL成功了// 则返回true,这时上面的循环不成立了,退出循环,也就是只通知了一个节点// 此时当前节点还是阻塞状态// 也就是说调用signal()的时候并不会真正唤醒一个节点// 只是把节点从条件队列移到AQS队列中return true;
}
signal()方法的大致流程为:
- 从条件队列的头节点开始寻找一个非取消状态的节点;
- 把它从条件队列移到
AQS队列; - 且只移动一个节点;
注意,这里调用signal()方法后并不会真正唤醒一个节点,那么,唤醒一个节点是在啥时候呢?
我们可以再回去看一下本文最开始的使用案例,在其他线程调用signal()方法后,该线程最终会执行lock.unlock()方法,此时才会真正唤醒一个正在同步队列中的节点,唤醒的这个节点如果曾经是条件节点被转移到条件队列中的话,就会继续执行await()方法“分界线”下面的代码。也就是说,在调用signal()方法的线程调用unlock()方法才是真正唤醒阻塞在条件上的节点(此时节点已经在AQS队列中);被唤醒之后,被唤醒的节点会再次尝试获取锁,后面的逻辑与lock()的逻辑基本一致了。
3.6 condition.signalAll()方法
signalAll与signal类似,区别是signalAll会将所有节点加入同步队列,除了doSignalAll方法以外,其他的方法都是一样的:
private void doSignalAll(Node first) {// 将条件队列的头节点和尾节点都置为nulllastWaiter = firstWaiter = null;// 遍历条件队列,依次唤醒所有线程do {Node next = first.nextWaiter;first.nextWaiter = null;transferForSignal(first);first = next;} while (first != null);
}
四、总结
- 条件锁是指为了等待某个条件出现而使用的一种锁;
- 条件锁比较经典的使用场景就是队列为空时阻塞在条件notEmpty上;
- ReentrantLock中的条件锁是通过AQS的ConditionObject内部类实现的;
- await()和signal()方法都必须在获取锁之后释放锁之前使用;
- await()方法会新建一个节点放到条件队列中,接着完全释放锁,然后阻塞当前线程并等待条件的出现;
- signal()方法会寻找条件队列中第一个可用节点移到AQS队列中;
- 在调用signal()方法的线程调用unlock()方法才真正唤醒阻塞在条件上的节点(此时节点已经在AQS队列中);
- 之后该节点会再次尝试获取锁,后面的逻辑与lock()的逻辑基本一致了。
参考文章
- 并发编程】Condition条件锁源码详解
相关文章:
科普文:JUC系列之多线程门闩同步器Condition的使用和源码解读
一、概述 条件锁就是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。 比如,在阻塞队列中,当队列中没有元素的时候是无法弹出一个元素的,这时候就需要阻塞在条件notEmpty上…...
Stable Diffusion绘画 | 图生图-基础使用介绍—提示词反推
按默认设置直接出图 拖入图片值图生图框中,保持默认设置,直接生成图片,出图效果如下: 因为重绘幅度0.7,所出图片与原图有差异,但整体的框架构图与颜色与原图类似。 输入关键词后出图 在正向提示词中输入…...
正点原子imx6ull-mini-Linux驱动之Linux SPI 驱动实验(22)
跟上一章一样,其实这些设备驱动,无非就是传感器对应寄存器的读写。而这个读写是建立在各种通信协议上的,比如上一章的i2c,我们做了什么呢,就是把设备注册成一个i2c平台驱动,这个i2c驱动怎么搞的呢ÿ…...
TypeScript 函数
函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为 的地方。 TypeScript为JavaScript函数添加了额外的功能&…...
C++ : namespace,输入与输出,函数重载,缺省参数
一,命名空间(namespace) 1.1命名空间的作用与定义 我们在学习c的过程中,经常会碰到命名冲突的情况。就拿我们在c语言中的一个string函数来说吧: int strncat 0; int main() {printf("%d", strncat);return 0; } 当我们运行之后&…...
目标检测 | yolov1 原理和介绍
1. 简介 论文链接:https://arxiv.org/abs/1506.02640 时间:2015年 作者:Joseph Redmon 代码参考:https://github.com/abeardear/pytorch-YOLO-v1 yolo属于one-stage算法,仅仅使用一个CNN网络直接预测不同目标的类别与…...
excel中有些以文本格式存储的数值如何批量转换为数字
一、背景 1.1 文本格式存储的数值特点 在平时工作中有时候会从别地方导出来表格,表格中有些数值是以文本格式存储的(特点:单元格的左上角有个绿色的小标)。 1.2 文本格式存储的数值在排序时不符合预期 当我们需要进行排序的时候…...
原神升级计划数据表:4个倒计时可以修改提示信息和时间,可以点击等级、命座、天赋、备注进行修改。
<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><title>原神倒计时</title><style>* {margin: 0;padding: 0;box-sizing: border-box;body {background: #0b1b2c;}}header {width: 100vw;heigh…...
YoloV10 论文翻译(Real-Time End-to-End Object Detection)
摘要 近年来,YOLO因其在计算成本与检测性能之间实现了有效平衡,已成为实时目标检测领域的主流范式。研究人员对YOLO的架构设计、优化目标、数据增强策略等方面进行了探索,并取得了显著进展。然而,YOLO对非极大值抑制࿰…...
第R1周:RNN-心脏病预测
本文为🔗365天深度学习训练营 中的学习记录博客 原作者:K同学啊 要求: 1.本地读取并加载数据。 2.了解循环神经网络(RNN)的构建过程 3.测试集accuracy到达87% 拔高: 1.测试集accuracy到达89% 我的环境&a…...
Golang | Leetcode Golang题解之第321题拼接最大数
题目: 题解: func maxSubsequence(a []int, k int) (s []int) {for i, v : range a {for len(s) > 0 && len(s)len(a)-1-i > k && v > s[len(s)-1] {s s[:len(s)-1]}if len(s) < k {s append(s, v)}}return }func lexico…...
远程连接本地虚拟机失败问题汇总
前言 因为我的 Ubuntu 虚拟机是新装的,并且应该装的是比较纯净的版本(纯净是指很多工具都尚未安装),然后在使用远程连接工具 XShell 连接时出现了很多问题,这些都是我之前没遇到过的(因为之前主要使用云服…...
WebRTC 初探
前言 项目中有局域网投屏与文件传输的需求,所以研究了一下 webRTC,这里记录一下学习过程。 WebRTC 基本流程以及概念 下面以 1 对 1 音视频实时通话案例介绍 WebRTC 的基本流程以及概念 WebRTC 中的角色 WebRTC 终端,负责音视频采集、编解码、NAT 穿…...
Python:read,readline和readlines的区别
在Python中,read(), readline(), 和 readlines() 是文件操作中常用的三个方法,它们都用于从文件中读取数据,但各自的使用方式和适用场景有所不同。 read() 方法: read(size-1) 方法用于从文件中读取指定数量的字符。如果指定了si…...
重生之我学编程
编程小白如何成为大神?大学新生的最佳入门攻略 编程已成为当代大学生的必备技能,但面对众多编程语言和学习资源,新生们常常感到迷茫。如何选择适合自己的编程语言?如何制定有效的学习计划?如何避免常见的学习陷阱&…...
如何将PostgreSQL的数据实时迁移到SelectDB?
PostgreSQL 作为一个开源且功能强大的关系型数据库管理系统,在 OLTP 系统中得到了广泛应用。很多企业利用其卓越的性能和灵活的架构,应对高并发事务、快速响应等需求。 然而对于 OLAP 场景,PostgreSQL 可能并不是最佳选择。 为了实现庞大规…...
关于c语言的const 指针
const * type A 指向的数据是常量 如上所示,运行结果如下,通过解引用的方式,改变了data的值 const type * A 位置是常量,不能修改 运行结果如下 type const * A 指针是个常量,指向的值可以改变 如上所示,…...
万能门店小程序开发平台功能源码系统 带完整的安装代码包以及安装搭建教程
互联网技术的迅猛发展和用户对于便捷性需求的不断提高,小程序以其轻量、快捷、无需安装的特点,成为了众多商家和开发者关注的焦点。为满足广大商家对于门店线上化、智能化管理的需求,小编给大家分享一款“万能门店小程序开发平台功能源码系统…...
C#初级——字典Dictionary
字典 字典是C#中的一种集合,它存储键值对,并且每个键与一个值相关联。 创建字典 Dictionary<键的类型, 值的类型> 字典名字 new Dictionary<键的类型, 值的类型>(); Dictionary<int, string> dicStudent new Dictionary<int, str…...
git版本控制的底层实现
目录 前言 核心概念串讲 底层存储形式探测 本地仓库的详细解析 提交与分支的深入解析 几个问题的深入探讨 前言 Git的重要性 Git是一个开源的版本控制工具,广泛用于编程开发领域。它极大地提高了研发团队的开发协作效率。对于开发者来说,Git是一个…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
如何为服务器生成TLS证书
TLS(Transport Layer Security)证书是确保网络通信安全的重要手段,它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书,可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...
令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...
Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...
