多线程进阶篇

news/2024/6/29 12:02:46 标签: java, 多线程, , CAS

多线程进阶篇

文章目录

  • 多线程进阶篇
    • 1、常见的策略
    • 2、死
      • 1) 死的三种典型情况:
      • 2) 如何解决死问题
      • 3) 死产生的必要条件
    • 3、Synchronized 采用策略
      • 1) 偏向
      • 2) 轻量级
      • 3) 其他优化操作
    • 4、CAS
      • 1) CAS的应用
        • 1. 实现原子类
        • 2. 实现自旋
      • 2) CAS 的 ABA 问题
      • 3) 解决方案
    • 5、Callable 接口
    • 6、JUC
      • 1) ReentrantLock 可重入
      • 2) 原子类的应用场景
      • 3) 信号量 Semaphore
      • 4) CountDownLatch
    • 7、集合类

1、常见的策略

这里讨论的策略不仅仅局限于 Java,此篇幅主要是认识几种常见的策略,能够知道概念。

接下来提及到的都不是某个具体的,而是抽象的概念。

描述的是的特性,描述的是“一类”。


1) 乐观 vs 悲观

二者都是对后续场景中的冲突现象进行一个预估。

乐观:预测后续的场景中,不会出现很多冲突的现象。(后续的工作会更少

悲观:预测后续的场景中,很容易出现冲突的现象。(后续会做出更多的工作来保证线程安全

冲突:两个线程尝试获取同一把,一个线程能获取成功,另一个线程阻塞等待。

冲突的概率大还是小,对后续的工作,是有一定影响的。


2) 重量级 vs 轻量级

重量级:加的开销是比较大的(花的时间多,占用系统资源多)

轻量级:加开销比较小的(花的时间少,占用系统资源少)

乐观悲观 vs 重量轻量

乐观悲观,是在加之前,对冲突概率的预测,决定工作的多少。

重量轻量,是在加之后,考量实际的的开销。

正是因为这样的概念存在重合,针对一个具体的,可能把它叫做乐观,也可能叫做轻量。但是此观点是不绝对的,反之也成立。


3) 自旋 vs 挂起等待

自旋:是轻量级的一种典型实现。

在用户态下,通过自旋的方式(while…循环),实现类似于加的效果。

这种,会消耗一定 cpu 资源,但是可以做到最快速度拿到

等待挂起:是重量级的一种典型实现。

通过内核态,借助系统提供的机制,当出现冲突的时候,会牵扯到内核对于线程的调度,是冲突的线程出现挂起(阻塞等待)

这种方式,消耗 cpu 资源更少.也就无法保证第一时间拿到


4) 读写 vs 互斥

互斥:就是简单的加(synchronized)解

读写:把读操作枷和写操作加分开了。

:是在读的时候加。 写:是在写的时候加

如果两个线程,都对读加,则不会产生竞争。(多线程并发执行的效率就更高)

如果两个线程,一个线程写加,一个线程也是写加,则会产生竞争。

如果两个线程,一个线程写加,两一个线程读加,则也会产生竞争。

实际开发中,读操作的频率,往往比读操作,高更多。

java标准库中也提供了现成的读写


5) 公平 vs 非公平

公平:遵循先来后到。(通过一定的数据结构去实现)

非公平:一拥而上,抢占式。(原生)

操作系统自带的(pthread_mutex)属于是非公平


6) 可重入 vs 不可重入

一个线程,针对同一把,连续加多次。如果产生了死,则是不可重入,如果没有产生死,就是可重入

可以按照字面意思来理解,可重入的字面意思是“可以重新进入的”,即允许同一个线程多次获取同一把

观察下面这串伪代码:

java">public synchronized void increase() {
    synchronized (this) {
        count ++;
    }
}

1.调用方法,先针对this加。 此时假设加成功了。
⒉接下来往下执行到代码块中的 synchronized ,此时,还是针对this来进行加

在不可重入

