Java 线程池

线程基础知识回顾

  • 在 JVM 中用户线程结束了 ,JVM 就会关闭,并不会管守护线程
  • 线程 setPriority() 的范围是 1-10 ,线程默认优先级是5
  • 以前停止线程用的是 stop() ,现在不建议使用了。现在要停止线程,使用的是 interrupt() 和 isInterrupted()

线程的生命周期:新建、就绪、运行、阻塞、死亡
线程的几种状态(点开Thread类,有一个叫 State 的枚举,可以看见这几个状态):NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING

线程的生命周期和状态对应关系如下图:

在这里插入图片描述

线程池

什么是线程池?

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处:
① 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
② 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
③ 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、 调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

线程池有什么作用?

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。

如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜),况且我们还不能控制线程池中线程的开始、挂起、和中止

线程池的体系结构:

  • java.util.concurrent.Executor 线程池的顶级接口定义了线程池的最基本方法
    • java.util.concurrent.ExecutorService 定义常用方法
      • java.util.concurrent.ThreadPoolExecutor 线程池的核心实现类
        • java.util.concurrent.ScheduledThreadPoolExecutor 在核心实现类上扩展了定时和周期性的功能
  • java.util.concurrent.Executors 线程池的工具类

他们的对应关系如下图:
在这里插入图片描述
现在我们大概就能知道这些类之间的关系了

从上可以看出,Executor 是线程池最基本的接口。他的实现如下,只定义了一个 execute() 方法,也就是说,我们线程池最核心的功能就是执行任务

public interface Executor {
    void execute(Runnable command);
}

但是我们都不是用的 Executor 接口(他就像 Collection 一样,我们一般也用的 Collection 的子接口)

然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;

然后ThreadPoolExecutor继承了类AbstractExecutorService。


在ThreadPoolExecutor类中有几个非常重要的方法:

execute()
submit()
shutdown()
shutdownNow()

execute() 方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit() 方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。

shutdown()shutdownNow() 都是用来关闭线程池的。区别就是:
executor.shutdown():等待任务队列所有的任务执行完毕后才关闭
executor.shutdownNow():立刻关闭线程池

还有很多其他的方法:

比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友可以自行查阅API。

以上就是对线程池的概述,现在我们进入线程池的重点内容—线程池的核心实现类ThreadPoolExecutor的讲解

线程池的 API 使用起来非常简单,三步就可以了,如下

有两种方法可以创建线程池,但是我们通常使用下面这种,即new ThreadPoolExecutor();

还有一种就是通过线程池的工具类的四种方法来创建线程池

//1. 创建一个线程池
ExecutorService es=new ThreadPoolExecutor();
//2. 执行我们想要被执行的任务
es.execute();
//3. 关闭线程池
es.shutdown();

那我们先来谈谈第一种创建线程池的方法:new ThreadPoolExecutor();

因为其实通过工具类来创建线程池,最后也是调用了这个方法

先来看下 ThreadPoolExecutor的七大参数

线程池的七大参数

面试的时候,我们着重了解一下线程池的原理 和 ThreadPoolExecutor()各个参数的含义

首先来说线程池的工作原理,ThreadPoolExecutor 中这7个参数是很重要的,面试常见

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)


下面解释下一下构造器中各个参数的含义

  • int corePoolSize

    核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

  • int maximumPoolSize

    线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程。只有在workQueue 也满了的时候,才会创建除了 corePoolSize 以外的线程,当大于 maximumPoolSize 的时候就要使用线程池的拒绝策略 RejectedExecutionHandler

  • BlockingQueue workQueue

    用来暂时保存任务的工作队列 。当大于corePoolSize 的时候,任务就暂时放在这个队列里面,等 corePoolSize空闲的时候取出去执行,当这个队列满了的时候就会触发 maximumPoolSize。一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

    1. SynchronousQueue:
      SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
      使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作。
    2. LinkedBlockingQueue:
      LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。
    3. ArrayBlockingQueue:
      ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。

    这三种队列的区别,主要就是SynchronousQueue只能有一个任务,ArrayBlockingQueue只能有固定数量个任务,LinkedBlockingQueue可以有无数个任务。ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。

  • RejectedExecutionHandler handler

    当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和时(达到了最大线程池的大小且工作队列已满),execute()方法将要调用的拒绝策略,有以下四种取值【四大拒绝策略】:

    //1. 丢弃任务并抛出RejectedExecutionException异常
    ThreadPoolExecutor.AbortPolicy
    //2. 也是丢弃任务,但是不抛出异常
    ThreadPoolExecutor.DiscardPolicy
    //3. 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    ThreadPoolExecutor.DiscardOldestPolicy
    //4. 由调用线程处理该任务 
    ThreadPoolExecutor.CallerRunsPolicy
    
  • long keepAliveTime

    表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

  • ThreadFactory threadFactory

    线程工厂,主要用来创建线程,一般不管这个参数,使用默认值;

  • TimeUnit unit

    参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

    TimeUnit.DAYS;               //天
    TimeUnit.HOURS;             //小时
    TimeUnit.MINUTES;           //分钟
    TimeUnit.SECONDS;           //秒
    TimeUnit.MILLISECONDS;      //毫秒
    TimeUnit.MICROSECONDS;      //微妙
    TimeUnit.NANOSECONDS;       //纳秒
    

