JAVA小知识31:多线程篇2

一、等待唤醒机制

生产者和消费者,也叫等待唤醒机制。他是一个十分经典的多线程协作的模式。我们来讲一个小故事:

在一个繁忙的工厂里,有一个生产线,我们称之为“共享资源”。这个生产线一次只能生产一个产品,而且需要在完全完成后才能进行下一个。工厂里有两类工人:一类是生产者,他们负责制造产品;另一类是消费者,他们负责将生产好的产品打包发货。

为了避免生产线混乱,工人们约定了一些规则:

生产者不会同时制造多个产品,每完成一个产品后,他们会通知消费者产品已准备好。 消费者不会试图包装未完成的产品,他们会等待生产者的通知。
有一天,生产者和消费者都开始工作了。生产者每制造出一个产品,就大声喊:“产品做好了!”然后消费者听到后,就会过来取走产品,并开始打包。

但是,如果生产线上已经有了一个产品,生产者就不能开始制造下一个,他们必须等待消费者把当前的产品取走。同样,如果生产线上没有产品,消费者就不能进行打包,他们必须等待生产者制造出新的产品。

为了更好地协调工作,他们在生产线旁边放了一个铃铛。每当生产者完成一个产品,他们就会敲响铃铛,通知消费者:“产品做好了,快来取!”而消费者听到铃声后,就会过来取走产品,并开始打包。

这个故事中的生产者和消费者,就像Java中的线程一样,通过同步机制(铃铛)来协调对共享资源(生产线)的访问,确保生产和消费的过程既高效又有序。

在这个故事中,生产线就是Java中的一个对象,生产者和消费者是两个线程,他们通过调用这个对象的同步方法来访问共享资源,并通过wait()和notify()方法来控制对资源的访问,确保资源在任何时候只被一个线程使用。这样,生产者和消费者就能和谐地工作,既不会生产出未完成的产品,也不会让消费者无事可做。

1.1、生产者与消费者的方法

类型方法名描述
线程start()启动线程,使线程开始执行其run()方法。
线程run()线程执行的入口方法,需要重写以定义线程要执行的操作。
同步控制wait() 导致线程等待,直到另一个线程调用相同对象的notify()或notifyAll()方法。
同步控制notify()唤醒在此对象监视器上等待的单个线程。
同步控制notifyAll()唤醒在此对象监视器上等待的所有线程。
锁机制synchronized用于方法或代码块,确保同一时间只有一个线程可以执行该段代码。
显式锁ReentrantLock可重入的互斥锁,比synchronized提供更灵活的锁定机制。
显式锁lock()获取锁,如果锁被另一个线程持有,则等待直到锁被释放。
显式锁unlock()释放锁,允许其他线程获取该锁。
条件变量Condition与锁对象配合使用,用于更复杂的线程间协调。
条件变量await()导致当前线程等待,直到另一个线程调用相同Condition的signal()或signalAll()方法。
条件变量signal()唤醒在此Condition上等待的单个线程。
条件变量signalAll()唤醒在此Condition上等待的所有线程。

这其中最重要的代码就是:

类型方法名描述
同步控制wait() 导致线程等待,直到另一个线程调用相同对象的notify()或notifyAll()方法。
同步控制notify()唤醒在此对象监视器上等待的单个线程。
同步控制notifyAll()唤醒在此对象监视器上等待的所有线程。

我们来看一个例子:

1.1.1 消费者代码:

public class Foodie extends Thread{
    @Override
    public void run() {
        /**
         * 1. 循环
         * 2. 同步代码块
         * 3. 判断共享数据是否到达末尾(到了)
         * 4. 判断共享数据是否到达末尾(没到)
         */
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    // 先去判断生产线上是否有产品
                    if (Desk.state == 0) {
                        // 如果没有就等
                        try {
                            Desk.lock.wait();// 让当前线程与锁对象进行绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        // 货物总数-1
                        Desk.count--;
                        // 如果有就打包
                        System.out.println("[消费者]正在打包商品,还剩" + Desk.count + "件商品");
                        // 打包完毕唤醒生产者生产
                        Desk.lock.notifyAll();
                        // 修改生产线的状态为没有(0)
                        Desk.state=0;
                    }
                }
            }
        }
    }
}
1.1.2 生产者代码:
public class Cook extends Thread {
    @Override
    public void run() {
        /**
         * 1. 循环
         * 2. 同步代码块
         * 3. 判断共享数据是否到达末尾(到了)
         * 4. 判断共享数据是否到达末尾(没到)
         */
        while (true) {
            synchronized (Desk.lock) {
                // 判断是否到达末尾
                if (Desk.count == 0) {
                    break;
                } else {
                    // 判断生产线上是否有食物
                    if (Desk.state == 0) {
                        // 如果没有食物
                        // 生产食物
                        System.out.println("[生产者]生产了一份食物");
                        // 修改生产线上食物状态
                        Desk.state = 1;
                        // 叫醒消费者开始打包
                        Desk.lock.notifyAll();
                    } else {
                        // 如果有食物 则等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }
    }
}
1.1.3 中间代码与启动代码
public class Desk {
    //生产线上是否有货物
    public static int state=0;
    //总个数
    public static int count=10;
    // 锁对象
    public static Object lock = new Object();
}
public class TestDemo {
    public static void main(String[] args) {
        Cook c = new Cook();
        Foodie f = new Foodie();
        c.start();
        f.start();
    }
}
1.1.4 输出图片

可以看到,非常符合我们的 一交一提的理想状态,生产者生产然后消费者消费。
在这里插入图片描述

1.2 阻塞队列

1.2.1 阻塞队列的继承结构

在Java中,阻塞队列(BlockingQueue)是java.util.concurrent包的一部分,它提供了一个线程安全的队列,可以在队列为空时阻塞插入操作,或者在队列为满时阻塞移除操作。阻塞队列继承自java.util.Queue接口,并且是java.io.Serializable的实现。

1.2.2 继承结构图

java.lang.Object
|
|-- java.io.Serializable
|
|-- java.util.Collection
|
|-- java.util.Queue
|
|-- java.util.concurrent.BlockingQueue

1.2.3 阻塞队列成员方法

BlockingQueue接口定义了以下阻塞操作的方法:

  • put(E e): 插入一个元素,如果需要的话,等待空间变得可用。
  • take(): 移除并返回队列头部的元素,如果需要的话,等待直到有一个元素变得可用。
  • offer(E e, long timeout, TimeUnit unit): 如果可能的话,尝试插入一个元素,否则在指定的时间内等待。
  • poll(long timeout, TimeUnit unit): 如果可能的话,尝试移除并返回队列头部的元素,否则在指定的时间内等待。
1.2.4 阻塞队列的实现

BlockingQueue接口有多个实现,例如:

  • ArrayBlockingQueue: 一个固定大小的数组实现,它是有界的。
  • LinkedBlockingQueue: 一个基于链表的实现,可以选择有界或无界。
1.2.5 阻塞队列的细节
  1. 生产者和消费者必须使用同一个阻塞队列
  2. ArrayBlockingQueue阻塞队列在声明时需要填写参数(也就是阻塞队列的长度)
1.2.6 阻塞队列实现代码以及讲解

我们先来看代码

有一个问题就是:我们的阻塞队列定义在哪里?
因为生产者和消费者必须使用同一个阻塞队列,那如果我们在生产者类和消费者类中都定义了阻塞队列,很明显那就是他俩分别用了不同的阻塞队列,所以我们需要将阻塞队列定义在额外的一个类中,然后通过构造方法的形式将阻塞队列传递进去,这样他俩就是一个类了。
在这里插入图片描述
在这里插入图片描述

来看生产者的代码,由于我们的队列长度是1,这里用put添加。

ublic class Cook extends Thread {
    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                queue.put("食品");
                System.out.println("生产者放了一个食品在产线上");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这里我们可以看一下put的源码,我们可以看到这里面已经声明了锁,所以在我们写代码的时候切记不要在外面再嵌套一层锁了,两层锁容易发生死锁的现象。

在这里插入图片描述

消费者的代码同样如此,用take取出。

public class Foodie extends Thread{
    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            try {
                String take = queue.take();
                System.out.println("消费者取出了一个"+take+"并且打包");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这里我们可以看一下take源码:

在这里插入图片描述

然后是main方法

public class TestDemo {
    public static void main(String[] args) {
        // 创建阻塞队列要指定数量
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
        Cook cook = new Cook(queue);
        Foodie foodie = new Foodie(queue);
        cook.start();
        foodie.start();
    }
}

最后我们来看看执行结果,你会发现,诶?怎么又多条相同的?不应该是一替一换吗?
这其中的原因就是我们的输出语句不在里面,但是数据确实是一替一换的

在这里插入图片描述

二、线程的状态

在这里插入图片描述

三、巩固练习题

3.1 打印奇数

同时开启两个线程,获取1-10000之间的数字,将输出所有的奇数。

public class ThreadTest1 extends Thread {
    private final Object object;

    public ThreadTest1(Object object) {
        this.object = object;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (object) {
                if (TestDemo.count > 10000) {
                    break;
                } else {
                    // 这是奇数
                    if (TestDemo.count % 2 == 1) {
                        System.out.println(getName() + " 获取了奇数 " + TestDemo.count);
                    }
                    TestDemo.count++;
                }
            }
        }
    }
}

public class ThreadTest2 extends Thread {
    private final Object object;

    public ThreadTest2(Object object) {
        this.object = object;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (object) {
                if (TestDemo.count > 10000) {
                    break;
                } else {
                    // 这是奇数
                    if (TestDemo.count % 2 == 1) {
                        System.out.println(getName() + " 获取了奇数 " + TestDemo.count);
                    }
                    TestDemo.count++;
                }
            }
        }
    }
}

public class TestDemo {
    public static int count = 1;

    public static void main(String[] args) {
        Object o = new Object();
        ThreadTest1 threadTest1 = new ThreadTest1(o);
        ThreadTest2 threadTest2 = new ThreadTest2(o);
        threadTest1.setName("线程1");
        threadTest2.setName("线程2");
        threadTest1.start();
        threadTest2.start();
    }
}

3.2 抢红包

在这里插入图片描述

首先是工具类 返回一个随机数,这里用到了BigDecimal,对于精确计算,要用BigDecimal ,这个之前我们讲过,可以看看JAVA小知识9

public class Random {

    public static  BigDecimal returnBalance(BigDecimal balance){
        java.util.Random random = new java.util.Random();
        // 怕随机到0,所以在最外面+0.01
        double v = (random.nextDouble()+0.01);
        // 转化为BigDecimal 类型
        BigDecimal bigDecimal = new BigDecimal(v);
        // 乘以balance 这就类似于一个数学题 x/1=y/balance 占比多少份
        // 例如0.1/1 就等于 10/100 如果balance是100 0.1是x 如何算出来这个10 就是用100*0.1
        bigDecimal=bigDecimal.multiply(balance);
        BigDecimal roundedDecimal = bigDecimal.setScale(2, RoundingMode.DOWN);
        return roundedDecimal;
    }
}

接下来是实现类

public class ThreadTest extends Thread {
    @Override
    public void run() {
        synchronized (Demomain.o) {
            // 首先判断红宝数是否为1 是的话直接返回剩余余额
            if (Demomain.count == 1) {
                System.out.println(getName() + "抢到了" + Demomain.balance);
                Demomain.count--;
            } else if (Demomain.count < 1) {
                System.out.println(getName() + "没抢到");
            } else {// 证明还有两个以上的红包
                // 随机出现在抢的钱数
                BigDecimal i = Random.returnBalance(Demomain.balance);
                Demomain.balance = Demomain.balance.subtract(i);
                System.out.println(getName() + "抢到了" + i);
                Demomain.count--;
            }
        }
    }
}

然后是启动类

public class Demomain {
    // 红包余额
    public static BigDecimal balance= BigDecimal.valueOf(100);
    // 红包剩余个数
    public static int count=3;
    public static Object o = new Object();
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        ThreadTest threadTest2 = new ThreadTest();
        ThreadTest threadTest3 = new ThreadTest();
        ThreadTest threadTest4 = new ThreadTest();
        ThreadTest threadTest5 = new ThreadTest();
        threadTest.setName("线程1");
        threadTest2.setName("线程2");
        threadTest3.setName("线程3");
        threadTest4.setName("线程4");
        threadTest5.setName("线程5");
        threadTest.start();
        threadTest2.start();
        threadTest3.start();
        threadTest4.start();
        threadTest5.start();

    }
}

看程序截图

在这里插入图片描述
在这里插入图片描述

四、线程池

4.1、核心原理

① 创建一个池子,池子中是空的
② 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
③ 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

4.2 线程池的构造方法

构造方法说明
public static ExecutorService newFixedThreadPool(int nThreads)创建一个固定线程数量的线程池。
public static ExecutorService newCachedThreadPool()创建一个根据需要创建新线程的线程池。(没有上限的线程池)

这种方法构造的线程池有很大的弊端,一般来讲我们都会使用自定义的线程池。

4.3 自定义线程池

4.3.1 构造方法
构造方法说明
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)创建一个线程池,具有给定的初始参数。核心线程池大小、最大线程池大小、线程空闲时间、时间单位和任务队列。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)创建一个线程池,具有给定的初始参数和线程工厂。线程工厂用于创建新线程。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)创建一个线程池,具有给定的初始参数和拒绝策略。拒绝策略用于处理无法执行的新任务。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)创建一个线程池,具有给定的初始参数、线程工厂和拒绝策略。