第二次 this 上的,得在 increase 方法执行完毕之后,才能释放。要想让代码继续往下执行,就需要把第二次加获取到,也就是把第一次加释放。要想把第一次加释放,又需要保证代码先继续执行。这就陷入了一个状态,程序无法执行。(这个状态是非常不合理的,第二次尝试加的时候,该线程已经有了这个的权限了,这个时候不应该加失败,不应该阻塞等待


不可重入:如果是一个不可重入。这把不会保存,是哪个线程对它加的,只要它当前处于加状态之后,收到了"加”这样的请求,就会拒绝当前加。而不管当下的线程是哪个。就会产生死

可重入:一把可重入,是会让这个保存,是哪个线程加上的。后续收到加请求之后,就会先对比一下,看看加的线程是不是当前持有自己这把的线程,这个时候就可以灵活判定了。

注:synchronized 实际上是一个可重入


可重入,是让记录了当前是哪个线程持有了,观察下面伪代码。

java">synchronized (this) {             //这个是真正加了,下面的只是虚晃一枪。
    synchronized (this) {         //判定了一下持有线程就是当前线程
        synchronized (this) {	  //同上
            …………
		}                 //执行到这个代码,出了代码块的时候,刚才加上的是否要释放??        答案是:不释放。
    }                     //如果在里层就释放了,意味着最外面的 synchronized 和次外层的代码,就没有处于的保护之中了
}

问题:如果加了 N 层,在遇到大括号时,JVM 咋知道当前这个大括号是最后一个(最外层的)呢??

答:让这里持有一个“计数器”就行了。让对象不光要记录是哪个线程持有的,同时再通过一个整形变量记录当前这个线程加了几次!!


2、死

什么是死??

是一种严重的 BUG!!导致一个程序的线程 “卡死”, 无法正常工作!

是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。


1) 死的三种典型情况:

1. 一个线程,一把,但是是不可重入。该线程针对这个连续加两次,就会出现死

java">public synchronized void increase() {
    synchronized (this) {
        count ++;
    }
}

2. 两个线程,两把。这两个线程先分别获取到一把,然后再同时尝试获取对方的

java">public class Demo1 {
    private static Object locker1 = new Object();    	 //第一把
    private static Object locker2 = new Object();    	 //第二把

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {                  	//获取第一把,成功获取。
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2) {              //获取第二把,由于 locker2 被占用,获取失败。(死
                    System.out.println("t1 两把成功!");
                }
            }
        },"t1");
        
        Thread t2 = new Thread(() -> { 
            synchronized (locker2) {				    //获取第二把,成功获取。
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker1) {  			  //获取第一把,由于 locker1 被占用,获取失败。(死
                    System.out.println("t2 两把成功!");
                }
            }
        },"t2");

        t1.start();
        t2.start();
    }
}

从 jconsole 中可以看到程序中两个线程中(t1、t2)的死。死的线程就僵住了,无法正常工作,会对程序造成严重的影响。
在这里插入图片描述


3. N个线程M把,哲学家就餐问题。

可以通过一个抽象的图来进行理解。有五个哲学家(五个线程),五根筷子(五把)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个哲学家,主要要做两件事:

  1. 思考人生。(此时会放下筷子)
  2. 吃面,会拿起左手和右手的筷子,再去夹面条吃。(拿起筷子)

其他设定:

  1. 每个哲学家,啥时候思考人生,啥时候吃面条,都很随机。
  2. 每个哲学家一旦想吃面条了,就会非常固执的完成吃面条的操作。如果此时,他的筷子被别人使用了,就会阻塞等待,而且等待过程中不会放下手里已经拿着的筷子。

基于上述的模型设定,绝大部分情况下,这些哲学家都是可以很好的工作。

但是,如果出现了极端情况,就会出现死

比如,同一时刻,五个哲学家都想吃面,并且同时伸出左手拿起左边的筷子。再尝试伸右手拿右边的筷子。

2) 如何解决死问题

