【JavaEE】线程安全问题

目录

一.线程安全问题

1.什么是线程安全

2.线程不安全的原因

3.如何解决线程安全问题?

3.1synchronized的使用方式

3.2解决示例自增带来的线程安全问题

(1)对代码块进行加锁

 (2)对方法进行加锁

4.synchronized的特性

5.死锁

5.1两个线程两把锁

5.2N个线程M把锁

​编辑 5.3造成死锁的必要条件

5.4如何避免出现死锁?

6.volatile关键字

7.wait和notify

7.1wait

7.2notify

notify和notifyAll的区别 

小练习

 7.3wait和sleep的区别


 在上一篇中,我们讲解了线程以及如何在java中创建线程,但在多线程之间存在着线程安全问题,本篇我们就围绕线程安全问题来展开。

一.线程安全问题

1.什么是线程安全

线程安全是指在多线程环境下,共享数据的访问和操作不会引起不正确的结果。具体来说,线程安全的程序能够正确地处理多个线程同时访问共享数据的情况,保证数据的一致性和正确性。当两个或者多个线程同时访问共享的数据时,导致数据不一致,称为线程安全问题

2.线程不安全的原因

  1. 线程在操作系统中是随机调度、抢占式执行的
  2. 多个线程同时修改同一个变量
  3. 修改操作不具"原子性"
  4. 内存可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。
  5. 指令重排序:计算机系统在执行程序时,为了提高程序性能,可能会对指令进行重新排序的操作。

示例:我们这里来利用两个线程来让count累加,每个线程体中循环次数为5w次。如下:

package Threads.threadtext;/*** Demo 类用于演示线程安全问题。* 本类中,两个线程同时增加一个静态变量 count 的值,以展示并发情况下可能出现的不一致问题。*/
public class Demo {// count 用于演示线程安全问题,初始值为 0。public static int count=0;//静态变量/*** 程序入口。* 创建两个线程,每个线程执行 50000 次 count 的增加操作。* 随后等待两个线程执行完毕,并打印最终的 count 值。* 期望输出为 100000,但实际运行结果可能因并发问题而小于该值。** @param args 命令行参数* @throws InterruptedException 如果线程在等待时被中断*/public static void main(String[] args) throws InterruptedException{// 创建线程 t1,负责增加 count 变量的值Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}});// 创建线程 t2,同样负责增加 count 变量的值Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}});// 启动线程 t1 和 t2t1.start();t2.start();// 等待线程 t1 和 t2 执行完毕t1.join();t2.join();// 打印最终的 count 值System.out.println(count);}
}

在上面的代码中,我们期望的count的是10w,但实际真的有10w吗?我们可以运行一下。

我们可以看到最后count的值为89931,这与我们期望的值相差挺大。但其实我们在每次运行之后,显示的答案都会不同,这主要是与线程在cpu中是抢占式执行、随机调度有关

在上图中,要执行count++过程,从cpu上要分为三个指令

指令是cpu执行的最基本单位,线程要调度,也至少需要将当前的指令执行完):

  1. 在内存中读取数据到cpu寄存器里(load)
  2. 把cpu寄存器里的数据+1              (add)
  3. 把寄存器里的数据写回到内存中  (save)

 

由于count++是三条指令,且线程在cpu中是抢占式执行、随机调度的,所以可能会出现cpu刚执行一条指令或者2个指令、3个指令被调走的情况。基于这些情况,当两个线程同时进行对count++时,就会出现线程安全问题。

以下是根据时间轴来画指令的执行情况:

若按照以上这种调度顺序执行,则可以得到10w,但这个概念非常小,由于是随机调度的,因此在计算时会产生很多其他的执行顺序,以下是例举的一些执行顺序:

在上图中,这几种执行顺序最终都不能让count的值为10w。

3.如何解决线程安全问题?

我们可以用synchronized关键字来对代码块或方法进行加锁。

3.1synchronized的使用方式

1)对指定代码块进行加锁

    synchronized(锁对象){代码块}

2)对指定方法进行加锁

    synchronized 权限修饰符 (static) 返回值 方法名(参数){代码块}

当进入synchronized修饰的代码块时,相当于加锁;退出synchronized修饰的代码块,相当于解锁。

