线程安全--深入探究线程等待机制和死锁问题

 ꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱
ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客
本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如需转载还请通知˶⍤⃝˶​
个人主页:xiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客

系列专栏:xiaoxie的JAVAEE学习系列专栏——CSDN博客●'ᴗ'σσணღ
我的目标:"团团等我💪( ◡̀_◡́ ҂)" 

( ⸝⸝⸝›ᴥ‹⸝⸝⸝ )欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​+关注(互三必回)! 

 一.线程等待机制

1.什么是线程等待机制

线程等待机制是多线程编程中用于同步线程执行流程的一种技术,它允许一个线程暂停执行(即进入等待状态),直到满足特定条件或其他线程发送一个通知信号为止。在Java以及许多其他支持多线程的语言中,这种机制通常通过以下方式实现:

 1.wait() 方法

wait() 是 java.lang.Object 类的一个方法,当在一个对象上调用 wait() 时,当前线程必须首先获得该对象的监视器(即锁)。调用后,线程会释放对象的锁,并进入等待状态,直到被其他线程通过调用 notify() 或 notifyAll() 方法唤醒。

 2.notify() 和 notifyAll() 方法

notify() 唤醒在此对象监视器上等待的一个单个线程。notifyAll() 唤醒在此对象监视器上等待的所有线程。

2.wait() 方法和join()方法和sleep()方法的区别

我们都知道wait() 方法和join()方法和sleep()方法都是控制线程行为的方法那么它们之间有什么区别吗?

  1. wait() 方法

    • 属于 java.lang.Object 类的方法,必须在 synchronized 代码块或方法中调用,因为它依赖于对象的监视器(锁)。
    • 当调用 wait() 时,当前线程会释放所持有的对象监视器(锁),并进入等待状态,直到其他线程调用同一对象上的 notify() 或 notifyAll() 方法将其唤醒。
    • wait() 方法主要用于线程间同步与通信,它使得线程能够有条件地等待资源就绪。
  2. join() 方法

    • 属于 java.lang.Thread 类的实例方法,用于让当前线程等待调用该方法的目标线程终止。
    • 当调用 t.join() 时,当前线程将阻塞,直到线程 t 完成它的任务。
    • join() 有助于实现线程间的顺序执行,比如主线程等待子线程完成后再继续执行。
  3. sleep() 方法

    • 也是 java.lang.Thread 类的静态方法,但它并不涉及线程间的交互和同步。
    • sleep(long millis) 会让当前线程暂时停止执行一段时间,这段时间内线程不会消耗CPU资源,但仍保持“活着”的状态。
    • sleep() 方法调用期间,线程不会释放已经获取的任何锁资源。
    • 主要用于模拟延迟或防止繁忙循环消耗过多CPU资源。

总结起来:

1.wait() 是一种协调机制,用于线程间通信和同步,会释放锁并阻塞线程直到收到通知。

 2.join() 用于线程顺序控制,让一个线程等待另一个线程结束,同样会阻塞当前线程,但不涉及锁的管理。

 3.sleep() 是简单的线程暂停机制,仅影响单个线程的行为,用于暂停一定时间,不涉及线程间的通信和锁的状

3.线程等待机制的代码案例

题目要求:有三个线程,分别只能打印A,B和C要求按顺序打印ABC,打印10次