在这里插入图片描述

我们以最后一个构造函数举例, 来看代码

public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        3,//核心线程数量 最小值为0
        8,// 最大线程数量 不小于零 且要大于核心线程数量
        60,// 空闲线程最大存活时间
        TimeUnit.SECONDS,// 空闲线程最大存活时间单位
        new ArrayBlockingQueue<>(3),// 任务队列
        Executors.defaultThreadFactory(), // 创建线程的工厂
        new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
    );

    executor.submit(new demo1());
    executor.submit(new demo1());
    executor.submit(new demo1());
    executor.submit(new demo1());
}
public class demo1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println( Thread.currentThread().getName()+ ": " + i);
        }
    }
}

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

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

相关文章

等保2.0 实施方案

一、引言 随着信息技术的广泛应用&#xff0c;网络安全问题日益突出&#xff0c;为确保信息系统安全、稳定、可靠运行&#xff0c;保障国家安全、公共利益和个人信息安全&#xff0c;根据《网络安全法》及《信息安全技术 网络安全等级保护基本要求》&#xff08;等保2.0&#x…

SQL Server和Oracle数据库的实时同步

数据同步在大数据应用中扮演着关键角色&#xff0c;它确保了数据的实时性和一致性&#xff0c;为数据分析和决策提供了重要支持。常见的数据同步方式包括ETL实时同步和实时ETL工具&#xff0c;后者可以基于日志追踪或触发器进行分类。不同的数据库系统针对实时同步也有各自的实…