注意:

  • 若synchronized修饰的是一个静态的方法,就相当于针对当前类对象进行加锁。
  • 若synchronized修饰的是一个普通的方法,就相当于针对this进行加锁。

锁对象的作用:锁对象可以是任意的Object/Object子类的对象,锁对象是谁并不重要,重要的是两个或多个线程的锁对象是否是同一个。若是同一个,则会出现锁竞争/锁冲突。反之,若不是针对同一个锁对象加锁,则不会出现。 

使用锁,本质上就是将线程从并行执行-->串行执行,这样来解决线程安全问题。

3.2解决示例自增带来的线程安全问题

(1)对代码块进行加锁

对上述的示例进行加锁,这里我们实例一个Object类对象locker作为锁对象,即:


/*** Demo 类用于演示线程安全问题。* 本类中,两个线程同时增加一个静态变量 count 的值,以展示并发情况下可能出现的不一致问题。*/
public class Demo {// count 用于演示线程安全问题,初始值为 0。public static int count = 0;//静态变量static Object locker = new Object();//锁对象/*** 程序入口主方法。* 创建两个线程,每个线程循环50000次,对静态变量count进行自增操作。* 使用synchronized关键字确保在同一时间只有一个线程可以访问count变量,以演示线程安全。** @param args 命令行参数* @throws InterruptedException 如果线程在等待、通知或唤醒过程中被中断*/public static void main(String[] args) throws InterruptedException {// 创建线程 t1,负责增加 count 变量的值Thread t1 = new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){count++;}}});// 创建线程 t2,同样负责增加 count 变量的值Thread t2 = new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){count++;}}});// 启动线程 t1 和 t2t1.start();t2.start();// 等待线程 t1 和 t2 执行完毕t1.join();t2.join();// 打印最终的 count 值System.out.println(count);}
}

可以看到,在加锁之后,count的值能达到我们所期望的。

 (2)对方法进行加锁
class Counter {public int count = 0;synchronized public void add(){count++;}
}public class Demo1 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();// 创建线程 t1,负责增加 count 变量的值Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (counter) {counter.add();}}});// 创建线程 t2,同样负责增加 count 变量的值Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (counter) {counter.add();}}});// 启动线程 t1 和 t2t1.start();t2.start();// 等待线程 t1 和 t2 执行完毕t1.join();t2.join();// 打印最终的 count 值System.out.println(counter.count);}
}

通过对类的实例对象的方法进行加锁<==>对类的实例对象进行加锁,也可以写成:

class Counter {public int count = 0;public void add(){synchronized (this){count++;}}
}

对静态方法进行加锁<==>对该类的类对象进行加锁


class Counter {public static int count = 0;synchronized public static void add(){count++;}
}public class Demo1 {public static void main(String[] args) throws InterruptedException {// 创建线程 t1,负责增加 count 变量的值Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (Counter.class) {Counter.add();}}});// 创建线程 t2,同样负责增加 count 变量的值Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (Counter.class) {Counter.add();}}});// 启动线程 t1 和 t2t1.start();t2.start();// 等待线程 t1 和 t2 执行完毕t1.join();t2.join();// 打印最终的 count 值System.out.println(Counter.count);}
}
class Counter {public static int count = 0;public static void add(){synchronized (Counter.class){count++;}}
}

为什么说是对该类的类对象进行加锁?

静态方法在java中是不需要创建类的实例对象就能进行调用。可以由该类本身或者该类的对象的引用来引用。

类对象:一个类只有一个类对象;

一个类对象中包含:

  1. 类有哪些属性,都是啥名字,啥类型,权限
  2. 类的方法有哪些,都是啥名字,参数,类型,权限
  3. 类自身继承了哪个类,实现了哪些接口等等

4.synchronized的特性

1.互斥性:synchronized会引起互斥效果,当某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。

假设现在有三个人准备上厕所,但是厕所只有一个,那么当第一个人进去厕所后,其余的人要等待前面的人上完厕所才能上。即一个线程先上了锁,其他线程只能等待这个线程释放,这个等待的过程称为“阻塞等待”。

2.可重入synchronized同步块对同一条线程来说是可重入的,不会出现死锁的情况。

 我们来看个例子:

class Demo1 {static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{synchronized (locker){synchronized (locker){System.out.println("t1");}}});t1.start();}
}

 在这个例子中,线程t1首先会对locker对象进行加锁,但是在给locker加完一次之后,再加一次锁,就会失败,为什么?第二次加锁需要第一次加锁释放之后才能加锁,但第一次加锁释放需要执行完synchronized修饰的代码块,很遗憾的是,由于第二次加锁在发现对象已经有锁之后,会进行阻塞等待状态,直到第一个锁被释放后才能进行加锁。从而造成了“死锁”。