// 创建一个演示类Demo1
public class Demo1 {// 定义一个共享静态变量count,用于记录循环次数public static int count;// 程序主入口public static void main(String[] args) throws InterruptedException {// 创建一个共享的对象locker作为线程间的同步锁Object locker = new Object();// 创建线程t1,循环10次,每次检查count是否能被3整除,不能则等待,能则输出当前线程名、增加count并唤醒所有等待线程Thread t1 = new Thread(() -> {for (int i = 0; i < 10; i++) {synchronized (locker) { // 对locker对象进行同步while (count % 3 != 0) { // 如果count不是3的倍数try {locker.wait(); // 当前线程等待,释放locker锁} catch (InterruptedException e) {throw new RuntimeException(e); // 处理中断异常}}System.out.print(Thread.currentThread().getName()); // 输出当前线程名count++; // 增加count值locker.notifyAll(); // 唤醒所有等待locker锁的线程}}}, "A");// 创建线程t2,逻辑与t1类似,但检查count是否为3的倍数加1Thread t2 = new Thread(() -> {for (int i = 0; i < 10; i++) {synchronized (locker) {while (count % 3 != 1) {try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.print(Thread.currentThread().getName());count++;locker.notifyAll();}}}, "B");// 创建线程t3,逻辑与t1类似,但检查count是否为3的倍数加2Thread t3 = new Thread(() -> {for (int i = 0; i < 10; i++) {synchronized (locker) {while (count % 3 != 2) {try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()); // 输出当前线程名并换行count++;locker.notifyAll();}}}, "C");// 启动三个线程t1.start();t2.start();t3.start();// 主线程休眠1秒,以便给其他线程运行机会Thread.sleep(1000);}
}

 输出结果:

  

在上述代码中,线程 t1t2 和 t3 分别会在满足各自条件时输出相应内容并更新 count 变量。具体过程如下:

  1. 当 t1 获取到 locker 锁后,会检查 count 是否为3的倍数。如果不是,则释放锁并调用 wait() 进入等待状态。此时,t1 不再占用锁,t2 和 t3 就有机会竞争锁。

  2. 若 t2 或 t3 其中之一成功获取锁,并满足自己的条件(即 count 为3的倍数加1或2),则会输出相应的线程名并增加 count 的值,然后调用 notifyAll() 唤醒所有等待 locker 锁的线程。

  3. 被唤醒的线程会重新开始尝试获取锁,并再次进入 while 循环检查条件。即使由于“虚假唤醒”被唤醒,由于采用了 while 循环而非 if,线程也会在未满足条件时继续等待,从而确保了正确的执行逻辑。

  4. 如此反复,直到 count 达到10*3=30,所有线程完成各自的循环,整个程序结束。在此过程中,线程间通过 wait() 和 notifyAll() 实现了同步与协作,确保了线程按序交替执行,并共同维护了对 count 变量的正确更新(count++操作是在锁内部完成的,不会出现内存可见性问题)。

注意:这里还有一个小知识点就是这里的条件判断为什么要用 while 而不是用 if?

1.多线程环境下,即使满足某个条件后调用了 wait() 方法,线程被唤醒时并不能保证条件依然成立。这是因为 notify() 或 notifyAll() 方法只会唤醒一个或所有等待的线程,但并不会立即恢复它们的执行,而是需要重新竞争锁资源。如果在当前线程被唤醒并重新获取锁之前,有其他线程改变了共享资源(如 count 变量),那么之前满足的条件可能不再满足。

2.为了确保线程安全,在进入临界区后使用 while 循环不断检查条件是一种最佳实践,这被称为“循环等待-通知”模式。只有当条件确实满足时,线程才会退出循环并执行后续操作,否则将继续等待,直到其他线程改变条件并再次唤醒它。这样可以防止出现“虚假唤醒”的问题,提高程序的健壮性。

3.当线程调用 wait() 方法时,它会释放锁并进入等待状态,直到被其他线程唤醒或者等待超时。然而,操作系统可能会出于效率或者其他因素(例如定时器精度、系统调度策略等)无预期地唤醒等待中的线程。尽管这种情况在实践中并不常见,但它是合法的,标准并未禁止此类行为。

为了避免虚假唤醒导致的错误逻辑执行,编程时推荐采用循环检查条件的方式来调用 wait() 方法,而不是简单地使用 if 判断之后立即调用 wait()

4.为什么要使用wait()方法和notify()方法