Executor 相当于集合里面的 Collection
Executors 相当于集合里面的 Collections
类比集合,让我们看看线程池的这个工具类 — Executors,另一种创建线程池的方法

线程池的工具类

除了上面通过ThreadPoolExecutor 创建线程池,我们还可以通过线程池的工具类创建线程池,但是阿里不推荐这种创建线程池的方法,最好用上面的那张方法创建线程池,阿里规范这样讲:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:

  1. FixedThreadPoolSingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
  2. CachedThreadPoolScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

Executors是线程池的工具类,提供了四种快捷创建线程池的方法:

newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,核心线程数是0,最大线程数是Integer.MAX_VALUE,使用的SynchronousQueue队列。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }

newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

newScheduledThreadPool
创建一个定长线程池,支持定时周期性任务执行。

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

案例演示:

/**
     * 创建一个定长线程池,支持延迟及周期性任务执行。延迟执行示例代码如下
     */
    public void fun4(){
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
        // 周期性执行任务(任务会执行多次)
        // 参数1:任务   参数2:延迟时间   参数3:每隔长时间   参数4:时间单位
        newScheduledThreadPool.scheduleAtFixedRate(()-> System.out.println("要执行的任务"), 3, 2, TimeUnit.SECONDS);
        // 延迟执行任务(任务执行一次)
        // 参数1:任务   参数2:延迟时间   参数3:时间单位
        newScheduledThreadPool.schedule(()->System.out.println("要执行的任务"), 3,TimeUnit.SECONDS);
    }

线程池的工作流程

  • 流程1 判断核心线程数

  • 流程2 判断任务能否加入到任务队列

  • 流程3 判断最大线程数量

  • 流程4 根据线程池的拒绝策略处理任务

从源码角度深入理解线程池原理

private final HashSet<Worker> workers = new HashSet<Worker>();

我们所说的线程池其实指的就是他,这个 workers。他就是存放工作线程的集合

Worker 代表一个工作线程,是ThreadPoolExecutor的一个内部类

// Worker工作者 代表一个工作线程
	// Worker 是线程池的内部类, 它实现了Runnable接口 
   private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
    // Worker中的两个重要属性:
		// 很重要的参数,说 Worker 是个工作线程,指的就是这个thread
        final Thread thread; // 具体的工作线程
        Runnable firstTask; //创建worker时,传入第一次要运行的任务【线程是可以复用的,这里指第一次使用】
        
        volatile long completedTasks;
        //Worker 的构造器:
        Worker(Runnable firstTask) {
            setState(-1); 
            this.firstTask = firstTask;// 创建worker时,传入第一次要运行的任务
            // 通过线程池工厂创建了一个线程
            /*
            newThread(this) 这个 this 指的是Worker,说明只要Thread线程
            一启动,就会调用Worker的 run 方法【Worker实现了Runnable接口】
            ,先看下下面的run方法
            */
            this.thread = getThreadFactory().newThread(this);
        }

        public void run() {
        	// 看下面的 runWorker 方法
            runWorker(this);
        }

        protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