但在java中,synchronized锁具有可重入性,即:在同一线程中对同一个锁对象加多次锁,不会造成死锁。

那如果在一个线程中,对同个锁对象加多次锁,什么时候才能释放?

可重入锁的内部,包含了:“线程持有者”和“计数器”。

在一个线程,如果某个线程加锁的时候,发现锁已经被占用,且占用的恰好是自己,那么仍然可以继续获取到锁,并让计数器自增。解锁时计数器递减为0时,才能真正释放锁。

5.死锁

如果没有snychronized的可重入性,那么如果针对一个锁连续加多次锁,就会出现死锁的情况。

常见的死锁有两种:

  1. 两个线程两把锁
  2. N个线程M把锁

5.1两个线程两把锁

现有两个线程t1和t2,线程t1已经获得了锁1,线程2已经获得了锁2,同时,线程t1想要获得锁2,线程t2想要获得锁1,就会发生死锁。

class Demo2{//锁对象private static Object locker1=new Object();private static Object locker2=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{synchronized (locker1) {System.out.println("t1获得锁1");//让线程睡眠1秒,是为了让两个线程都能先拿到锁,如果没有sleep,执行结果就不可控,可能就会出现某个线程一次拿了两个锁,另一个线程还在执行,无法构成死锁try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t1获得锁2");}}});Thread t2=new Thread(()->{synchronized (locker2) {System.out.println("t2获得锁2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1){System.out.println("t2获得锁1");}}});t1.start();t2.start();}
}

上述代码在运行之后,会出现两个线程一直僵持的状态

也可以打开jconsole进行查看两个线程的状态,可以看到两个线程都是BLOCKED状态

 

5.2N个线程M把锁

这就得提到一个经典的问题:哲学家进餐问题 。

有5个哲学家共用同一张桌子,分别坐在周围的五张椅子上,在桌子上有5只碗和5根筷子,他们只有两个行为:思考和进餐,在思考的时候,不需要任何动作,在进餐的时候需要分别拿起左右最近的筷子进餐。而且哲学家非常的固执,如果他拿到了筷子,如果没吃到东西是不会放下筷子的。

这些哲学家进餐时间是不定的,如果一个哲学拿到左手边的筷子,此时需要右手边的筷子,但右边的筷子也被另一位哲学家拿了,那么他们就会陷入僵持状态,谁也不让谁。若当5个哲学家同时拿起了筷子,那么就会造成5个哲学家一直吃不到东西。

那么如何解决上述问题,让每个哲学家都能进餐呢? 

我们给哲学家进行编号,从第一个哲学家开始,先拿起左边的筷子,再看看右边的筷子是否有使用,若没有就拿起来。同理,往后的每个哲学家都是先拿起左边的筷子,右边的筷子若有哲学家使用就进行等待。

当1号哲学家用完之后,此时2号哲学家就能进行就餐,以此类推,当4号哲学家进完餐之后,5号就可以拿起他左右边的筷子进行就餐。

 5.3造成死锁的必要条件

  1. 锁是互斥的(锁的基本特性)。当一个线程1被上锁之后,另一个线程2想要上锁,就会进入阻塞状态,等到线程1释放锁。
  2. 锁是不可被抢占的(锁的基本特性)。线程1拿到了锁A,如果线程1不主动释放锁A,线程2就不能把锁A抢过去。
  3. 请求和保持(代码结构)。一个已经有锁线程请求获取另一个锁,但同时又不释放现有的锁。
  4. 循环等待/环路等待/循环依赖(代码结构)。如小白正在玩电脑,同时也想玩手机,而小黑正在玩手机,同时也想玩电脑。但两个人谁也不让谁,都想同时玩电脑和手机,就陷入了僵局。