打破数据生产力的桎梏,打造数据分析驱动的新型组织

在当前的经济环境下&#xff0c;各行业面临着前所未有的挑战&#xff0c;降本增效成为企业普遍追求的目标。数字化转型被视为实现这一目标的关键路径。通过数字化手段&#xff0c;企业能够探索新的增长机会&#xff0c;提升运营效率&#xff0c;并有效控制成本支出。在这一转型…

电影解说 剪辑实战带货全新蓝海市场,电影解说实战课程(16节)

课程目录 1-影视解说自媒体带货新玩法_1.mp4 2-影视解说选品及解说规范标准_1.mp4 3-电影解说的脚本模版及流程_1.mp4 4-电影解说编写文案及爆火规律_1.mp4 5-手把手教你影视素材哪里找_1.mp4 6-影视解说剪辑、配音及创收方式_1.mp4 7-电影解说剪辑的实操课程A_1.mp4 8…

Zabbix 配置SNMP监控

Zabbix SNMP监控介绍 Zabbix提供了强大的SNMP监控功能&#xff0c;可以用于监控网络设备、服务器和其他支持SNMP协议的设备。SNMP&#xff08;Simple Network Management Protocol&#xff0c;简单网络管理协议&#xff09;是一种广泛用于网络管理的协议。它用于监控网络设备&…

SRC通杀小技巧-巧用域名“横向移动“

文章目录 前言还是DevOps做个字典&#xff1f; 前言 周末闲暇时间无聊顺便挖挖洞,低危小子的我叒找到个低危&#xff0c;本想着一个低危实在是食之无味&#xff0c;弃之又可惜&#xff0c;打算将域名先存起来&#xff0c;等过段时间有活动一块交&#xff0c;就在复制域名的时候…

【人工智能】GPT-5的即将到来:从高中生进化到,,,博士生?

GPT-5的即将到来&#xff1a;从高中生进化到,博士生&#xff1f; 随着近月GPT-4o的出世&#xff0c;OpenAI也在进行一系列的采访和介绍接下来的展望和目标。 在6月22日的采访中&#xff0c;美国达特茅斯工程学院公布了OpenAI首席技术官米拉穆拉蒂的访谈内容。穆拉蒂确认&#…

嵌入式上gst rtsp server opencv mat

0 安装gstreamer sudo apt install libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-…

PhysioLLM 个性化健康洞察:手表可穿戴设备实时数据 + 大模型

个性化健康洞察&#xff1a;可穿戴设备实时数据 大模型 提出背景PhysioLLM 图PhysioLLM 实现数据准备用户模型和洞察生成个性化数据总结和洞察是如何生成的&#xff1f; 解析分析 提出背景 论文&#xff1a;https://arxiv.org/pdf/2406.19283 虽然当前的可穿戴设备伴随应用&…

uniapp应用如何实现传感器数据采集和分析