使用 wait() 和 notify() 方法的必要性和应用场景主要包括以下几点:

  1. 必要性

    • 线程同步与协作:在多线程环境中,当多个线程共享资源并且需要按照某种特定顺序或条件进行操作时,wait() 和 notify() 方法是必要的。例如,在生产者-消费者问题中,生产者线程需要在缓冲区满时等待,消费者线程在缓冲区空时等待,两者都需要对方操作后才能继续执行,这就需要用到 wait() 和 notify() 来进行线程间的有效沟通。
    • 避免资源竞争与死锁:通过适时地释放锁并进入等待状态,线程能够有效地避免资源的竞争,减少死锁发生的可能性。
    • 性能优化:相比不断地轮询检查条件是否满足(称为“忙等待”),wait() 方法可以让线程在等待条件变化时释放CPU,从而节省系统资源。
  2. 应用场景

    • 生产者-消费者模型:生产者线程负责生成数据放入共享容器,当容器满时,生产者调用 wait() 方法等待;消费者线程从容器中取出数据消费,当容器空时,消费者调用 wait() 方法等待。一旦数据产生或消耗,对应的线程会调用 notify() 方法唤醒等待的线程。
    • 资源池管理:在资源池达到最大限制时,请求新资源的线程会被阻塞,直到有线程归还资源后通过 notify() 触发等待线程继续执行。
    • 信号量控制:在复杂的并发场景中,可以通过 wait() 和 notify() 控制多个线程在多个资源之间按需分配和回收。

总之,wait() 和 notify() 方法是在多线程编程中实现高效、安全线程间通信的核心工具,它们解决了线程间的同步问题,使得不同线程可以根据预设条件有序地进行工作,极大地提高了程序设计的灵活性和并发效率。不过需要注意的是,使用这两个方法时应当遵循特定的准则,如必须在同步代码块或同步方法中调用,同时还要警惕死锁和其他并发问题的发生。在现代Java编程中,java.util.concurrent 包提供的高级并发工具(如 SemaphoreBlockingQueue(阻塞队列)CountDownLatch 等)往往更加易于理解和使用,但了解底层的 wait() 和 notify() 工作原理仍然具有重要意义。

二.死锁(面试常考)

1.什么是死锁

死锁是指在多线程或多进程环境下,两个或多个进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们都无法向前推进(即完成各自的任务)。具体地说,每个进程都至少占有一个资源,并且正在等待另一个进程中占有的资源被释放,然而这个资源又正被等待它释放资源的进程所占有,形成了一个环形等待链,这样每个进程都在等待别的进程释放资源,从而陷入了一个永久的停滞状态。

2.MySql的死锁问题(也是面试常考)

这里既然提到了死锁问题,博主就顺便也简单说明一下MySql的死锁问题吧

MySQL数据库中的死锁(Deadlock)指的是两个或多个事务在执行过程中,由于对相同资源请求不同的锁定顺序,彼此互相等待对方释放锁定资源,从而形成的一种循环等待状态,导致事务无法正常执行下去。

MySQL中的死锁主要发生在并发事务处理过程中,当两个或多个事务尝试以不同的顺序锁定相同的资源时可能发生。例如:

  • 事务A锁定了记录R1,并试图锁定记录R2;
  • 与此同时,事务B已经锁定了记录R2,并尝试锁定记录R1;
  • 由于事务A持有R1的锁,所以事务B无法获得R1的锁,而事务B持有R2的锁,事务A也无法获得R2的锁;
  • 因此,两个事务都进入了等待对方释放资源的状态,形成了死锁。