5.4如何避免出现死锁?

互斥和不可抢占是锁的基本特性,我们可以通过避免代码结构“嵌套锁”,但在某些场景下需要用到。所以最好避免死锁的是破坏循环等待。在加多把锁的时候,先加编号小的锁,再加编号大的锁,且所有的线程都要遵循这一个规则。

class Demo2{//锁对象private static Object locker1=new Object();private static Object locker2=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{synchronized (locker1) {System.out.println("t1获得锁1");//让线程睡眠1秒,是为了让两个线程都能先拿到锁,如果没有sleep,执行结果就不可控,可能就会出现某个线程一次拿了两个锁,另一个线程还在执行,无法构成死锁try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t1获得锁2");}}});Thread t2=new Thread(()->{synchronized (locker1) {System.out.println("t2获得锁1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t2获得锁2");}}});t1.start();t2.start();}
}

6.volatile关键字

volatile关键字修饰的变量,保证了“内存的可见性”,在编译器判断是否要进行优化时,通过volatile能够让编译器知道当前的变量不需要进行优化。

示例:

class Demo12{static int i=0;public static void main(String[] args) {Thread t1=new Thread(()->{System.out.println("t1 线程开始");while(i==0){;}System.out.println("t1 线程结束");});Thread t2=new Thread(()->{System.out.println("请输入i的值");Scanner scanner=new Scanner(System.in);i=scanner.nextInt();});t1.start();t2.start();}
}

当我们在输入一个值后,发现程序并没有因此停止

 

这是为什么呢?

我们都知道变量是存储在内存中的,当我们需要使用这个变量的时候,cpu寄存器会从内存中将该变量的数据读取过来,再将cpu寄存器获取到的变量的值读取到我们的代码中,但是从内存中读取变量的速度远小于直接从寄存器中读取变量的速度。当需要对某个变量进行多次的读取时,编译器会自动优化:即从内存中读取到一次的值存放到cpu寄存器中,剩下的读取操作则直接从寄存器中读取即可。

因此,在这里,当我们启动线程t1,t2时,t1内部的循环可能已经进行成千上万次甚至更多,那么此时编译器就会对其进行优化,直接从cpu寄存器中读取,以此来提高运行速度。所有,当我们在线程t2输入一个值后,线程t1中的变量并不能获取到内存中已经修改的值,就会一直执行下去。

 这就是“内存不可见性”,当在一个线程中对某个变量进行修改,由于编译器的优化,在另外的线程中不能获取到内存中已修改的值。

因此,我们需要使用volatile关键字来修饰变量,让编译器知道这个变量不能进行优化,每次操作都需要从内存中获取到值,而不是直接从cpu寄存器中获取。

/*** Demo12 类用于演示两个线程之间的交互。* 其中一个线程等待另一个线程输入并设置共享变量 i 的值。*/
class Demo12{/*** 共享变量 i,使用 volatile 关键字修饰以确保多线程环境下的可见性。*/volatile static int i=0;/*** 程序入口点。* 创建两个线程,一个负责等待直到 i 被赋值,另一个负责从用户输入中获取 i 的值。* @param args 命令行参数*/public static void main(String[] args) {// 创建线程 t1,负责等待直到 i 的值被设置Thread t1=new Thread(()->{System.out.println("t1 线程开始");while(i==0){// 通过空循环等待 i 的值被设置}System.out.println("t1 线程结束");});// 创建线程 t2,负责从用户输入中获取 i 的值Thread t2=new Thread(()->{System.out.println("请输入i的值");Scanner scanner=new Scanner(System.in);i=scanner.nextInt();});// 启动两个线程t1.start();t2.start();}
}

虽然使用volatile关键字能够保证线程之间内存是可见的,但是不具有原子性。但使用synchronized能够保证原子性。

示例:

class Demo{static volatile int count=0;public static void main(String[] args) {Thread t1=new Thread(()->{for (int i=0;i<5000;i++){count++;}});Thread t2=new Thread(()->{for (int i=0;i<5000;i++){count++;}});t1.start();t2.start();try {t1.join();t2.join();}catch (InterruptedException e){e.printStackTrace();}System.out.println("count="+count);}
}

 

可以看到,即使使用volatile,也不能让count的值为1w。

7.wait和notify

由于线程之间是抢占式执行、随机调度的,因此线程执行的先后顺序是无法确定的。但在实际开发中我们有时候希望能够协调多个线程之间的先后执行顺序。

wait和notify都是Object中的方法,任意的Object类对象都可以使用者两个方法

7.1wait

wait可以让调用的线程进入阻塞状态。

wait一共会做三件事:

  1. 释放锁
  2. 进入阻塞状态,等待通知
  3. 收到通知之后,唤醒线程,并重新尝试获取锁

我们来看个例子,既然wait和notify都是object中的方法那么能够直接使用吗?

class Demo14{static Object lock=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{System.out.println("t1 线程开始");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println();});t1.start();}
}

 我们在前面提到了wait需要做三件事,第一件事就是释放锁,说明在使用wait时,需要给他加锁才能够使用。

class Demo14{static Object lock=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{System.out.println("t1 线程开始");synchronized (lock){try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println();});t1.start();}
}

 当我们运行上面的代码之后,可以看到线程t1现在是处于WAITTING(不超时的等待),但由于现在没有其他的线程去notify,因此t1会一直等下去。

7.2notify

使用notify去通知t1线程并让wait去唤醒线程。

/*** Demo14 类用于演示使用对象锁和等待/通知机制进行线程通信的简单示例。*/
class Demo14{/*** 用于线程同步的锁对象。*/static Object lock=new Object();/*** 程序入口点。* 创建两个线程,一个线程负责等待,另一个线程负责通知。* @param args 命令行参数*/public static void main(String[] args) {/* 创建线程 t1,该线程打印开始信息后进入同步块并等待 */Thread t1=new Thread(()->{System.out.println("t1 线程开始");synchronized (lock){try {/* t1 线程释放锁并等待,直到被通知 */lock.wait();} catch (InterruptedException e) {/* 将中断异常转换为运行时异常并抛出 */throw new RuntimeException(e);}}System.out.println("t1 线程结束");});/* 创建线程 t2,该线程读取用户输入后进入同步块并唤醒等待中的线程 */Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in);scanner.nextInt();synchronized (lock){/* t2 线程唤醒等待中的线程后离开同步块 */lock.notify();}System.out.println("t2结束");});/* 启动两个线程 */t1.start();t2.start();}
}

这里我们设置从控制台输入,方便观察等待线程被唤醒时的现象。

 

notify和notifyAll的区别 

notify()是用于通知单个线程,notifyAll()是通知所有线程

 7.3wait和sleep的区别

wait默认是“死等”,wait也提供了带参数的版本,指定超时时间,若wait达到了最大的时间,notify还没有通知就不会继续等待下去,而是会继续执行。

wait和sleep有着本质区别:

  1. wait是为了提前唤醒线程;而sleep是固定时间的阻塞,不涉及唤醒,但sleep可以被interrupt唤醒,但调用interrupt是终止线程。
  2. wait需要搭配synchronized使用,wait会先释放锁,并同时等待;sleep和锁无关,如果不加锁sleep可以正常使用,但如果加了锁,sleep也不会释放锁,而是拉着锁一起睡眠,其他线程无法拿到锁。
  3. wait是Object的⽅法sleep是Thread的静态⽅法.

小练习

利用线程,按照顺序打印出ABC

 这里需要用到两个object对象,为什么呢?

如果我们使用同一个object,那么打印B和C的线程的顺序就是随机的,这就不符合我们的预期。可以去实验一下,使用同一个锁对象,可能就会导致打印顺序不确定。线程调度是由操作系统决定的,我们不能保证线程总是按照预期的顺序被调度。这里就不过多说。

