科普文: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是一个…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...
深入浅出Diffusion模型:从原理到实践的全方位教程
I. 引言:生成式AI的黎明 – Diffusion模型是什么? 近年来,生成式人工智能(Generative AI)领域取得了爆炸性的进展,模型能够根据简单的文本提示创作出逼真的图像、连贯的文本,乃至更多令人惊叹的…...

算术操作符与类型转换:从基础到精通
目录 前言:从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符:、-、*、/、% 赋值操作符:和复合赋值 单⽬操作符:、--、、- 前言:从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...

Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程
鸿蒙电脑版操作系统来了,很多小伙伴想体验鸿蒙电脑版操作系统,可惜,鸿蒙系统并不支持你正在使用的传统的电脑来安装。不过可以通过可以使用华为官方提供的虚拟机,来体验大家心心念念的鸿蒙系统啦!注意:虚拟…...