解决方法:针对进行编号,并且规定加的顺序。每个线程如果要获取多把,必须先获取编号小的,后获取编号大的

利用上述办法,1 2 3 4 号哲学家分别获取到 1 2 3 4 号筷子。当 5 号哲学家开始获取筷子时,只能去获取 4 号筷子,但是 4 号筷子已经被 4 号哲学家获取到了,因此只能阻塞等待 4 号哲学家用完后释放,才能获取到。当 1 号哲学家用完 1 5 两根筷子时,1 5 均被释放,2 号就可以获取到 1 号筷子……以此类推,当 4 号哲学家释放 4 号筷子时,5号哲学家才能开始动筷。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


3) 死产生的必要条件

  1. 互斥使用:当一个线程获取到一把后,别的线程不能获取到着吧。(的基本特性)
  2. 不可抢占:只能是被持有者主动释放,而不能是被其他线程直接抢走。(的基本特性)
  3. 请求和保持:这一个线程去尝试获取多把,在获取第二把的过程中,会保持对第一把的获取状态。
  4. 循环等待:t1 尝试获取 locker2,需要 t2 执行完,释放 locker2; t2尝试获取 locker1,需要 t1 执行完,释放 locker1。

3、Synchronized 采用策略

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

synchronized 加过程:代码中写了一个 synchronized 之后,这里可能会产生一系列的"自适应的过程",升级(膨胀)

→偏向→轻量级→重量级


1) 偏向

偏向,不是真的加,而只是做了一个"标记"。如果有别的线程来竞争了,才会真的加。如果没有别的线程竞争,就自始至终都不会真的加了。

本身,有一定开销。能不加,就不加。非得是有人来竞争了,才会真的加~

偏向在没有其他人竞争的时候,就仅仅是一个简单的标记(非常轻量)。一旦有别的线程尝试进行加,就会立即把偏向,升级成真正加的状态,让别人只能阻塞等待。


2) 轻量级

synchronized 通过自旋的方式来实现轻量级

当一个线程把占用时,其它线程就会按照自旋的方式,来反复查询当前的的状态是不是被释放了。

但是,后续如果竞争这把的线程越来越多了(冲突更激烈了),从轻量升级成重量级


3) 其他优化操作

1.消除

编译器,会智能的判定当前代码是否有必要加

如果你写了加,但是实际上没有必要加,就会把加操作自动删除掉。

有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

java">StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加和解.。但如果只是在单线程中执行这个代码,那么这些加操作是没有必要的,白白浪费了一些资源开销。

2.粗化

关于"的粒度",如果加操作里包含的实际要执行的代码越多,就认为的粒度越大。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有的时候,希望的粒度小比较好,并发程度更高

有的时候,也希望的粒度大比较好 (因为加本身也有开销).


CAS_369">4、CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“。

能够比较和交换某个寄存器中的值和内存中的值,看是否相等。如果相等,则把另外一个寄存器中的值和内存进行交换。

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 address 与 expectValue 是否相等。(比较)
  2. 如果比较相等,将 swapValue写入 address。(交换)
  3. 返回操作是否成功。

CAS 伪代码

java">boolean CAS(address, expectValue, swapValue) {  //判断 address 与 expectValue 是否相等,若相等则将 swapValue                                                   写入 address
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

CAS_395">1) CAS的应用

1. 实现原子类

比如,多线程针对一个 count 变量进行 ++。在 java 标准库中,已经提供了一组原子类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于CAS又能衍生出一套"无编程",进一步提高代码运行效率。

这里面提供了 自增/自减/自增任意值/自减任意值,这些操作,就可以基于 CAS编程的方式来实现。

上述的原子类,就是基于 CAS 来实现的。

java">//伪代码实现
class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

假设两个线程同时调用 getAndIncrement ,同时假设 value 是 0.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