MySQL处理死锁的方式包括:

  1. 自动检测与解决: MySQL的InnoDB存储引擎具有死锁检测机制,定期检查是否存在死锁,并在检测到死锁时自动回滚其中一个事务,使其他事务得以继续执行。

  2. 预防死锁

    • 设计应用程序时确保事务尽可能短小,减少事务锁定资源的时间。
    • 确保事务对资源的锁定顺序一致,例如按照相同的索引顺序访问表。
    • 适当设置事务的隔离级别,虽然较低的隔离级别(如读已提交)可以减少死锁的可能性,但也可能导致更多的并发问题(如不可重复读或幻读)。
  3. 手动解决

    • 当遇到死锁时,DBA可以手动分析并决定是否应该杀死其中一个或多个事务,以打破死锁循环。
  4. 应用层处理

    • 在应用程序设计层面,可以通过添加重试机制来应对可能出现的死锁,当事务因死锁被回滚时,应用层可以捕获异常并重新发起事务。
  5. 配置调整

    • 调整数据库相关的参数,如增大innodb_lock_wait_timeout,当等待锁的时间超过设定阈值时,事务将自动回滚,从而避免无限期等待。

综上所述,MySQL中的死锁问题需要结合数据库服务器的内部机制和应用程序的设计来进行综合考虑和处理。

当然,MySQL的死锁问题有很多细节值得深入探讨和扩展,下面是一些额外的点:

1.死锁的检测与处理

  1. 死锁检测算法:InnoDB存储引擎使用了一种基于等待图的死锁检测算法。每当事务请求新的锁时,都会检查是否存在循环等待的情况。如果有,InnoDB会选择牺牲(rollback)一个事务以打破死锁。

  2. innodb_deadlock_detect 参数:MySQL可以通过配置 innodb_deadlock_detect 参数来控制是否启用死锁检测功能。默认情况下,此参数为ON,表示InnoDB会积极检测死锁并在发现死锁时立即回滚一个事务。但是,关闭此选项意味着系统在等待锁超时后才有可能检测到死锁。

  3. innodb_lock_wait_timeout:这是一个影响死锁处理的重要参数,它指定了一个事务在等待锁时可以等待的最大时间(单位秒)。超时后,事务将被回滚并抛出一个错误,客户端可以根据错误代码识别死锁并选择合适的重试策略。

2.死锁的预防策略

  • 严格的事务顺序:在编写事务时,尽量保持固定的资源获取顺序,避免交叉锁定资源引起死锁。

  • 最小化锁定范围:只锁定那些真正需要修改的数据,避免不必要的大范围锁定。

  • 合理设置事务隔离级别:尽管较低的隔离级别减少了死锁的风险,但可能引入其他并发问题。根据实际需求选择合适的事务隔离级别,兼顾并发性能和一致性需求。

  • 及时提交或回滚事务:不要让事务长时间持有锁,特别是在循环中逐条处理大量记录时,应及时提交或回滚事务,释放资源。

  • 使用乐观锁或版本控制:在某些场景下,可以使用乐观锁(如CAS操作)或数据库的行版本控制机制(MVCC),这类机制在一定程度上降低了死锁发生的概率。

3.死锁监控与排查

  • SHOW ENGINE INNODB STATUS:可以查看InnoDB引擎的状态,其中包含有关最近发生的死锁的信息。

  • performance_schema:MySQL的性能模式包含了丰富的监控信息,可以帮助开发者跟踪和诊断死锁的具体情况。

  • 日志记录:通过对MySQL错误日志的监控,可以捕获到死锁相关的错误信息,帮助定位问题。

 3.Java中的死锁问题(面试常考)

1.死锁发生的必要条件(缺一不可)

  1. 互斥条件(锁的特性):至少有一个资源是不可共享的,也就是说,一段时间内仅允许一个进程使用。
  2. 持有并等待条件:已经获得了至少一个资源的进程还在等待获取其他资源,而不会释放已持有的资源。
  3. 非抢占条件(锁的特性):资源一旦被分配给一个进程,就不能被强制性地从该进程中收回,只能由进程自身主动释放。
  4. 循环等待条件:存在一个进程-资源的闭环链,每个进程都在等待下一个进程所占用的资源,循环阻塞等待了。

2.出现死锁的几个经典场景(在多线程的环境下)

1.锁顺序不一致:线程A按照顺序先锁定资源A再锁定资源B,而线程B则是先锁定资源B再锁定资源A,当两个线程同时执行时,可能因资源获取顺序不同而导致死锁