UniApp是一种跨平台的应用开发框架&#xff0c;它支持在同一份代码中同时开发iOS、Android、H5等多个平台的应用。在UniApp中实现传感器数据采集和分析的过程可以分为以下几个步骤&#xff1a; 引入相关插件或库 UniApp通过插件或库的形式扩展功能。对于传感器数据采集和分析&…

【APK】SDKManager运行后闪退

本地JDK已安装&#xff0c;且配置了环境变量&#xff0c;未安装 android studiio 问题描述&#xff1a;右键以管理员身份运行 SDKManager&#xff0c;终端窗口闪退 问题原因&#xff1a;未找到正确的Java路径 解决办法&#xff1a; 1.修改tools目录下的 android.bat 文件&am…

数字人直播源码开发全攻略揭秘:如何搭建自己的数字人直播平台?

当前&#xff0c;数字人直播逐渐成为众多中小型企业线上带货和品牌宣传的不二之选&#xff0c;而艾媒研究数据也显示&#xff0c;超五成以上的被调查群体的企业使用过虚拟人技术&#xff0c;超三成被调查群体的企业计划使用虚拟人技术。在此背景下&#xff0c;越来越多的创业者…

10计算机视觉—物体检测算法

目录 1.R-CNN(区域卷积神经网络)2014兴趣区域(RoI)池化层Fast RCNN 2015Faster R-CNN 2015Mask R-CNN 2017总结2. SSD(单发多框检测)2016SSD模型总结3.YOLO(你只看一次)快!很重要4.目标检测算法性能对比5.SSD代码实现 使用很少,比不上yolo多尺度锚框实现SSD代码实现训练…

DOM 中包含哪些重要方法

1. alert 带有指定消息的警告框 alert("hello world"); 2. confirm 带有确定和取消的对话框&#xff0c;点击确定返回 true&#xff0c;点击取消返回 false confirm("你好吗"); 3. prompt 显示一个提示框&#xff0c;允许用户输入文本&#xff0c;点击…

数据恢复篇:5 款最佳 Mac 数据恢复软件

说到保护我们的数字生活&#xff0c;数据恢复软件的重要性怎么强调都不为过。无论您是意外删除了假期照片的普通用户&#xff0c;还是面临硬盘损坏的专业人士&#xff0c;随之而来的恐慌都是普遍存在的。幸运的是&#xff0c;数据恢复工具可以缓解这些压力。在Mac用户可用的众多…

零障碍入门:SSH免密登录与Hadoop生态系统的完美搭档【实训Day02】

一、 SSH免密登录配置 1 生成公钥和秘钥(在hadoop101上) # su star # cd /home/star/.ssh # ssh-keygen -t rsa 2 公钥和私钥 公钥id_rsa.pub 私钥id_rsa 3 将公钥拷贝到目标机器上(在hadoop101上) # ssh-copy-id hadoop101 # ssh-copy-id hadoop102 # ssh-co…

翔云发票查验接口状态码说明,哪种情况扣次数那种情况不扣次数呢

翔云发票查验API&#xff0c;实时联网&#xff0c;可以实现发票信息真伪的快速核验&#xff0c;帮助企业财务摆脱繁琐的发票真伪查验工作。那么知道了发票查验接口的作用&#xff0c;对于开发者而言&#xff0c;接口返回的状态码又分别代表什么含义呢&#xff1f;下面就翔云发票…

【Elasticsearch】Elasticsearch索引创建与管理详解

文章目录 &#x1f4d1;引言一、Elasticsearch 索引的基础概念二、创建索引2.1 使用默认设置创建索引2.2 自定义设置创建索引2.3 创建索引并设置映射 三、索引模板3.1 创建索引模板3.2 使用索引模板创建索引 四、管理索引4.1 查看索引4.2 更新索引设置4.3 删除索引 五、索引别名…

掌握高效实用的VS调试技巧

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 1.编程常见的错误 1.1编译型错误 编程编译型错误是指在编译代码时发现的错误。编译器在编译过程中会检查代码是否符合语法规范和语义要求&#xff0c;如果发现错误会产生编译错误。 直接看错误提示信息&#xff08;双击&#…

超声波气象站的工作原理

TH-CQX5超声波气象站中的超声波技术是其核心工作原理之一&#xff0c;以下是关于超声波气象站中超声波的详细解释&#xff1a;超声波是一种频率高于人耳能听到的声音频率范围的声波&#xff0c;通常指频率在20kHz以上的声波。超声波具有较短的波长和强的穿透能力&#xff0c;能…