class Demo16{static Object lock1=new Object();static Object lock2=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{System.out.print("A");synchronized (lock1){lock1.notify();}});Thread t2=new Thread(()->{synchronized (lock1){try {lock1.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.print("B");synchronized (lock2){lock2.notify();}});Thread t3=new Thread(()->{synchronized (lock2){try {lock2.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("C");});t1.start();t2.start();t3.start();}
}

 打印10个ABC

class Demo16{static Object lock1=new Object();static Object lock2=new Object();static Object lock3=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{for(int i=0;i<10;i++) {System.out.print("A");synchronized (lock1) {lock1.notify();}synchronized (lock3) {try {lock3.wait();} catch (InterruptedException e) {e.printStackTrace();}}}});Thread t2=new Thread(()->{for(int i=0;i<10;i++) {synchronized (lock1) {try {lock1.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print("B");synchronized (lock2) {lock2.notify();}}});Thread t3=new Thread(()->{for(int i=0;i<10;i++) {synchronized (lock2) {try {lock2.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("C");synchronized (lock3) {lock3.notify();}}});t1.start();t2.start();t3.start();}
}

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Python+Flask+MySQL+日线指数与情感指数预测的股票信息查询系统【附源码,运行简单】

PythonFlaskMySQL日线指数与情感指数预测的股票信息查询系统【附源码&#xff0c;运行简单】 总览 1、《股票信息查询系统》1.1 方案设计说明书设计目标工具列表 2、详细设计2.1 登录2.2 程序主页面2.3 个人中心界面2.4 基金详情界面2.5 其他功能贴图 3、下载 总览 自己做的项…

H3CNE(路由基础、直连路由与静态路由)

目录 6.1 直连路由 6.2 静态路由理解性实验 6.2.1 配置直连路由 6.2.2 配置静态路由 6.3 路由表的参数与比较 6.3.1 优先级的比较 6.3.2 开销的比较 6.4 路由器中的等价路由、浮动路由、默认路由 6.4.1 等价路由 6.4.2 浮动路由 6.4.3 默认路由(缺省路由) 6.1 直连路…

C++:模板(函数模板,类模板)

目录 泛型编程 函数模板 函数模板格式 函数模板的原理 函数模板的实例化 类模板 类模板格式 类模板实例化 模板分为函数模板和类模板 在C中使用模板可以让我们实现泛型编程 泛型编程 如果我们需要实现一个加法add函数&#xff0c;那么会怎么实现呢&#xff1f; int…

opencv grabCut前景后景分割去除背景

参考&#xff1a; https://zhuanlan.zhihu.com/p/523954762 https://docs.opencv.org/3.4/d8/d83/tutorial_py_grabcut.html 环境本次&#xff1a; python 3.10 提取前景&#xff1a; 1、需要先把前景物体框出来 需要坐标信息&#xff0c;可以用windows自带的画图简单提取像素…

如何合并电脑硬盘分区?轻松合并电脑硬盘分区

在日常使用电脑的过程中&#xff0c;我们有时需要对硬盘进行分区管理。然而&#xff0c;随着时间的推移&#xff0c;我们可能会发现原有的分区设置不再满足需求&#xff0c;这时就需要对分区进行调整&#xff0c;甚至合并分区。那么&#xff0c;我们该如何合并电脑硬盘分区呢&a…

【Vue实战教程】之Vue工程化项目详解

Vue工程化项目 随着多年的发展&#xff0c;前端越来越模块化、组件化、工程化&#xff0c;这是前端发展的大趋势。webpack是目前用于构建前端工程化项目的主流工具之一&#xff0c;也正变得越来越重要。本章节我们来详细讲解一下如何使用webpack搭建Vue工程化项目。 1 使用we…

【数据结构】稀疏数组

问题引导 在编写五子棋程序的时候&#xff0c;有“存盘退出”和“续上盘”的功能。现在我们要把一个棋盘保存起来&#xff0c;容易想到用二维数组的方式把棋盘表示出来&#xff0c;但是由于在数组中很多数值取默认值0&#xff0c;因此记录了很多没有意义的数据。此时我们使用稀…

飞机数据网络--ARINC 664协议

飞机数据网络主要是根据ARINC 664协议规范进行数据的计算&#xff0c;传输转换。然而ARINC 664 英文规范太过复杂&#xff0c;不易理解&#xff0c;即使是专业人员&#xff0c;也需要对其进行抽丝剥茧&#xff0c;结合实际进行理解。本文即从基础角度简单分析一下ARINC 664 应用…

【python学习】思考-如何在PyCharm中编写一个简单的Flask应用示例以及如何用cProfile来对Python代码进行性能分析

引言 Python中有两个流行的Web框架&#xff1a;Django和Flask。Django是一个高级的Python Web框架&#xff0c;它鼓励快速开发和干净、实用的设计&#xff1b;Flask是一个轻量级的Web应用框架&#xff0c;适用于小型到大型应用。以下是使用Flask创建一个简单应用的基本步骤cPro…

【书籍推荐】探索AI大语言模型的基石与边界:《基础与前沿》

本文主要介绍了AI大语言模型的基础与前沿&#xff0c;希望能对学习大模型的同学们有所帮助。 文章目录 1. 前言2. 书籍推荐 2.1 内容简介2.2 本书作者2.3 本书目录2.4 适合读者 1. 前言 全球首个完全自主的 AI 软件工程师上线&#xff0c;它是来自 Cognition 这家初创公司…

上市公司-企业数据要素利用水平(2010-2022年)

企业数据要素利用水平数据&#xff1a;衡量数字化时代企业竞争力的关键指标 在数字化时代&#xff0c;企业对数据的收集、处理、分析和应用能力成为衡量其竞争力和创新能力的重要标准。企业数据要素利用水平的高低直接影响其市场表现和发展潜力。 企业数据要素利用水平的测算…

学习记录——day17 数据结构 队列 链式队列

队列介绍 1、队列也是操作受限的线性表:所有操作只能在端点处进行&#xff0c;其删除和插入必须在不同端进行 2、允许插入操作的一端称为队尾&#xff0c;允许删除操作的一端称为队头 3、特点:先进先出(FIFO) 4、分类&#xff1a; 顺序存储的栈称为顺序栈 链式存储的队列&a…

Spring Boot+WebSocket向前端推送消息

​ 博客主页: 南来_北往 &#x1f525;系列专栏&#xff1a;Spring Boot实战 什么是WebSocket WebSocket是一种在单个TCP连接上进行全双工通信的协议&#xff0c;允许服务器主动向客户端推送信息&#xff0c;同时也能从客户端接收信息。 WebSocket协议诞生于2008年&#…

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第四十七章 字符设备和杂项设备总结回顾

i.MX8MM处理器采用了先进的14LPCFinFET工艺&#xff0c;提供更快的速度和更高的电源效率;四核Cortex-A53&#xff0c;单核Cortex-M4&#xff0c;多达五个内核 &#xff0c;主频高达1.8GHz&#xff0c;2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…

springboot旅游规划系统-计算机毕业设计源码60967

摘 要 微信小程序的旅游规划系统设计旨在为用户提供个性化的旅游规划服务&#xff0c;结合Spring Boot框架实现系统的高效开发与部署。该系统利用微信小程序平台&#xff0c;包括用户信息管理、目的地选择、行程规划、路线推荐等功能模块&#xff0c;为用户提供便捷、智能的旅…

英迈中国与 Splashtop 正式达成战略合作协议

2024年7月23日&#xff0c;英迈中国与 Splashtop 正式达成战略合作协议&#xff0c;英迈中国正式成为其在中国区的战略合作伙伴。此次合作将结合 Splashtop 先进的远程桌面控制技术和英迈在技术服务与供应链管理领域的专业优势&#xff0c;为中国地区的用户带来更加安全的远程访…

Python:对常见报错导致的崩溃的处理

Python的注释&#xff1a; mac用cmd/即可 # 注释内容 代码正常运行会报以0退出&#xff0c;如果是1&#xff0c;则表示代码崩溃 age int(input(Age: )) print(age) 如果输入非数字&#xff0c;程序会崩溃&#xff0c;也就是破坏了程序&#xff0c;终止运行 解决方案&#xf…

Java开发之Redis

1、非关系型数据库、快、高并发、功能强大 2、为什么快&#xff1f;内存单线程 非阻塞的IO多路复用有效的数据类型/结构 3、应用&#xff1a;支持缓存、支持事务、持久化、发布订阅模型、Lua脚本 4、数据类型&#xff1a; 5 种基础数据类型&#xff1a;String&#xff08;字…

html 解决tooltip宽度显示和文本任意位置换行文本显示问题

.el-tooltip__popper {max-width: 480px;white-space: break-spaces; /* 尝试不同的white-space属性值 */word-break:break-all; }