Java代码

public class Demo2 {public static void main(String[] args) {Object lockerA = new Object();Object lockerB = new Object();Thread A = new Thread(() -> {synchronized (lockerA) {System.out.println("线程A获取锁A");try {Thread.sleep(100); // 模拟处理时间} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockerB) {System.out.println("线程A获取锁B");}}});Thread B = new Thread(() -> {synchronized (lockerB) {System.out.println("线程B获取锁B");try {Thread.sleep(100); // 模拟处理时间} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockerA) {System.out.println("线程B获取锁A");}}});A.start();B.start();}
}

2.资源分配不当:假设线程A持有了资源R1并请求资源R2,线程B持有了资源R2并请求资源R1,这时线程A和线程B都将阻塞,形成死锁。

Java代码:

public class Demo3 {// 定义两种资源对象static class Resource {private String name;public Resource(String name) {this.name = name;}@Overridepublic String toString() {return "Resource: " + name;}}public static void main (String[]args){// 创建资源R1和R2Resource r1 = new Resource("R1");Resource r2 = new Resource("R2");// 创建线程A和线程BThread threadA = new Thread(() -> {synchronized (r1) {System.out.println(Thread.currentThread().getName() + " 获取 " + r1);synchronized (r2) {System.out.println(Thread.currentThread().getName() + " 获取 " + r2);}}}, "线程A");Thread threadB = new Thread(() -> {synchronized (r2) {System.out.println(Thread.currentThread().getName() + " 获取 " + r2);synchronized (r1) {System.out.println(Thread.currentThread().getName() + " 获取 " + r1);}}}, "线程B");// 启动线程threadA.start();threadB.start();}
}

3.经典的哲学家就餐问题:五位哲学家围绕圆桌而坐,每位哲学家有一只手拿筷子,他们需要两只筷子才能就餐。当所有哲学家都拿起左边的筷子后,大家都在等待右边筷子,形成死锁。

 4.死锁的解决方法

  1. 预防死锁

    • 资源一次性分配:一次性为进程分配所需的全部资源,这样就可以避免循环等待条件。
    • 资源有序分配:规定所有的进程都按照统一的顺序申请资源,这样可以避免循环等待。
    • 破坏请求和保持条件:要求进程在请求新资源之前释放已有资源,或者在申请资源时不立即锁定资源,而是等到可以一次性获得所有所需资源时才进行锁定。
    • 破坏不可剥夺条件:允许资源抢占,即当一个进程请求资源无法得到满足时,系统可以剥夺已经分配给其他进程但尚未使用的资源。
  2. 避免死锁

    • 动态分配资源时使用银行家算法等策略,系统在分配资源前预先计算是否有足够的资源满足所有进程的安全序列,以防止系统进入不安全状态,从而避免死锁。
  3. 检测死锁

    • 在系统中设置周期性的死锁检测机制,通过算法(如Wait-For Graph算法)来发现死锁。
    • 当检测到死锁时,采取一定的策略进行解除,如选择一个进程取消(回滚或撤销),或者选择资源进行抢占。
  4. 解除死锁

    • 撤销进程:选择一个死锁进程进行回滚,释放其所占用的资源,使其余进程得以继续执行。
    • 资源剥夺:强制从死锁进程手中剥夺部分资源,分配给其他进程,打破循环等待。
  5. 资源超时回收

    • 设置资源请求的超时时间,当请求超出指定时间仍未能获取资源时,释放已持有资源,然后重新发起请求,这样可以避免长期等待和死锁。

每种方法都有其适用场景和局限性,实际应用时需要根据系统的具体情况和性能要求选择合适的方法。在设计并发和多线程程序时,良好的编程实践和精心设计的资源管理策略也是非常重要的,例如尽量减少资源的持有时间、确保资源释放的完整性以及避免不必要的资源竞争。

以上就是关于线程安全问题的所以内容了,感谢你的阅读!

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1688详情API接口:解锁多元化应用场景java php c++

随着互联网的快速发展&#xff0c;数据交换和信息共享已成为企业日常运营不可或缺的一部分。在这样的背景下&#xff0c;API&#xff08;应用程序接口&#xff09;接口作为实现数据互通的重要工具&#xff0c;受到了越来越多企业的青睐。1688详情API接口作为阿里巴巴旗下的重要…

壁纸小程序Vu3(预览页面:弹窗)

1.展示跳转后的分类列表图片 classlist.vue <template><view class"classlist"><view class"content"><navigator class"item" v-for"item in 10"><image src"../../common/images/64.png" mode…

计算机网络 实验指导 实验16

实验16 PPP配置实验 1.实验拓扑图 实验10讲了如何添加Se的接口 名称接口IP地址Router1se0/0/0192.168.1.1/24Router0se0/0/0192.168.1.2/24se0/0/1192.168.2.1/24Router2se0/3/0192.168.2.2/24 2.实验目的 &#xff08;1&#xff09;掌握PPP的基本配置步骤和方法 &#xf…

Java入门基础知识第六课(超基础,超详细)——循环结构

前面二白讲了选择结构相关知识&#xff0c;主要是if选择结构和swich选择结构&#xff0c;这次咱们讲一下循环结构&#xff0c;主要是while、do-while、for这三种循环结构 一、while循环结构 语法&#xff1a; 初始值代码; while(循环条件){ 循环操作代码块; 迭代代码; } 执行…

UE4 面试题整理

1、new与malloc的区别 new&#xff1a; new首先会去调用operator new函数&#xff0c;申请足够的内存&#xff08;大多数底层用malloc实现&#xff09;&#xff0c;然后调用类型的构造函数来初始化变量&#xff0c;最后返回自定义类型的指针&#xff0c;delete先调用析构函数&…

“AI复活”背后的数字永生:被期待成为下一个电商,培育市场认知和用户心智还需时间

“AI复活”背后的数字永生&#xff1a;被期待成为下一个电商&#xff0c;培育市场认知和用户心智还需时间© 由 九派新闻 提供 数字永生&#xff0c;还是电子宠物&#xff1f;过去一个月&#xff0c;因包小柏用AI技术让爱女在数字世界“复活”一事&#xff0c;《流浪地球2…

java 基本类型与包装类

8种基本类型与对应的包装类 尽量不要使用构造方法&#xff0c;因为构造方法已经过时了 自动装箱 自动装箱&#xff1a;基本自动转换为包装类 Integer a 3&#xff1b; 虽然所有的类中有类似于parseInt这种方法&#xff0c;但是所有类中都有valueOf方法&#xff0c;推荐使用va…

护眼落地灯到底是不是智商税?六种实用挑选方法帮你排雷避坑!

光线对眼睛的影响是不可忽视的。现代人由于长时间使用电子产品&#xff0c;如电脑、手机等&#xff0c;往往会处于高强度的光线照射下&#xff0c;导致眼睛疲劳、干涩、红肿等问题。而长期处于光线不足的环境中&#xff0c;又容易引发视力下降等眼部问题。因此&#xff0c;保护…

【机器学习入门】使用YOLO模型进行物体检测

系列文章目录 第1章 专家系统 第2章 决策树 第3章 神经元和感知机 识别手写数字——感知机 第4章 线性回归 第5章 逻辑斯蒂回归和分类 第5章 支持向量机 第6章 人工神经网络(一) 第6章 人工神经网络(二) 卷积和池化 第6章 使用pytorch进行手写数字识别 文章目录 系列文章目录前…

静态路由协议实验综合实验

需求&#xff1a; 1、除R5的换回地址已固定外&#xff0c;整个其他所有的网段基于192.168.1.0/24进行合理的IP地址划分。 2、R1-R4每台路由器存在两个环回接口&#xff0c;用于模拟连接PC的网段&#xff1b;地址也在192.168.1.0/24这个网络范围内。 3、R1-R4上不能直接编写到…

Android14应用启动流程(源码+Trace)

1.简介 应用启动过程快的都不需要一秒钟&#xff0c;但这整个过程的执行是比较复杂的&#xff0c;无论是对手机厂商、应用开发来说启动速度也是核心用户体验指标之一&#xff0c;本文采用Android14源码与perfetto工具进行解析。 源码参考地址&#xff1a;Search trace分析工…

Redis的配置文件详解

单位&#xff1a;Redis配置对大小写不敏感&#xff01; 注意这里&#xff1a;任何写法都可&#xff0c;不区分大小写。 units are case insensitive so 1GB 1Gb 1gB are all the same.包含&#xff1a;搭建Redis集群时&#xff0c;可以使用includes包含其他配置文件网络&…

在c# 7.3中不可用,请使用9.0或更高的语言版本

参考连接&#xff1a;在c# 7.3中不可用,请使用8.0或更高的语言版本_功能“可为 null 的引用类型”在 c# 7.3 中不可用。请使用 8.0 或更高的语言版本-CSDN博客https://blog.csdn.net/liangyely/article/details/106163660 [踩坑记录] 某功能在C#7.3中不可用,请使用 8.0 或更高的…

RGB三通道和灰度值的理解

本文都是来自于chatGPT的回答!!! 目录 Q1:像素具有什么属性?Q2:图像的色彩是怎么实现的?Q3:灰度值和颜色值是一个概念吗?Q4:是不是像素具有灰度值&#xff0c;也有三个颜色分量RGB&#xff1f;Q5:灰度图像是没有色彩的吗&#xff1f;Q6: 彩色图像是既具有灰度值也具有RGB三…

【JavaWeb】Day30.SpringBootWeb请求响应——响应

响应 HTTL协议的交互方式&#xff1a;请求响应模式&#xff08;有请求就有响应&#xff09;那么Controller程序&#xff0c;除了接收请求外&#xff0c;还可以进行响应。 1.ResponseBody 在我们前面所编写的controller方法中&#xff0c;都已经设置了响应数据。 controller方…

利用甘特图实现精细化项目管控

在项目管理中,通过精细化管控,项目经理能够有效规划、监督和协调各项任务,从而最大限度控制风险,优化资源配置,并确保按时、按质、按量完成项目目标。而在众多项目管理工具中,甘特图无疑是实现精细化项目管控的利器。zz-plan 是一个非常好用的在线甘特图制作工具&#xff0c;一…

【力扣】94. 二叉树的中序遍历、144. 二叉树的前序遍历、145. 二叉树的后序遍历

先序遍历&#xff1a;根-左-右中序遍历&#xff1a;左-根-右后序遍历&#xff1a;左-右-根 94. 二叉树的中序遍历 题目描述 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3…

【三十五】【算法分析与设计】综合练习(2),22。 括号生成,77。 组合,494。 目标和,模拟树递归,临时变量自动维护树定义,递归回溯,非树结构模拟树

22. 括号生成 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;["&#xff08;&#xff08;&#xff08;&#xff09;&#xff09;&#xff0…

C 回调函数的两种使用方法

对回调&#xff08;callback&#xff09;函数的一点粗陋理解&#xff0c;在我小时候&#xff0c;隔壁村有家月饼小作坊&#xff08;只在中秋那段时间手工制作一些月饼出售&#xff0c;后来好像不做了&#xff09;&#xff0c;做出的月饼是那种很传统很经典的款式&#xff0c;里…

金融中的数学模型

平稳时间序列 时间序列的基本统计特性&#xff0c;如均值、方差和自相关等&#xff0c;在时间上不随时间的推移而发生显著的变化。 平稳时间序列通常具有以下特征&#xff1a; 均值不随时间变化&#xff1a;序列的均值在时间上保持恒定。方差不随时间变化&#xff1a;序列的…