JavaEE之多线程的风险以及如何避免

上文我们了解了单线程以及线程的一些基本常见方法,但是多线程在一些方面会存在安全问题,此文我们来为多线程的安全 保驾护航!! 详情请见下文

1. 多线程带来的风险——线程安全

1.1 观察线程不安全

/**
 * 使用两个线程,让count自增到10w
 */
public class Thread_lesson03_01 {
    //定义一个变量,让其自增10w次
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        thread1.start();
        
        Thread thread2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        thread2.start();
        
        //等待线程结束
        thread1.join();;
        thread2.join();
        
        //结束后,打印计算的count值
        System.out.println("count="+count);
    }
}

在这里插入图片描述

1.2 线程安全的概念

想给出⼀个线程安全的确切定义是复杂的,
但我们可以这样认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的

1.3 线程不安全的原因

1. 线程调度是随机的

由于线程的执行顺序无法人为控制,抢占式执行是造成线程安全问题的主要原因而且我们解决不了,完全是CPU自己调度而且和CPU核数有关

2. 修改共享数据

  • 多个线程修改同一个变量,会出现线程安全问题
  • 多个线程修改不同的变量,不会出现线程安全问题
  • 一个线程修改一个变量,也不会出现线程安全问题

3. 原子性

我们在前面的MySQL学习过程中,知道了事务是有原子性的,比如对一个事务,如果不让他完全执行任务时,就对他操作,会造成幻读,不可读等等不好的效果,MySQL中我们通过隔离级别保证事务的原子性,原子性即 :事务要么全都执行,要么全都不执行
在线程中,我们知道⼀条Java语句不⼀定是原⼦的,也不⼀定只是⼀条指令
比如刚才我们看到的count++,其实是由三步操作组成的:

  1. 从内存把数据读到CPU (LOAD)
  2. 进行数据更新 (ADD)
  3. 把数据写回到CPU (STORE)

由于线程是抢占式执行的,此处通过时间线来助于理解线程的原子性:

在这里插入图片描述

在这里插入图片描述

下图,对线程的原子性助以理解:

在这里插入图片描述

由于执行CPU指令不是原子性的,导致这三条指令没有全部执行完成就被CPU调度走了
另外的线程加载到的值是一个原始值,当两个线程分别完成自增操作之后把值写回内存时发生了覆盖现象

4. 内存可见性

可见性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到.
Java虚拟机规范中定义了Java内存模型(JMM),如下图所示:
在这里插入图片描述

  1. 线程之间的共享变量存在主内存(MainMemory).
  2. 每⼀个线程都有自己的"⼯作内存"(WorkingMemory),且线程工作内存之间是隔离的,线程对共享变量的修改线程执行相互感知不到
  3. 当线程要读取⼀个共享变量的时候,会先把变量从主内存拷贝到⼯作内存,再从⼯作内存读取数据.
  4. 当线程要修改⼀个共享变量的时候,也会先修改⼯作内存中的副本,再同步回主内存.
  5. 工作内存是JAVA层面对物理层面的关于程序所使用到了寄存器的抽象
  6. 如果通过某种方式 让线程之间可以相互通信,称之为内存可见性

5.指令重排序

指令重排序概念:
重排序是编译器、JVM和CPU为了提高执行效率对指令顺序进行调整的现象。它在保证单线程语义不变的前提下,减少了读写操作,提升了程序运行速度,并且保证程序的运行结果是正确。重排序分为编译器优化、CPU重排序和内存系统的“重排序”。虽然带来性能提升,但也要注意其可能影响多线程环境中的数据一致性。

2. 如何解决线程不安全问题

1.线程的调度是随机执行的:硬件层面的,我们解决不了
2.修改共享数据:在真实业务场景中,很难避免多线程,提供效率,我们解决不了
3.原子性:我们可以通过🔒锁实现原子性,下文介绍
4.内存可见性:我们可以让进程之间通过一种通信关系,解决内存的不可见性
5.指令重排序:我们可以程序员直接指定那个先执行

能够解决 3、4、5 其中一项,我们的线程不安全问题就可以解决

3.synchronized 关键字

3.1 synchronized 的特性