CAS 中比较 value 和 oldValue 是否相等时,其实就是在检查当前 value 的值是不是变了。是不是被别的线程穿插进来做出修改了!!进一步就发现了当前的 ++ 操作不是一气呵成的原子操作了,一旦发现出现其他线程穿插的情况,立即重新读取内存的值准备下一次尝试~~

当两个线程并发的去执行++操作的时候,如果不加任何限制,就意味着,有时候,这俩++是串行的,能计算正确的。有的时候这俩++操作是穿插的,这个时候是会出现问题的。可以通过加保证线程安全:通过,强制避免出现穿插~~
原子类/CAS保证线程安全:借助CAS来识别当前是否出现“穿插"的情况,如果没穿插,此时直接修改,就是安全的。如果出现穿插了,就重新读取内存中的最新的值,再次尝试修改。

2. 实现自旋

基于 CAS 实现更灵活的, 获取到更多的控制权.

java">public class SpinLock {
    private Thread owner = null;     //用owner表示当前线程持有的,null为解状态。
    public void lock(){
        // 通过 CAS 看当前是否被某个线程持有. 
        // 如果这个已经被别的线程持有, 那么就自旋等待. 
        // 如果这个没有被别的线程持有, 那么就把 owner 设为当前尝试加的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
        //获取当前线程引用.哪个线程调用lock,这里得到的结果就是哪个线程的引用!
       }
   }
   
    public void unlock (){
        //当该已经处于加状态,这里就会返回false, cas不会进行实际的交换操作.接下来循环条件成立,继续进入下一轮循环.
        this.owner = null;
   }
}

CAS__ABA__465">2) CAS 的 ABA 问题

上面讲到了,CAS 的关键要点,是比较 寄存器1 和 内存 中的值,通过这里的是否相等,来判定内存的值是否发生变化。

如果内存的值发生变化,则存在其他线程进行了修改。如果内存的值没有发生变化,则没有别的线程修改,接下来进行的修改就是安全的。

但是我们要想到一个问题,如果这里的值没变,就一定没有别的线程进行修改吗?

ABA 问题就描述了另一个线程,把变量的值从A->B,又从B->A。此时本线程区分不了,这个值是始终没变,还是出现变化又回来了的情况。

大部分情况下,就算是出现 ABA 问题,也没啥太大影响。但是在一些比较极端情况下,还是会出现问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虽然上述操作,概率比较小,也需要去考虑。

ABA 问题,CAS 基本的思路是 ok 的,但是主要是修改操作能够进行反复横跳,就容易让咱们 CAS 的判定失效。


3) 解决方案

我们也有相应的解决办法,可以给上述案例中的账户余额安排一个隔壁邻居 ——— 版本号

给要修改的值,引入版本号。 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期。

  • CAS 操作在读取旧值的同时, 也要读取版本号.
  • 真正修改时
    • 如果当前版本号和读到的版本号相同,则修改数据,并把版本号 + 1。
    • 如果当前版本号高于读到的版本号。就操作失败(认为数据已经被修改过了)。

在 Java 标准库中提供了 AtomicStampedReference<E> 类。这个类可以对某个类进行包装,在内部就提供了上面描述的版本管理功能。

5、Callable 接口

Callable 是一个 interface。相当于把线程封装了一个 “返回值”。方便程序猿借助多线程的方式计算结果。

Callable interface 也是创建线程的一种方式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果进行多线程操作,如果你只是关心多线程执行的过程,使用 Runnable 即可。(只关心过程)

如果是关心多线程的计算结果,使用Callable更合适。(比如说通过多线程,计算一个公式,返回结果)

java">import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        Integer result = futureTask.get();
        System.out.println(result);
    }
}

使用 Callable 不能直接作为 Thread 的构造方法参数。而是需要用到 FutureTask 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面 Callable 线程结果啥时候能算出来??这是最关心的一点。使用 futureTask 就可以帮助咱们解决这个问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

获取 call 方法的返回结果。get 类似于 join 一样,如果 call 方法没执行完,会阻塞等待。

6、JUC

Juc (java.util.concurrent) 的常见类也是并发编程。

