Java-多线程-ThreadPoolExecutor

news2025/1/11 2:51:37

前言

前面我们讲解线程的时候,讲到了使用Executors创建线程池,但是它里面所有方法可变的参数太少,不能很好的进行自定义设置,以及以后的扩展,更合理的使用cpu线程的操作,所以使用ThreadPoolExecutor创建线程池是最好的使用方式

1、快速使用

public static void main(String[] args) throws Exception{
    // 创建线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,
        3,
        500,
        TimeUnit.MINUTES,
        new LinkedBlockingQueue<>(),
        new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("我的线程池-");
                return thread;
            }
        },
        new ThreadPoolExecutor.AbortPolicy()
    );

    // execute用于执行没有返回值的实现了Runnable的方法
    // 还有一个submit方法可以执行Runnable和Callable的有返回值的方法
    executor.execute(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    });

    // 记得一定要关闭线程池,最好放在finally里面,线程执行用try包裹住
    executor.shutdown();
}

2、参数讲解

在这里插入图片描述

使用ThreadPoolExecutor创建线程池,只能使用这一个有参构造创建,有7个参数,下面进行讲解

  • int corePoolSize:核心线程池的大小
  • int maximumPoolSize:最大线程池的大小
  • long keepAliveTime:非核心线程池的空闲时间
  • TimeUnit unit:存活时间的单位
  • BlockingQueue orkQueue:阻塞/工作队列,常用的有
    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • SynchronousQueue
    • 在这里插入图片描述
  • ThreadFactory threadFactory:线程池里面线程的线程创建工厂
  • RejectedExecutionHandler handler:拒绝策略,有四种
    • AbortPolicy:如果超出最大线程数+队列排队限制数,则抛出异常
    • CallerRunsPolicy:如过超出,则由创建线程池的线程来执行方法
    • DiscardOldestPolicy:如果超出,则将队列中最前面等待的线程任务弹出,当前线程从排队序列后面加入进去
    • DiscardPolicy:不做任何处理,超出则不执行线程,不做任何操作

在这里插入图片描述

3、源码解析

3.1、线程池的五种状态

RUNNING(111):正常运行中

SHUTDOWN(000):关闭线程池,不接受新任务。线程池内队列以及正在执行的任务会正常执行完毕

STOP(001):关闭线程池,不接受新任务。线程池内所有线程也强制关闭

TIDYING(010):过度状态

TERMINATED(011):线程池凉凉

// 核心线程池的数量,int类型,使用了原子类进行加减
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 32-3 = 29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 1<<29:00100000 00000000 00000000 00000000
// -1:	 00011111 11111111 11111111 11111111	
// 000代表线程池的状态,后面代表线程池的容量
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// -1<<29:111.....后面不管	RUNNING:111
private static final int RUNNING    = -1 << COUNT_BITS;
// 0<<29:000...	SHUTDOWN:000
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// 1<<29:001...	STOP:001
private static final int STOP       =  1 << COUNT_BITS;
// 2<<29:010... TIDYING:010
private static final int TIDYING    =  2 << COUNT_BITS;
//3<<29:011... TERMINATED:011
private static final int TERMINATED =  3 << COUNT_BITS;

3.2、线程池的状态切换流程

在这里插入图片描述

3.3、execute方法

在将来某个时候执行给定的任务。任务可以在新线程或现有的合并的线程中执行。如果任务无法提交执行,由于此执行程序已关闭或已达到其容量,该任务将由当前的RejectedExecutionHandler处理。

public void execute(Runnable command) {
    // 健壮性判断
    if (command == null)
        throw new NullPointerException();

    // 获取crl的数量
    int c = ctl.get();

    // 工作线程的个数是否小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 通过add方法,添加一个核心线程去执行command任务
        if (addWorker(command, true))
            // 添加核心线程成功,返回true,直接return结束
            return;
        // 如果在并发情况下,添加核心线程失败的线程,需要重新获取一次ctl属性
        c = ctl.get();
    }
    // 创建核心线程失败的情况
    // 判断当前线程池状态是否为RUNNING
    // 如果是RUNNING,执行offer方法将任务添加到工作队列中
    if (isRunning(c) && workQueue.offer(command)) {
        // 添加任务到工作队列成功
        int recheck = ctl.get();
        // 判断线程池是否是RUNNING状态,如果不是RUNNING状态,需要将任务从工作队列移除
        if (! isRunning(recheck) && remove(command))
            // 执行拒绝策略(线程池状态不正确)
            reject(command);
        // 判断工作线程是否为0
        else if (workerCountOf(recheck) == 0)
            // 工作线程数为0,但是工作队列中有任务在排队
            // 添加一个非核心空任务的线程,为了处理在工作队列排队的任务
            addWorker(null, false);
    }
    // 添加任务到工作队列失败,添加非核心线程去执行当前任务
    else if (!addWorker(command, false))
        // 添加非核心线程失败,执行reject拒绝策略
        reject(command);
}