synchronized 会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同⼀个对象synchronized就会阻塞等待.

  • 进⼊synchronized修饰的代码块,相当于加锁
  • 退出synchronized修饰的代码块,相当于解锁

在这里插入图片描述

synchronized⽤的锁是存在Java对象头⾥的。

在这里插入图片描述
可以粗略理解成,每个对象在内存中存储的时候,都存有⼀块内存表示当前的"锁定"状态(类似于厕所 的"有⼈/⽆⼈").

  • 如果当前是"⽆⼈"状态,那么就可以使⽤,使⽤时需要设为"有⼈"状态.
  • 如果当前是"有⼈"状态,那么其他⼈无法使用,只能排队

在这里插入图片描述
理解"阻塞等待"

针对每⼀把锁,操作系统内部都维护了⼀个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进⾏加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程, 再来获取到这个锁.
注意:

  • 上⼀个线程解锁之后,下⼀个线程并不是⽴即就能获取到锁.⽽是要靠操作系统来"唤醒".这也就 是操作系统线程调度的⼀部分⼯作.
  • 假设有ABC三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C 都在阻塞队列中排队等待.但是当A释放锁之后,虽然B⽐C先来的,但是B不⼀定就能获取到锁, ⽽是和C重新竞争,并不遵守先来后到的规则.

3.2 synchronize关键字的魔力

我们通过下列代码来探究synchronize的魔力吧!


3.2.1 代码1:不添加synchronize关键字,查看计算结果

public class Thread_lesson04_01 {
    public static void main(String[] args) throws InterruptedException {
        //初始化累加对象
        Counter01 counter01=new Counter01();

        //创建两个线程对一个变量进时累加
        Thread thread1=new Thread(()->{
            counter01.increase();
        });

        Thread thread2=new Thread(()->{
            counter01.increase();
        });

        // 启动线程
        thread1.start();
        thread2.start();

        //等待线程
        thread1.join();
        thread2.join();
        System.out.println(counter01.count);
    }
}
class Counter01 {
    public int count=0;
    public void increase(){
        for (int i = 0; i < 50000; i++) {
            count++;
        }
    }
}

在这里插入图片描述


3.2.2 代码2:给increase方法加上synchronize关键字

