当前位置: 首页 > news >正文

科普文:JUC系列之多线程门闩同步器Condition的使用和源码解读

一、概述

条件锁就是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。

比如,在阻塞队列中,当队列中没有元素的时候是无法弹出一个元素的,这时候就需要阻塞在条件notEmpty上,等待其它线程往里面放入一个元素后,唤醒这个条件notEmpty,当前线程才可以继续去做“弹出一个元素”的行为。

注意,这里的条件,必须是在获取锁之后去等待,对应到ReentrantLock的条件锁,就是获取锁之后才能调用condition.await()方法。

Java中,条件锁的实现都在AQSConditionObject类中,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的时候出现的,是用来替代Objectwaitnotify。所以显而易见,Conditionawaitsignal肯定效率更高、安全性更好。Condition是依赖于lock实现的。并且awaitsignal只能在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 内部类

ConditionObjectCondition的实现类。

//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();
}

这里有几个难理解的点:

  1. Condition的队列和AQS的队列不完全一样;

    • AQS的队列头节点是不存在任何值的,是一个虚节点;
    • Condition的队列头节点是存储着实实在在的元素值的,是真实节点。
  2. 各种等待状态(waitStatus)的变化;

    • 首先,在条件队列中,新建节点的初始等待状态是CONDITION(-2)
    • 其次,移到AQS的队列中时等待状态会更改为0(AQS队列节点的初始等待状态为0);
    • 然后,在AQS的队列中如果需要阻塞,会把它上一个节点的等待状态设置为SIGNAL(-1)
    • 最后,不管在Condition队列还是AQS队列中,已取消的节点的等待状态都会设置为CANCELLED(1)
    • 另外,后面我们在共享锁的时候还会讲到另外一种等待状态叫PROPAGATE(-3)
  3. 相似的名称;

    • AQS中下一个节点是next,上一个节点是prev
    • Condition中下一个节点是nextWaiter,没有上一个节点。

总结一下await()方法的大致流程:

  1. 新建一个节点加入到条件队列中去;
  2. 完全释放当前线程占有的锁;
  3. 阻塞当前线程,并等待条件的出现;
  4. 条件已出现(此时节点已经移到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()方法的大致流程为:

  1. 从条件队列的头节点开始寻找一个非取消状态的节点;
  2. 把它从条件队列移到AQS队列;
  3. 且只移动一个节点;

注意,这里调用signal()方法后并不会真正唤醒一个节点,那么,唤醒一个节点是在啥时候呢?

我们可以再回去看一下本文最开始的使用案例,在其他线程调用signal()方法后,该线程最终会执行lock.unlock()方法,此时才会真正唤醒一个正在同步队列中的节点,唤醒的这个节点如果曾经是条件节点被转移到条件队列中的话,就会继续执行await()方法“分界线”下面的代码。也就是说,在调用signal()方法的线程调用unlock()方法才是真正唤醒阻塞在条件上的节点(此时节点已经在AQS队列中);被唤醒之后,被唤醒的节点会再次尝试获取锁,后面的逻辑与lock()的逻辑基本一致了。

3.6 condition.signalAll()方法

signalAllsignal类似,区别是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);
}

四、总结

  1. 条件锁是指为了等待某个条件出现而使用的一种锁;
  2. 条件锁比较经典的使用场景就是队列为空时阻塞在条件notEmpty上;
  3. ReentrantLock中的条件锁是通过AQS的ConditionObject内部类实现的;
  4. await()和signal()方法都必须在获取锁之后释放锁之前使用;
  5. await()方法会新建一个节点放到条件队列中,接着完全释放锁,然后阻塞当前线程并等待条件的出现;
  6. signal()方法会寻找条件队列中第一个可用节点移到AQS队列中;
  7. 在调用signal()方法的线程调用unlock()方法才真正唤醒阻塞在条件上的节点(此时节点已经在AQS队列中);
  8. 之后该节点会再次尝试获取锁,后面的逻辑与lock()的逻辑基本一致了。

参考文章

  • 并发编程】Condition条件锁源码详解

相关文章:

科普文:JUC系列之多线程门闩同步器Condition的使用和源码解读

一、概述 条件锁就是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。 比如,在阻塞队列中,当队列中没有元素的时候是无法弹出一个元素的,这时候就需要阻塞在条件notEmpty上…...

Stable Diffusion绘画 | 图生图-基础使用介绍—提示词反推

按默认设置直接出图 拖入图片值图生图框中,保持默认设置,直接生成图片,出图效果如下: 因为重绘幅度0.7,所出图片与原图有差异,但整体的框架构图与颜色与原图类似。 输入关键词后出图 在正向提示词中输入…...

正点原子imx6ull-mini-Linux驱动之Linux SPI 驱动实验(22)

跟上一章一样,其实这些设备驱动,无非就是传感器对应寄存器的读写。而这个读写是建立在各种通信协议上的,比如上一章的i2c,我们做了什么呢,就是把设备注册成一个i2c平台驱动,这个i2c驱动怎么搞的呢&#xff…...

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)

​摘要 近年来&#xff0c;YOLO因其在计算成本与检测性能之间实现了有效平衡&#xff0c;已成为实时目标检测领域的主流范式。研究人员对YOLO的架构设计、优化目标、数据增强策略等方面进行了探索&#xff0c;并取得了显著进展。然而&#xff0c;YOLO对非极大值抑制&#xff0…...

第R1周:RNN-心脏病预测

本文为&#x1f517;365天深度学习训练营 中的学习记录博客 原作者&#xff1a;K同学啊 要求&#xff1a; 1.本地读取并加载数据。 2.了解循环神经网络&#xff08;RNN&#xff09;的构建过程 3.测试集accuracy到达87% 拔高&#xff1a; 1.测试集accuracy到达89% 我的环境&a…...