3.4、addWorker方法

boolean addWorker(Runnable firstTask, boolean core):第一个参数为线程任务,第二个参数为创建核心/非核心线程的标识

private boolean addWorker(Runnable firstTask, boolean core) {
    // 对线程池的判断,以及对工作线程数量的判断
    // 设置标签,跳出外层循环
    retry:
    for (;;) {
        // 获取ctl的值
        int c = ctl.get();
        // 拿到线程池的状态
        int rs = runStateOf(c);

        // 如果线程池状态不是RUNNING,就再次做后续判断,查看当前任务是否可以不处理
        if (rs >= SHUTDOWN &&
            // 线程池状态为SHUTDOWN,并且任务为空,并且工作队列不为空
            // 如果同时满足了这三个要求,那就是要处理工作队列当前中的任务
            ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
            // 只要不是RUNNING状态,不处理新任务
            // 如果SHUTDOWN状态,并且满足了之前addWorker(null,false),并且工作队列有任务时,不能走当前位置
            return false;

        for (;;) {
            // 基于ctl获取当前工作线程数量
            int wc = workerCountOf(c);
            // 判断工作线程数是否大于最大值
            if (wc >= CAPACITY ||
                // 如果是核心线程,是否大于设置的corePoolSize,如果是非核心线程,是否大于maximunPoolSize
                wc >= (core ? corePoolSize : maximumPoolSize))
                // 当前工作线程已经达到最大值了
                return false;
            // 以CAS的方式,对工作线程数+1,如果成功
            if (compareAndIncrementWorkerCount(c))
                // 直接跳出外层for循环
                break retry;
            // 重新获取ctl的值
            c = ctl.get();
            // 基于新获取的ctl拿到线程池状态,判断和之前的rs状态是否一直
            if (runStateOf(c) != rs)
                // 说明并发操作导致线程池状态变化,需要重新判断状态
                continue retry;
        }
    }

    // 添加工作线程,并启动工作线程
    // 工作线程是否启动了
    boolean workerStarted = false;
    // 工作线程是否添加了
    boolean workerAdded = false;
    // Worker就是工作线程
    Worker w = null;
    try {
        // new Worker勾线工作线程,将任务扔到了Worker中
        w = new Worker(firstTask);
        // 拿到了Worker中绑定的Thread线程
        final Thread t = w.thread;
        // 肯定部位null,健壮性判断
        if (t != null) {
            // 加锁。。。
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // 基于重新获取的ctl,拿到线程池的状态
                int rs = runStateOf(ctl.get());
				// 如果满足线程池状态为RUNNING,就添加工作任务
                if (rs < SHUTDOWN ||
                    // 如果线程池状态为SHUTDOWN,并且传入的任务为null
                    (rs == SHUTDOWN && firstTask == null)) {
                    // 开始添加工作线程
                    // 判断当前线程是否处于run状态(健壮性判断)
                    if (t.isAlive())
                        throw new IllegalThreadStateException();
                    // 将构件好的Worker对象添加到了workers
                    workers.add(w);
                    // 获取工作线程个数
                    int s = workers.size();
                    // 如果现在的工作线程数,大于历史最大的工作线程数,就重新赋值给largestPoolSize
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

3.5、Worker对象

private final class Worker
    extends AbstractQueuedSynchronizer // 线程中断
    implements Runnable // 存储需要执行的任务
{

    // 工作线程的Thread对象
    final Thread thread;
    // 需要执行的任务
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;

    
    Worker(Runnable firstTask) {
        // 刚刚初始化的工作线程不允许被中断
        setState(-1); 
        // 第一次new的时候,会将任务赋值给firstTask
        this.firstTask = firstTask;
        // Worker构建Thread对象
        this.thread = getThreadFactory().newThread(this);
    }

    // 调用t.start(),执行当前的run方法
    public void run() {
        runWorker(this);
    }

    // Lock methods
    //
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.

    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) {
            }
        }
    }
}

3.6、runWorker方法

用于启动Worker对象中的线程

final void runWorker(Worker w) {
    // 拿到当前工作线程
    Thread wt = Thread.currentThread();
    // 拿到Worker对象中封装的任务
    Runnable task = w.firstTask;
    // 将Worker的firstTask归为null
    w.firstTask = null;
    // 将Worker的state归为0,代表可以被中断
    w.unlock();
    // 执行任务时,钩子函数中是否出现异常的标识
    boolean completedAbruptly = true;
    try {
        // 获取任务的第一个方式,就是执行execute、submit时,传入的任务直接处理
        // 获取任务的第二个方式,就是从工作队列中获取任务执行
        while (task != null || (task = getTask()) != null) {
            // 加锁,在SHUTDOWN状态下,当前线程不允许被中断
            // 并且Worker内部实现的锁,并不是可重入锁,因为在中断时,也需要对worker进行lock,不能获取就代表当前工作线程正在执行任务
            w.lock();
            // 如果线程池状态变为了STOP状态,必须将当前线程中断
            // 第一个判断:判断当前线程池状态是否是STOP
            // 第二个判断:查看中断标记为,并归位,如果为false,说明不是STOP,如果变为true,需要再次查看是否并发操作导致线程池为STOP
            if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) &&
                // 查询当前线程中断标记是否为false,如果为false,就执行wt.interrupt()
                !wt.isInterrupted())
                // 将中断标记设置为true
                wt.interrupt();
            try {
                // 执行任务的钩子函数
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 执行任务
                    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
                task = null;
                // 执行成功的任务个数+1
                w.completedTasks++;
                // 将state标记位设置为0
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

3.7、getTask方法

从工作队列中获取任务

private Runnable getTask() {
    // 表示(非核心线程可以干掉)
    boolean timedOut = false; 

    for (;;) {
        // 获取ctl表示
        int c = ctl.get();
        // 拿到了线程池的状态
        int rs = runStateOf(c);

        // 如果进入if,需要干掉当前工作线程
        // 线程池状态为SHUTDOWN、STOP
        // 如果线程池状态大于等于STOP、需要移除掉当前工作线程
        // 如果线程池状态为SHUTDOWN、并且工作队列为空,需要移除掉当前工作线程
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            // 移除当前工作线程
            decrementWorkerCount();
            return null;
        }

        // 重新获取工作线程的个数
        int wc = workerCountOf(c);

        // allowCoreThreadTimeOut:是否允许核心线程超时(一般为false)
        // 工作线程是否大于核心线程
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if (
            // 工作线程是否已经大于最大线程数了
            // 工作线程数大于核心线程数,并且当前线程已经超时
            // 尝试干掉当前线程
            (wc > maximumPoolSize || (timed && timedOut))
            // 工作线程数大于1、或者工作队列为空
            // 如果工作队列为空,我就干掉我自己
            // 如果工作线程数大于1,我就干掉我自己
            && (wc > 1 || workQueue.isEmpty())) {
            // 基于CAS的方式移除掉当前线程,只有一个线程会CAS成功
            if (compareAndDecrementWorkerCount(c))
                // 返回null,交给processWorkerExit移除当前工作线程
                return null;
            continue;
        }
        
        // ==================从工作队列获取任务=============================
        try {
            Runnable r = timed ?
                // 阻塞一定时间从工作队列拿任务(可以理解为非核心走这个方法)
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                // 一直阻塞!(可以理解为核心线程走这个)
                workQueue.take();
            if (r != null)
                // 如果拿到任务直接返回执行。。
                return r;
            // 从队列获取任务时,超时了(达到了当前工作线程的最大生存时间)
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

3.8、processWorkerExit方法

移除当前工作线程

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 如果执行processWorkerExit方法的操作不是getTask中的操作,是直接因为异常引起的。(一般是钩子函数中抛出异常)
    if (completedAbruptly)
        decrementWorkerCount();

    // 加锁
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 记录当前线程池一共处理了多少个任务
        completedTaskCount += w.completedTasks;
        // 移除工作线程
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    // 尝试将线程池关系(到过度状态 - 销毁状态)
    tryTerminate();

    // 重新获取ctl
    int c = ctl.get();
    // 当前线程池状态,进到这,说明是RUNNING、SHUTDOWN
    if (runStateLessThan(c, STOP)) {
        // 如果是正常状态移除当前工作线程
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 说明是不正常的方式移除了当前工作线程,再添加一个工作线程
        addWorker(null, false);
    }
}

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

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

相关文章

SpringBoot集成webservice

前言 之前在工作中&#xff0c;有时候需要去对接第三方的医院&#xff0c;而很多医院的his系统用的都是老技术&#xff08;WebService&#xff09;。一直在对接webservice接口&#xff0c;却不知道webservice接口是怎么实现的&#xff0c;这一次&#xff0c;我们来一探究竟。 …

Android Compose Bloom 项目实战 (一) : 项目说明与配置

1. 项目介绍 Bloom是谷歌 AndroidDevChallenge (Android 开发挑战赛) 中的一期活动&#xff0c;目的是为了推广Compose&#xff0c;非常适合用来练手&#xff0c;通过这个项目&#xff0c;我们可以很好的入门Compose。本文介绍了如何从零开始&#xff0c;开发这个Compose项目。…

计算机系统结构期末复习

名词解释 程序访问局部性 时间局部性是指最近被访问过的数据很可能再次被访问 空间局部性是指最近被访问过的存储空间的附近空间可能会被访问 计算机体系结构 计算机体系结构是程序员所看到的计算机属性&#xff0c;即概念性结构与功能特性 窗口重叠技术 为了能更简单、更直接…

UE5笔记【五】操作细节——光源、光线参数配置、光照图修复

设置光线重载质量模式为预览&#xff1a;可以加快重构速度。 为了更快速高效的学习&#xff0c;直接查看别人已经建好的素材实例。 在EpicGames启动器中打开示例&#xff0c;找到这个照片级渲染。查看别人建好的效果图。 创建工程以UE5版本打开。进入查看。 观察Baked和碰撞测…

MySQL常用函数大全(面试篇)

本篇文章讲解是是MySQL的函数方法&#xff0c;涵盖所有的MySQL常见的方法。主要介绍了面试常问函数。 一、数字函数二、字符串函数三、日期函数四、MySQL高级函数 &#xff08;一&#xff09;数字函数 1、ABS(x) 返回x的绝对值 2、AVG(expression) 返回一个表达式的平均值&am…

Redis分布式锁

1. 什么是分布式锁 分布式锁指的是&#xff0c;所有服务中的所有线程都去获得同一把锁&#xff0c;但只有一个线程可以成功的获得锁&#xff0c;其他没有获得锁的线程必须全部等待&#xff0c;等到获得锁的线程释放掉锁之后获得了锁才能进行操作。 Redis官网中&#xff0c;set…

中加石墨再冲刺港交所上市:2022年初至今收入为零,陈东尧为CEO

11月18日&#xff0c;中加石墨控股股份有限公司&#xff08;下称“中加石墨”&#xff09;在港交所递交招股书&#xff0c;准备在港交所主板&#xff0c;宏信为独家保荐人。据贝多财经了解&#xff0c;这是中加石墨第二次递表&#xff0c;此前曾于2022年2月28日递交上市申请材料…

通过inode结构体取到次设备号,实现LED灯的亮灭

对Linux来说&#xff0c;设备驱动也是文件。驱动控制硬件的过程&#xff0c;实际上是对驱动文件的读写操作。 对内核来说&#xff0c;如何获取唯一的文件标识呢&#xff1f;当然是通过file结构体中的&#xff0c;inode结构体识别应用层打开的到底是哪一个设备文件。 实验操作及…

数据结构题目收录(二十)

1、含有n个非叶结点的m阶B树中至少包含&#xff08;&#xff09;个关键字。 A&#xff1a;n(m1)B&#xff1a;nC&#xff1a;n(┌\ulcorner┌m/2┐\urcorner┐-1)D&#xff1a;(n-1)(┌\ulcorner┌m/2┐\urcorner┐-1)1 解析 除根结点外&#xff0c;m阶B树中的每个非叶结点至…

mongodump工具安装及使用详解

MongoDB导入导出和备份的命令工具从4.4版本开始不再自动跟随数据库一起安装&#xff0c;而是需要自己手动安装。 官方网站下载链接&#xff1a;Download MongoDB Command Line Database Tools | MongoDB 将下载的压缩包通过工具上传到服务器或者虚拟机中某个路径下并解压&…

ZYNQ图像处理项目——线性神经网络识别mnist

一、线性神经网络识别mnist 线性神经网络其实也可以叫做线性分类器&#xff0c;其实就是没有激活函数的神经网络来对目标进行识别&#xff0c;像支持向量机、多元回归等都是线性的。这边我采用了线性神经网络来识别mnist数字。 我这边是看了一本讲神经网络的书籍&#xff0c;然…

分析高数值孔径物镜的聚焦特性

摘要 高数值孔径的物镜广泛用于光刻、显微等方面。 因此&#xff0c;在仿真聚焦时考虑光的矢量性质是至关重要的。VirtualLab可以支持此类透镜的光线和场追迹分析。通过场追迹分析&#xff0c;可以清楚地显示出由于矢量效应引起的非对称焦点。相机探测器和电磁场探测器可以方便…

【MySQL】Spring Boot项目基于Sharding-JDBC和MySQL主从复制实现读写分离(8千字详细教程)

目录前言一、 介绍二、 主从复制1. 原理2. 克隆从机3. 克隆从机大坑4. 远程登陆5. 主机配置6. 从机配置7. 主机&#xff1a;建立账户并授权8. 从机&#xff1a;配置需要复制的主机9. 测试10. 停止主从同步三、 读写分离1. Sharding-JDBC介绍2. 一主一从3. 一主一从读写分离3.1 …

安服-windowslinux日志分析

目录 windows日志分析 windows事件日志 日志分析工具 Linux日志分析 windows日志分析 windows事件日志 日志分析工具 Linux日志分析 rsyrslog.conf中记录了&#xff0c;这些日志文件存储的位置以及存储的内容是关于什么的日志 其中lastlog比较重要&#xff0c;记录了用户登录…

FRP之入门篇

目录 一、前言 1、概述 2、原理 3、支持功能 4、适用场景 二、环境准备 三、使用 1、安装包下载 2、服务端部署 2.1、上传安装包 2.3、启动服务端 3、客户端部署 3.1、代理服务准备 3.2、上传安装包 3.3、客户端配置 3.4、启动客户端 4、功能验证 一、前言 1、…

Redis在Windows和Linux下的安装方法(超级详细)

Redis的两种不同系统安装1. redis在Windows下的安装2. redis在Linux下的安装1. redis在Windows下的安装 下载安装包(https://github.com/MicrosoftArchive/redis/releases) 下载完后得到安装包找一个自己熟悉的路径就可以进行解压了,我放的是D盘 解压后的文件: 进入到文件夹中…

Java集合(二):Map集合与Collections工具类

目录 Map接口 Map接口的常用方法 删除方法 判断方法 查找方法 增加方法 Map常用方法&遍历操作 HashTable 字典-Dictionary,v> HashMap、HashTable和LinkedHashMap TreeMap 【2】TreeMap-存储自定义数据类型-【内部比较器】 HashMap底层源码 jdk8-源码…

央视春晚临近,主持人李思思被爆离职,知情人火速做出回应

每年的这个时候&#xff0c;中央电视台的春晚&#xff0c;都成为人们热议的话题&#xff0c;不过今年的话题却比较火爆。大众们所关注的央视春晚&#xff0c;第一是参加春晚的明星嘉宾&#xff0c;其次就是参加春晚的节目主持人。 说起央视春晚的主持人&#xff0c;最早要追溯到…

【笔试题】【day26】

文章目录第一题&#xff08;就绪队列中的进程数&#xff09;第二题&#xff08;磁盘缓冲区存在的意义&#xff09;第三题&#xff08;进程从执行态变成就绪态的原因&#xff09;第四题&#xff08;管道&#xff09;第五题&#xff08;文件打开&#xff0c;操作系统会做什么&…

python 图像处理(一阶梯度图像和角度图像)

在整个图像处理的学习过程中可以看到&#xff0c;在很多应用中图像强度的变化情况是非常重要的信息。强度的变化可以用灰度图像I&#xff08;对于彩色图像&#xff0c;通常对每个颜色通道分别计算导数&#xff09;的x和y的方向导数和进行描述。 图像的梯度向量为&#xff1a; …