public class Thread_lesson04_02 {
    public static void main(String[] args) throws InterruptedException {
        Counter02 counter=new Counter02();
        Thread thread1=new Thread(()->{
           counter.increase();
        });
        Thread thread2=new Thread(()->{
            counter.increase();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count="+counter.count);
    }
}
class Counter02 {
    public int count=0;
    public synchronized void increase(){
        for (int i = 0; i < 50000; i++) {
            count++;
        }
    }
}

在这里插入图片描述

此时,thread1线程先获取了锁,方法执行完成之后thread2线程再获取锁,这样的情况是一个单线程运行状态,是把多线程转换为一个单线程,从而解决线程安全问题,但并不是我们想要的效果


3.2.3 代码3:给increase方法中的代码块加上synchronize关键字

public class Thread_lesson04_03 {
    public static void main(String[] args) throws InterruptedException {
        Counter03 counter=new Counter03();
        Thread thread1=new Thread(()->{
            counter.increase();
        });
        Thread thread2=new Thread(()->{
            counter.increase();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count="+counter.count);
    }
}
class Counter03 {
    public int count=0;
    public  void increase(){
        //在真实业务中,在执行锁代码块之前有很多的数据获取或其他的可以并行执行的逻辑
        //1.从数据库中查询数据 selectAll()
        //2.对数据进行处理 build()
        //3.其他的不修改共享变量的方法
        //......
        
        //当执行到修改共享变量的逻辑时,再加锁
        //被锁修饰的代码块用{}包裹,其中()中可以是任何对象,使用this就是当前调用该方法的对象
        synchronized(this) {for (int i = 0; i < 50000; i++) {
            count++;
        }
        }
    }
}

在这里插入图片描述

虽然当前代码依旧是按串行执行了,但是在锁定的代码块前后,有其他的方法或代码可以进程执行


3.2.4 代码4:定义方法increase和方法increase1,increase方法用synchronize关键字修饰,线程1调用increase方法,线程2调用increase1方法

public class Thread_lesson04_04 {
    public static void main(String[] args) throws InterruptedException {
        Counter04 counter=new Counter04();
        Thread thread1=new Thread(()->{
            counter.increase();
        });
        Thread thread2=new Thread(()->{
            counter.increase1();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count="+counter.count);
    }
}
class Counter04 {
    public int count=0;
    public synchronized void increase(){
       for (int i = 0; i < 50000; i++) {
            count++;
        }
    }

    public void increase1(){
        for (int i = 0; i < 50000; i++) {
            count++;
        }
    }
}

在这里插入图片描述

由于increase1方法不加锁,在对increase1方法方法执行时,increase1方法不需要锁即可执行,导致了线程不安全问题,具体情况如下:
在这里插入图片描述


总结 :关于synchronized
1.被 synchronized 修饰的代码会变成串行执行
2.synchronized 可以去修饰方法,也可以修饰代码块
3.被synchronized 修饰的代码并不是一次性在CPU上执行完,而是中途可能会被CPU调度走,但是只有当所有的指令执行完成之后才会释放锁
4.只给一个线程加锁,也会出现线程安全问题

了解完了synchronize关键字后,我们来谈谈何为锁,锁又有哪些知识呢??

4. 锁🔒

我们知道,事务的隔离级别是通过锁和MVCC机制保证的,那么Java当中锁是用什么来实现的呢?锁存放在哪里呢??

4.1 锁是如何解决线程安全问题的?

  1. 解决线程非原子性

通过synchronize给方法加锁解决了原子性问题

在这里插入图片描述

  1. 解决内存不可见性

后一个线程永远读到的是上一个线程存放到主内存的值,通过这样的方式实现了内存可见性,
并没有对内存可见性做技术上的处理

  1. 解决不了重排序问题

8.2 锁存放的位置

在Java虚拟机中,对象在内存中的结构可以划分为4个区域

  1. markword: 对象头:锁信息、GC(垃圾回收)次数,程序计数器,一般为8BYTE
  2. 类型指计:当时的对象是哪个类,一般为4BYTE
  3. 实例数据:成员变量,不定
  4. 对齐填充:一个对象所的占的内存必须是8byte的整数倍,根据实例数据确定

5. volatile 关键字

上面我们了解到引发线程不安全的问题有几种情况:

  • 线程的调度是随机的
  • 多个线程修改了共享数据
  • 原子性
  • 内存不可见性
  • 指令重排序

其中线程的调度是随机的,我们解决不了,对于共享数据的修改,我们使用了synchronize关键字对修改共享数据的代码进行加锁,实现了原子性,由于原子性让代码串行执行,间接的实现了内存的可见性,然而在真正意义上并没有对内存的不可见性做修改,Java中提供了volatile关键字,实现了内存可见性和禁止指令重排序,我们现在开始来揭开volatile神秘的面纱吧

volatile的作用: volatile 修饰的变量, 能够保证 “内存可见性”,也可以解决有序性的问题(禁止指令重排序)

5.1 内存可见性

代码在写入 volatile 修饰的变量的时候

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

前⾯我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存),
速度非常快, 但是可能出现数据不⼀致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.

我们提供一段代码示例,来揭开volatile的魅力吧

在这个代码中

  • 创建两个线程 thread1 和 thread2
  • thread1中包含⼀个循环, 这个循环以 flag != 0 为循环条件.
  • thread2中从键盘读⼊⼀个非0整数, 并把这个整数赋值给 flag.
  • 预期当用户输入 !=0 的值的时候, thread1 线程结束.
public class Thread_lesson05_01 {
    static int flag=0;
    public static void main(String[] args) {
        Thread thread1= new Thread(()->{
        System.out.println("thread1线程启动...");
            while (flag==0) {
                //do nothing
            }
            System.out.println("thread1线程退出...");
        });

        //想要通过线程2对flag的值进行修改,让线程1停止
        Thread thread2= new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("thread2线程启动...");
            System.out.println("输⼊⼀个非0整数:");
            flag=scanner.nextInt();
        });

        //启动线程
        thread1.start();
        thread2.start();

    }
}

在这里插入图片描述


为何thread2对flag值的修改thread1无法感知到呢??

thread1 读的是自己的工作内存(寄存器)中的内容.
当 thread2 对 flag 变量进行修改, 此时 thread1 感知不到 flag 的变化.

图解:

在这里插入图片描述

下面我们试着给flag加上volatile关键字修饰,查看效果:

static volatile int flag=0;

在这里插入图片描述

我们发现通过volatile关键字解决了内存不可见问题,那么volatile是究竟如何解决的呢?

我们知道为了解决内存的可见性问题,最重要的就是当一个线程修改了另一个线程需要的变量,必须让另一个线程感知到

在CPU层面:
在这里插入图片描述

在Java层面:

内存屏障
作用是保证指令执行的先后顺序从而保证内存可见性
在这里插入图片描述
volatile读:
在这里插入图片描述
volatile写:
在这里插入图片描述


5.2 有序性

用volatile 修饰过的变量,由于前后有内存屏障,保证了指令的执行顺序,也可以理解为告诉编译器不要进行指令重排,以此保证了有序性

5.3 volatile不能保证原子性

我们通过一段代码来测试volatile是否可以保证线程的原子性

public class Thread_lesson05_02 {
    static class Counter01{
       volatile int  count=0;
        void increase(){
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //初始化自增对象
        Counter01 counter01=new Counter01();
        
        //创建两个线程对一个变量进时累加
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter01.increase();
            }
        });

        Thread thread2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter01.increase();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        //等待线程
        thread1.join();
        thread2.join();
        System.out.println("count="+counter01.count);
    }
}

在这里插入图片描述

结论volatile解决不了原子性问题

6. wait 和 notify

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序,所以Java中的Object类给出了wait和notify方法

此时我们就有一个疑问了,前面我们不是学习过join方法也可以进行等待呀,为什么Java还要提供wait方法呢?

我们可以通过一个案列来理解:

一家人要吃包子 (主线程) ,妈妈让小明去买包子 (子线程),此时join主要等待子线程的结果,即一家人等着小明买回来的包子,当小明来到包子铺,有两种情况:

  1. 包子已经做好,小明可以直接买回家
  2. 老板正在做包子,此时小明就需要等待包子做好
    此时小明买包子这个线程和老板包包子这个线程没有主子线程之分,但是要等老板做包子这个线程的结果,这是就需要用wait(),当老板做好包子之后,大喊一声包子出锅了notify()

总结: join()用于主子线程的等待,wait()用于非主子线程的等待,wait()可以理解为当前线程等待另一个线程准备资源,当资源准备好之后,通过notify()通知当前线程

6.1 wait()方法

wait()/wait(long timeout): 让当前线程进入等待状态

wait 做的事情:

  • 使当前执行代码的线程进行等待.(把线程放到等待队列中)
  • 释放当前的锁
  • 满足⼀定条件时被唤醒,重新尝试获取这个锁.

wait要搭配synchronized来使用.脱离synchronized使用wait会直接抛出异常.

wait 结束等待的条件:

  • 其他线程调用该对象的notify方法.
  • wait等待时间超时(wait方法提供⼀个带有timeout参数的版本,来指定等待时间).
  • 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException 异常.

6.2 notify()方法

notify() 唤醒在当前对象上等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其
    它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈wait状态的线程。(并没有"先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

6.3 notifyAll方法

notifyAll(): 唤醒在当前对象上等待的线程.
notify方法只是唤醒某⼀个等待线程.使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程.

6.4 演示wait() 和 notify()方法的使用

import java.util.concurrent.TimeUnit;

/**
 * 演示wait() 和 notify()方法的使用
 * 创建两个线程,一个线程调用wait(),一个线程调用notify()
 */
public class Thread_lesson05_03 {
    public static void main(String[] args) {
        //定义一个锁对象
        Object locker=new Object();
        
        //创建调用wait()线程
        Thread t1=new Thread(()->{
                synchronized (locker) {
                    System.out.println("调用wait()之前...");
                    // 执行线程的逻辑
                    // 如果没有满足线程所需要的数据,那么就等待
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("wait()唤醒之后..");
                }
        });


        Thread t2 = new Thread(() -> {
                System.out.println("notify()之前...");
                // 同等待的锁对象进行唤醒
                synchronized (locker) {
                    locker.notify();
                }
                System.out.println("notify()之后...");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });

        // 启动线程
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
wati与notify 必须配置synchronized一起使用, 并且使用同一个锁对象

在这里插入图片描述

注意:

  1. 当一个线程调用了wait之后,就释放掉当前持有的锁,等待被其他的线程唤醒
  2. 当另一个线程调用了notify之后,之前调用了wait的线程被唤醒,需要重新去竞争锁,拿到锁之后,会从wait的位置继续执行逻辑

使用小结:

1. wait和notify必须搭配synchronized一起使用
2. wait和notify使用的锁对象必须是同一个
3. notify执行多少次都没有关系(即使没有线程在wait)

6.5 wait和sleep的区别

其实理论上wait和sleep完全是没有可比性的,因为

1. ⼀个是用于线程之间的通信的,⼀个是让线程阻塞⼀段时间
2. 唯⼀的相同点就是都可以让线程放弃执行一段时间
3. wait需要搭配synchronized使用,sleep,join不需要
4. wait是Object的方法,sleep是Thread的静态方法

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【OpenCV】直方图

理论 可以将直方图视为图形或曲线图&#xff0c;从而使您对图像的强度分布有一个整体的了解。它是在X轴上具有像素值(不总是从0到255的范围)&#xff0c;在Y轴上具有图像中相应像素数的图。 这只是理解图像的另一种方式。通过查看图像的直方图&#xff0c;您可以直观地了解该…

MoeCTF2024-Web题解

目录 1、弗拉格之地的入口 2、垫刀之路01: MoeCTF&#xff1f;启动&#xff01; 3、ez_http 4、ProveYourLove 5、弗拉格之地的挑战 6、ImageCloud前置 7、垫刀之路02: 普通的文件上传 8、垫刀之路03: 这是一个图床 9、垫刀之路05: 登陆网站 10、垫刀之路06: pop bas…

python学opencv|读取图像(六)读取图像像素RGB值

【1】引言 前序已经掌握了如何获取灰度图像的像素&#xff0c;文章链接为&#xff1a; python学opencv|读取图像&#xff08;五&#xff09;读取灰度图像像素-CSDN博客 实际上像素就像一个坐标轴&#xff0c;约束了图像的大小。 但实际上我们在学习过程中&#xff0c;对于同…

ThingsBoard规则链节点:RabbitMQ 节点详解

ThingsBoard 是一个开源的物联网平台&#xff0c;允许开发者快速构建IoT产品。它提供了设备连接、数据收集、处理和可视化等功能。为了实现高效的数据处理和消息传递&#xff0c;ThingsBoard 集成了多种消息队列服务&#xff0c;其中就包括了RabbitMQ。 RabbitMQ 是一个广泛使用…

如何创建基于udp的客户端和服务端

1.先创建好udpServer.hpp、udpServer.cc、udpClient.hpp、udpClient.cc的框架。 #pragma once #include <string> #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <cerrno> #include…

TCP 2

文章目录 Tcp状态三次握手四次挥手理解TIME WAIT状态 如上就是TCP连接管理部分 流量控制滑动窗口快重传 延迟应答原理 捎带应答总结TCP拥塞控制拥塞控制的策略 -- 每台识别主机拥塞的机器都要做 面向字节流和粘包问题tcp连接异常进程终止机器重启机器掉电/网线断开 Tcp状态 建…

ChatGPT Pro是什么

ChatGPT Pro 和 ChatGPT Plus 的区别主要体现在功能范围、适用场景和目标用户上。 ChatGPT Plus 功能 • 价格&#xff1a;20美元/月。 • 目标用户&#xff1a;针对个人用户设计。 • 主要特点&#xff1a; • 在高峰期响应速度更快。 • 使用高级模型&#xff08;如 GPT-4…

【开源免费】基于Vue和SpringBoot的桂林旅游景点导游平台(附论文)

博主说明&#xff1a;本文项目编号 T 079 &#xff0c;文末自助获取源码 \color{red}{T079&#xff0c;文末自助获取源码} T079&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析…

C++ day3——C++核心

作业&#xff1a; 整理思维导图 2、整理课上代码 3、把课上类的三个练习题的构造函数写出来 1、定义一个矩形类Rec #include <iostream>using namespace std; class Rec {const int length;int width; public:Rec(int length,int width):length(length),width(width)…

目前Java后端就业前景到底怎么样?

很多人都说今年对于IT行业根本没有所谓的“金三银四”“金九银十”。在各大招聘网站或者软件上不管是大厂还是中小公司大多都是挂个招聘需求&#xff0c;实际并不招人&#xff1b;在行业内的程序员基本都已经感受到了任老前段时间口中所谓的“寒气”。 虽然事实确实是如此&…

倚光科技助力自由曲面设计与加工

近年来&#xff0c;自由曲面因其在光学、汽车、航空航天等领域的广泛应用&#xff0c;受到设计师和工程师的高度关注。自由曲面作为一种具有更高自由度的非球面透镜&#xff0c;能够在光学系统中实现更加精确的光线控制&#xff0c;优化像差校正&#xff0c;并且在满足功能需求…

算法论文/半监督1——2024最新半监督目标检测综述(CNN和Transformer)全文1.5W字

Semi-Supervised Object Detection: A Survey on Progress from CNN to Transformer 摘要 半监督学习的惊人进步促使研究人员探索其在计算机视觉领域内目标检测任务中的潜力。半监督对象检测 &#xff08;SSOD&#xff09; 利用小型标记数据集和较大的未标记数据集的组合。这…

在Windows 10中使用SSH远程连接服务器(附花生壳操作方法)

SSH 在 linux 中是一种重要的系统组件&#xff0c;用户可以使用 SSH 来远程连接 linux 系统的计算机&#xff0c;或者传输文件。不过在 win10 以前&#xff0c;windows 并不原生支持 SSH&#xff0c;需要借助第三方工具来使用 SSH 功能。而实际上&#xff0c;微软在 2015 年就曾…

自动秒收录程序与自动秒收录网站源码论坛版本下载

自动秒收录程序与自动秒收录网站源码论坛版本下载 随着互联网的快速发展&#xff0c;网站优化已成为众多企业和个人博主提升在线影响力的关键手段。其中&#xff0c;SEO&#xff08;搜索引擎优化&#xff09;作为提升网站排名的核心策略&#xff0c;备受关注。而SEO优化的一个…

在Goland中对goroutine协程断点调试

在Goland中对goroutine协程断点调试 环境: Goland 参考了 chatgpt 的回复 进行断点调试的代码 package mainimport ("fmt""sync""time" )// worker 模拟处理任务 func worker(id int, wg *sync.WaitGroup) {defer wg.Done() // 确保任务完成后…

Sublime Text 64位:前端及全栈开发利器

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;Sublime Text作为一款高效的文本编辑器&#xff0c;在前端网页开发领域受到广泛青睐&#xff0c;特别是其64位版本在处理大型项目和高内存需求的场景下表现出色。编辑器内置Emmet插件&#xff0c;提供代码高亮、…

css矩形样式,两边圆形

废话不多说&#xff0c;代码如下&#xff0c;直接拷贝即可使用&#xff1a; index.vue文件 <template><view class"wrap"><view class"tabs"><view class"tab active"><view class"name">标签</view…

Spring Boot 3 中Bean的配置和实例化详解

一、引言 在Java企业级开发领域&#xff0c;Spring Boot凭借其简洁、快速、高效的特点&#xff0c;迅速成为了众多开发者的首选框架。Spring Boot通过自动配置、起步依赖等特性&#xff0c;极大地简化了Spring应用的搭建和开发过程。而在Spring Boot的众多核心特性中&#xff…

C# MVVM 牛牛的实现依赖注入和MVVM绑定(DependencyInjection+CommunityToolkit)

这段时间在网上发现搜索MVVM数据绑定时&#xff0c;发现很多都是最基本的数据绑定&#xff0c;完全没有考虑依赖注入的问题&#xff0c;这里实现一下我们的方法&#xff0c;让我们的数据绑定和依赖注入都变得简单起来。 安装资源包 首先我们要下载一下资源包DependencyInject…

最小二乘法拟合出二阶响应面近似模型

背景&#xff1a;根据样本试验数据拟合出二阶响应面近似模型&#xff08;正交二次型&#xff09;&#xff0c;并使用决定系数R和调整的决定系数R_adj来判断二阶响应面模型的拟合精度。 1、样本数据&#xff08;来源&#xff1a;硕士论文《航空发动机用W形金属密封环密封性能分析…