1) ReentrantLock 可重入

这个,没有 synchronized 那么常用,但是也是一个可选的加的组件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ReentrantLock 具有一些特点,是 synchronized 不具备的功能。

  1. 提供了一个tryLock方法进行加。对于lock操作,如果加不成功,就会阻塞等待(死等)。对于tryLock,如果加失败,直接返回false/也可以设定等待时间。tryLock给加操作提供了更多的可操作空间~~
  2. ReentrantLock有两种模式。可以工作在公平状态下,也可以工作在非公平的状态下。构造方法中通过参数设定的公平/非公平模式
  3. ReentrantLock 也有等待通知机制。搭配Condition 这样的类来完成。这里的等待通知要比 wait notify功能更强。这几个是ReentrantLock的优势~~

synchronized 对象是任意对象。ReentrantLock 对象就是自己本身。如果你多个线程针对不同的 ReentrantLock 调用 lock 方法,此时是不会产生竞争的。


2) 原子类的应用场景

  1. 计数需求

    播放量、点赞量、投币量、转发量、收藏量等……

    同一个视频,有很多人同时播放、点赞、收藏……

  2. 统计效果

    统计出现错误的请求数量。使用原子类,记录出错的请求的数目

3) 信号量 Semaphore

semaphore 是并发编程中的一个重要组件。它可以用来控制同时访问某个资源的线程数量。Semaphore维护了一个许可证集合,线程在访问资源前必须先获取许可证,如果许可证已经全部被占用,则线程必须等待其他线程释放许可证后才能获取许可证并访问资源。

准确来说,Semaphore 是一个计数器(变量),描述了“可用资源的个数”。

描述当前线程,是否“有临界资源可以使用”。(多个线程修改同一个变量,这个变量就可以认为是临界资源)

acquire 方法表示申请资源(P操作),release 方法表示释放资源(V操作)。

java">import java.util.concurrent.Semaphore;

// 信号量
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        // 构造方法中, 就可以用来指定计数器的初始值.
        Semaphore semaphore = new Semaphore(4);   //申请 4 个资源
        semaphore.acquire();            // 计数器 -1
        System.out.println("执行 P 操作 1");
        
        semaphore.acquire();            // 计数器 -1
        System.out.println("执行 P 操作 2");
        
        semaphore.acquire();            // 计数器 -1
        System.out.println("执行 P 操作 3");
        
        semaphore.acquire(); 			// 计数器 -1
        System.out.println("执行 P 操作 4");        //到此为止所有资源已占用完,如果再申请资源则阻塞等待。
        
        semaphore.release();			// 计数器 +1
        System.out.println("执行 V 操作 1");
        
        semaphore.acquire(); 			// 计数器 -1
        System.out.println("执行 P 操作 5");
    }
}

打印结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4) CountDownLatch

同时等待 N 个任务执行结束。

好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

当需要把一个任务拆成多个任务,如何衡量现在是把多个任务都搞定了呢?这时候就需要用到 CountDownLatch.

java">import java.util.concurrent.CountDownLatch;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        // 构造方法中, 指定创建几个任务.
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(() -> {
                System.out.println("线程" + id + "开始工作!");
                try {
                    // 使用 sleep 代指某些耗时操作, 比如下载.
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + id + "结束工作!");
                // 每个任务执行结束这里, 调用一下方法
                // 把 10 个线程想象成短跑比赛的 10 个运动员. countDown 就是运动员撞线了.
                countDownLatch.countDown();
            });
            t.start();
        }

        // 主线程如何知道上述所有的任务都完成了呢??
        // 难道要在主线程中调用 10 次 join 嘛?
        // 万一要是任务结束, 但是线程不需要结束, join 不就也不行了嘛?
        // 主线程中可以使用 countDownLatch 负责等待任务结束.
        // a => all 等待所有任务结束. 当调用 countDown 次数 < 初始设置的次数, await 就会阻塞.
        countDownLatch.await();
        System.out.println("多个线程的所有任务都执行完毕了!!");
    }
}