runWorker() 方法:

 // 通过这个方法,你就会知道为什么线程执行完当前任务,还会执行下一个任务
  final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        // 第一次要执行的任务
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
        /*这个 while 循环就是线程复用的原因,只要 task 不为null或者
        getTask()还能获得任务,就会继续执行循环
        getTask() 是从哪里取得任务呢?看下面的getTask源码……
        */
            while (task != null || (task = getTask()) != null) {
                w.lock();

                //这段if 是在判断线程池的状态的
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                    //注意这个run方法
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                // 清空已经执行完的任务
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

getTask() 方法:

// Worker线程启动后,会不断的使用getTask()方法获取任务执行
    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
            /* 看到这个workQueue 了吗? getTask()就是从workQueue 
            取出来的任务,就是我们前面讲过的任务队列
            */

			// poll 和 take 是两种取任务的方法
                Runnable r = timed ?
  			/*
  			poll 不是阻塞的方法,一般用于临时线程【>coreSize && <=MaxSize的那些线程】,
  			超过keepAliveTime的时间就停止获取任务了
  			*/
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
			// 是阻塞的,用于核心线程获取任务
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

大概看完上面的Worker,了解到这个是线程池工作的一个核心的类,Worker里面有一个Thread线程,这个Thread运行的时候,会调用 Worker 的run() 方法,这个run方法里面的那个while 循环就是实现线程复用的关键,while (task != null || (task = getTask()) != null) ,getTask() 是从工作队列【workQueue】里面获取任务的一个方法,也就是说只要工作队列还有任务,while 循环就不会退出。

在getTask()方法里面,我们又看到了线程获取任务的两种方法,poll 和 take ,前者是非阻塞的,针对临时线程,后者是阻塞的,用于核心线程,因为临时线程超过keepAliveTime的时候,线程就会被销毁,而核心线程可以一直活着,一直等,等工作队列来任务

大概了解了Worker 之后,思考:Worker什么时候会被创建呢?创建完了之后,他里面的Thread 什么时候会被启动呢?

让我们分析一下 ThreadPoolExecutor 里面的execute方法,你可能就明白了

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
	
		// AtomicInteger ctl 是一个integer的原子类对象 
		// 主要作用: 1.记录线程池的状态信息  2.记录线程池工作线程的数量
        int c = ctl.get();
        
        // workerCountOf(c) : 工作线程的数量
		// isRunning(c): 线程池是否运行状态
		
        // 流程1: 工作线程的数量如果小于核心线程的数量,就添加一个worker。
        //看完这个方法再去分析addWorker方法 
        /*
        在addWorker方法里面其实就是创建了一个Worker,就是再多增加条线程吧
        addWorker方法 参数1:要执行的任务   参数2: true代表添加核心线程
        */
        if (workerCountOf(c) < corePoolSize) {
        // 第二个参数为true,代表添加的是核心线程,否则添加的是临时线程
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 流程2: 尝试向任务队列workQueue中添加一个任务
        if (isRunning(c) && workQueue.offer(command)) {
        // 对线程池的状态做一个重新检查,避免刚添加完任务,线程池就被关闭了
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 流程3: 添加一个worker, 参数1: 要执行的任务  参数2:false代表添加临时线程
        else if (!addWorker(command, false))
        // 流程4: 线程池已饱和无法在创建新的worker,执行饱和策略
            reject(command); // 调用具体的拒绝策略
    }

总结一下这个execute() 方法就是:
① 判断这个任务是不是null,为null 就抛出异常
② 判断当前线程池中的线程数目是不是小于核心线程数,如果是,就再添加一个线程【或者说再创建一个Worker】
③ 在②不满足的时候,也就是说当超过核心线程数目的时候,再来的任务应该放在workQueue中
④ 当workQueue 也放不下的时候,就创建临时线程。else if (!addWorker(command, false)) ,就是说临时线程也不能创建成功的时候,就执行拒绝策略

再来看看上面多次执行的addWorker()方法是如何创建worker的:

// boolean 值为true,代表添加的是一个核心线程,否则,添加的是一个临时线程
    private boolean addWorker(Runnable firstTask, boolean core) {
 	.....删掉了一部分代码
            for (;;) {
            //wc 取出当前工作线程的数量
                int wc = workerCountOf(c);
                // core=true 判断是否大于等于 corePoolSize
                // core=false 判断是否大于等于 maximumPoolSize
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
		
		// 从这里开始准备创建Worker
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
        // 通过构造器得到worker对象
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //把新的Worker添加到workers集合中【workers是个HashSet】
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        //添加成功的标志设置为true 
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                //如果新的Worker添加成功,启动该worker中的线程
                /* 这个t 就是work 里面的线程,也就是说,在addWork()
                方法里面,Worker被创建呢,Thread 会被启动
                【回答了上面提出来的问题】
                */
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

记录一篇写的很好的文章:
https://www.cnblogs.com/dolphin0520/p/3932921.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值