Golang | Leetcode Golang题解之第321题拼接最大数

题目&#xff1a; 题解&#xff1a; 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 虚拟机是新装的&#xff0c;并且应该装的是比较纯净的版本&#xff08;纯净是指很多工具都尚未安装&#xff09;&#xff0c;然后在使用远程连接工具 XShell 连接时出现了很多问题&#xff0c;这些都是我之前没遇到过的&#xff08;因为之前主要使用云服…...

WebRTC 初探

前言 项目中有局域网投屏与文件传输的需求&#xff0c;所以研究了一下 webRTC&#xff0c;这里记录一下学习过程。 WebRTC 基本流程以及概念 下面以 1 对 1 音视频实时通话案例介绍 WebRTC 的基本流程以及概念 WebRTC 中的角色 WebRTC 终端,负责音视频采集、编解码、NAT 穿…...

Python:read,readline和readlines的区别

在Python中&#xff0c;read(), readline(), 和 readlines() 是文件操作中常用的三个方法&#xff0c;它们都用于从文件中读取数据&#xff0c;但各自的使用方式和适用场景有所不同。 read() 方法&#xff1a; read(size-1) 方法用于从文件中读取指定数量的字符。如果指定了si…...

重生之我学编程

编程小白如何成为大神&#xff1f;大学新生的最佳入门攻略 编程已成为当代大学生的必备技能&#xff0c;但面对众多编程语言和学习资源&#xff0c;新生们常常感到迷茫。如何选择适合自己的编程语言&#xff1f;如何制定有效的学习计划&#xff1f;如何避免常见的学习陷阱&…...

如何将PostgreSQL的数据实时迁移到SelectDB?

PostgreSQL 作为一个开源且功能强大的关系型数据库管理系统&#xff0c;在 OLTP 系统中得到了广泛应用。很多企业利用其卓越的性能和灵活的架构&#xff0c;应对高并发事务、快速响应等需求。 然而对于 OLAP 场景&#xff0c;PostgreSQL 可能并不是最佳选择。 为了实现庞大规…...

关于c语言的const 指针

const * type A 指向的数据是常量 如上所示&#xff0c;运行结果如下&#xff0c;通过解引用的方式&#xff0c;改变了data的值 const type * A 位置是常量&#xff0c;不能修改 运行结果如下 type const * A 指针是个常量&#xff0c;指向的值可以改变 如上所示&#xff0c…...

万能门店小程序开发平台功能源码系统 带完整的安装代码包以及安装搭建教程

互联网技术的迅猛发展和用户对于便捷性需求的不断提高&#xff0c;小程序以其轻量、快捷、无需安装的特点&#xff0c;成为了众多商家和开发者关注的焦点。为满足广大商家对于门店线上化、智能化管理的需求&#xff0c;小编给大家分享一款“万能门店小程序开发平台功能源码系统…...

C#初级——字典Dictionary

字典 字典是C#中的一种集合&#xff0c;它存储键值对&#xff0c;并且每个键与一个值相关联。 创建字典 Dictionary<键的类型, 值的类型> 字典名字 new Dictionary<键的类型, 值的类型>(); Dictionary<int, string> dicStudent new Dictionary<int, str…...

git版本控制的底层实现

目录 前言 核心概念串讲 底层存储形式探测 本地仓库的详细解析 提交与分支的深入解析 几个问题的深入探讨 前言 Git的重要性 Git是一个开源的版本控制工具&#xff0c;广泛用于编程开发领域。它极大地提高了研发团队的开发协作效率。对于开发者来说&#xff0c;Git是一个…...

大数据学习栈记——Neo4j的安装与使用

本文介绍图数据库Neofj的安装与使用&#xff0c;操作系统&#xff1a;Ubuntu24.04&#xff0c;Neofj版本&#xff1a;2025.04.0。 Apt安装 Neofj可以进行官网安装&#xff1a;Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

边缘计算医疗风险自查APP开发方案

核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

【入坑系列】TiDB 强制索引在不同库下不生效问题

文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法

深入浅出&#xff1a;JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中&#xff0c;随机数的生成看似简单&#xff0c;却隐藏着许多玄机。无论是生成密码、加密密钥&#xff0c;还是创建安全令牌&#xff0c;随机数的质量直接关系到系统的安全性。Jav…...

《Playwright:微软的自动化测试工具详解》

Playwright 简介:声明内容来自网络&#xff0c;将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具&#xff0c;支持 Chrome、Firefox、Safari 等主流浏览器&#xff0c;提供多语言 API&#xff08;Python、JavaScript、Java、.NET&#xff09;。它的特点包括&a…...

pam_env.so模块配置解析

在PAM&#xff08;Pluggable Authentication Modules&#xff09;配置中&#xff0c; /etc/pam.d/su 文件相关配置含义如下&#xff1a; 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块&#xff0c;负责验证用户身份&am…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)

🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

dify打造数据可视化图表

一、概述 在日常工作和学习中&#xff0c;我们经常需要和数据打交道。无论是分析报告、项目展示&#xff0c;还是简单的数据洞察&#xff0c;一个清晰直观的图表&#xff0c;往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server&#xff0c;由蚂蚁集团 AntV 团队…...

uniapp 集成腾讯云 IM 富媒体消息(地理位置/文件)

UniApp 集成腾讯云 IM 富媒体消息全攻略&#xff08;地理位置/文件&#xff09; 一、功能实现原理 腾讯云 IM 通过 消息扩展机制 支持富媒体类型&#xff0c;核心实现方式&#xff1a; 标准消息类型&#xff1a;直接使用 SDK 内置类型&#xff08;文件、图片等&#xff09;自…...