7、集合类

原来的集合类, 大部分都不是线程安全的。

Vector,Stack,HashTable,是线程安全的(不建议用),其他的集合类不是线程安全的。

1) 多线程环境使用 ArrayList

  1. 自己使用同步机制 (synchronized 或者 ReentrantLock)

  2. Collections.synchronizedList(new ArrayList);

  3. 使用 CopyOnWriteArrayList

2) 多线程环境使用队列

  1. ArrayBlockingQueue

    基于数组实现的阻塞队列

  2. LinkedBlockingQueue

    基于链表实现的阻塞队列

  3. PriorityBlockingQueue

    基于堆实现的带优先级的阻塞队列

  4. TransferQueue

    最多只包含一个元素的阻塞队列

3) 多线程环境使用哈希表

HashMap 本身不是线程安全的.

多线程环境下使用哈希表可以使用:

  • Hashtable
  • ConcurrentHashMap

http://www.niftyadmin.cn/n/5105185.html

相关文章

基于RuoYi-Flowable-Plus的若依ruoyi-nbcio支持自定义业务表单流程的集成方法与步骤(二)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 前面讲了集成的后端部分内容&#xff0c;下面简单介绍一下前端的内容 1、前端生成的页面需要进行修改&…

基于ESP32-WROOM-32的USB转WIFI模块设计

一、ESP32-WROOM-32简介&#xff1a; ESP32-WROOM-32是一个功能强大的通用Wi-FiBTBLE MCU模块&#xff0c;针对多种应用&#xff0c;从低功耗传感器网络到最苛刻的任务&#xff0c;如语音编码、音乐流和MP3解码。该模块的核心是ESP32-D0WDQ6芯片*。嵌入式芯片的设计是可扩展的和…

水库大坝安全监测方案,筑牢水库安全防线!

方案背景 党的十九届五中全会提出&#xff1a;“统筹发展和安全、加快病险水库除险加固”&#xff1b;国务院常务会议明确“十四五”期间&#xff0c;水库除险加固和运行管护要消除存量隐患&#xff0c;实现常态化管理&#xff1b;到2025年前&#xff0c;完成新出现病险水库的…

分类预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost多输入分类预测

分类预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost多输入分类预测 目录 分类预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost多输入分类预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.MATLAB实现基于BiLSTM-…

开源/免费的scrum工具:敏捷项目管理的选择

有许多开源和免费的敏捷管理工具可供敏捷团队使用&#xff0c;以支持他们的敏捷项目管理和开发需求。以下是一些常见的开源/免费敏捷管理工具&#xff1a; 免费敏捷工具 以下是一些免费的敏捷工具&#xff0c;这些工具提供了一定的功能&#xff0c;可用于支持敏捷项目管理和开…

搭建Pytorch的GPU环境超详细

效果 1、下载和安装VS2019 https://visualstudio.microsoft.com/zh-hans/vs/older-downloads/ 登录需要用户名和密码 安装后需要联网下载组件的,安装的时候要勾选使用C++的桌面开发 2、下载和安装显卡驱动 查看自己的显卡型号 从英伟达下载和安装最新驱动

AAOS CarMediaService 服务框架

文章目录 前言MediaSessionCarMediaService作用是什么&#xff1f;提供了哪些接口&#xff1f;如何使用&#xff1f;CarMediaService的实现总结 前言 CarMediaService 是AAOS中统一管理媒体播放控制、信息显示和用户交互等功能的服务。这一服务依赖于android MediaSession框架…

Python数据挖掘实用案例——自动售货机销售数据分析与应用

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;陈童学哦&#xff0c;目前学习C/C、算法、Python、Java等方向&#xff0c;一个正在慢慢前行的普通人。 &#x1f3c0;系列专栏&#xff1a;陈童学的日记 &#x1f4a1;其他专栏&#xff1a;CSTL&…