线程池原理及使用

线程池继承关系

1.为什么使用线程池?

        1.反复创建线程开销大;

        2.过多线程会占用太多内存(执行任务易出现“内存溢出”);

        3.加快程序响应速度;

        4.合理利用CPU和内存;

        5.统一管理线程;

2.创建和停止线程池

2.1.线程池参数解释

1.keppAliveTime

        如果线程池当中的线程数量大于“corePoolSize”,当这些多余的线程空闲时间超过keepAliveTime时它们就会被回收;

2.threadFactory

        新的线程是由threadFactory创建的默认使用Executors.defaultThreadFactory(),创建出来的线程都是用户线程且属于同一个线程组优先级都默认为5,如果自己指定threadFactory那么就可以改变线程名、线程组、优先级、是否是守护线程等信息;

3.workQueue

        常见三种队列类型:

        1.直接交换队列(SynchronousQueue)

                即队列不存储任务,直接将任务发往线程池执行;

        2.无界队列(LinkedBlockingQueue)

                即队列存储任务数量没有上限(有内存溢出风险);

        3.有界队列(ArrayBlockingQueue)

                即队列中指定存储多少个任务;

2.2.线程池添加线程规则

        1.如果线程数小于corePoolSize就创建一个新线程去运行新任务;

        2.如果线程数大于等于corePoolSize但小于最大线程数(maxPoolSize)则将任务放入队列;

        3.如果队列中任务已满,并且线程数小于maxPoolSize则创建一个新线程来执行任务(队列塞满才尝试去扩容);

        4.如果队列已满且线程数大于maxPoolSize则拒绝该任务添加到线程池;

2.3.使用工具类创建线程池的四种方式

如下四种创建线程池各参数情况:

        1.newFixedThreadPool

                原理:

                        corePoolSize = maxPoolSize = 传入线程个数;

                        使用队列:LinkedBlockingQueue(无界队列容量无上限);

                固定长度线程池,newFixedThreadPool由于使用的工作队列是LinkedBlockingQueue是没有容量上限的,所以当请求越来越多并且来不及处理时会出现请求堆积造成大量内存被占用出现“内存溢出”问题;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * 演示使用newFixedThreadPool
 *      该线程池使用的是LinkedBlockingQueue(无界队列,即队列中可以存放任意多任务有“内存溢出”风险)
 */
public class FixedThreadPool {
    public static void main(String[] args) {
        // 创建固定数量线程池
        ExecutorService pool = Executors.newFixedThreadPool(4);
        // 假设需执行1000个任务,每个任务休眠200ms再打印出线程名
        for (int i = 1; i <= 1000 ; i++) {
            pool.execute(new Task());
        }
    }

    // 任务类
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程名:" + Thread.currentThread().getName());
        }
    }
}

       

        2.newSingleThreadExecutor

                原理:

                        corePoolSize = maxPoolSize = 1

                        使用队列:LinkedBlockingQueue(无界队列容量无上限)

                单线程线程池(该线程池只有一个线程)其原理和newFixedThreadPool线程池相同,当请求堆积且来不及处理时易出现“内存溢出”问题;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * 演示使用newSingleThreadExecutor(单线程线程池)
 * 该线程池使用的是LinkedBlockingQueue(无界队列,即队列中可以存放任意多任务有“内存溢出”风险)
 */
public class SingleThreadPool {
    public static void main(String[] args) {
        // 创建单线程线程池
        ExecutorService pool = Executors.newSingleThreadExecutor();
        // 假设需执行1000个任务,每个任务休眠200ms再打印出线程名
        for (int i = 1; i <= 1000 ; i++) {
            pool.execute(new SingleThreadPool.Task());
        }
    }

    // 任务类
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程名:" + Thread.currentThread().getName());
        }
    }
}

        3.CachedThreadPool

                可缓存线程池使用直接交换队列,队列中不存储任务当任务来了判断当前线程数是否小于Integer.MAX_VALUE,若小于则直接交给线程池去执行,CachedThreadPool具有回收多余线程的功能(默认情况下空闲60s的线程会被自动回收),CachedThreadPool的弊端在于maxPoolSize的值为Integer.MAX_VALUE,这可能会创建非常多的线程可能会导致“内存溢出”问题;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * 演示使用CachedThreadPool(可缓存线程池)
 *      该线程池使用的是SynchronousQueue(直接交换队列,即队列中不存储任务当任务来了判断当前线程数是否小于Integer.MAX_VALUE,若小于则直接交给线程池去执行)
 */
public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 1; i <= 1000 ; i++) {
            pool.execute(new Task());
        }
    }

    // 任务类
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程名:" + Thread.currentThread().getName());
        }
    }
}

        4.ScheduledThreadPool

                支持定期及周期性任务执行的线程池(内部使用了延迟队列);

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
 * 演示使用ScheduledThreadPool(周期性执行线程池)
 */
public class ScheduledThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
        // 用法一(延迟三秒后执行)
        pool.schedule(new Task(),3,TimeUnit.SECONDS);

        System.out.println("====================    分割线    ====================");

        // 用法二(周期性执行(首次执行延迟一秒后执行,后面每间隔3秒周期性执行))
        pool.scheduleAtFixedRate(new Task(),1,3,TimeUnit.SECONDS);
    }

    // 任务类
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程名:" + Thread.currentThread().getName());
        }
    }
}

2.4.如何设置线程池线程数量

        1.CPU密集型:例如:加密,计算等高耗CPU的操作,最佳线程数为CPU核心数的1-2倍左右;

        2.耗时IO型:例如:读写数据库,文件,网络读写操作,最佳线程数一般大于CPU核心数的很多倍,以JVM线程监控显示繁忙情况为依据确保线程空闲能够衔接上;

        3.公式:CPU核心数 * (1 + 平均等待时间/平均工作时间);

2.5.停止线程池相关方法

        1.shutdown

       shutdown命令用于停止线程池,线程池调用shutdown命令并不是立即去停止线程池的,使用shutdown命令线程池会等到正在执行的任务和队列中的任务都执行完毕后才会停止,当线程池调用shutdown命令后对于新提交过来的任务线程池会抛出“RejectedExecutionException”异常去拒绝接收新任务;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * 演示停止线程池方法 - shutdown命令
 */
public class shutDown {
    public static void main(String[] args) throws InterruptedException {

        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 提交1000个任务
        for (int i = 1; i <= 1000 ; i++) {
            pool.execute(new Task());
        }

        // 休眠三秒让1000个任务全部已经提交给线程池
        TimeUnit.SECONDS.sleep(3);
        pool.shutdown();
        boolean shutdown = pool.isShutdown();
        System.out.println("isShutdown()用于判断当前线程池是否执行了shutdown或shutdownNow命令:" + shutdown);
        boolean terminated = pool.isTerminated();
        System.out.println("isTerminated()用于判断当前线程池是否真正停止(正在执行的任务 + 队列中任务)都执行完毕且已调用shutdown或shutdowNow命令: = " + terminated);
    }

    // 任务
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }
    }
}

        2.shutdownNow

      shutdownNow命令用于停止线程池,当线程池调用shutdownNow命令时会立即停止线程池执行shutdownNow命令会做两件事;

        1.线程池会对正在执行的线程发出中断信号去中断当前正在执行的线程;

        2.对于在队列中等待执行的任务shutdownNow命令返回值会返回在队列中的所有任务,一般执行shutdownNow命令时我们需要对返回值做处理(比如将这些队列中的任务存储到数据库中或使用其它线程池去执行队列中的任务);

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ShutdownNow {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(10);

        // 提交100个任务
        for (int i = 1; i <= 100 ; i++) {
            pool.execute(new Task());
        }
        // 休眠2秒让100个任务全部添加到线程池中去
        TimeUnit.SECONDS.sleep(2);

        // 队列中剩余任务个数
        List<Runnable> taskQueue = pool.shutdownNow();
        System.out.println("队列中剩余任务个数: " + taskQueue.size());
    }

    // 任务
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("线程池已执行shutdownNow命令,当前线程:" + Thread.currentThread().getName() + " 被中断了...");
            }
            System.out.println(Thread.currentThread().getName());
        }
    }
}

        3.isShutdown

判断线程池是否已停止,当调用shutdown()或shutdownNow() 方法后则此方法返回true;           

具体使用见本章shutdown命令演示代码;

        4.isTerminated

      判断线程池是否完全停止,当调用shutdown()或shutdownNow() 方法后且线程池中正在执行的任务及队列中任务都已经执行完毕则此方法返回true;

具体使用见本章shutdown命令演示代码;

        5.awaitTermination

     等待指定时间后检测当前线程池是否已关闭,一般情况下会和shutdown方法组合使用;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * 演示:awaitTermination命令(等待指定时间后检测当前线程池是否已关闭)
 */
public class AwaitTermination {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(10);

        for (int i = 1; i <= 1000 ; i++) {
            pool.execute(new Task());
        }

        // 等待指定时间后检测当前线程池是否已关闭
        boolean flag = pool.awaitTermination(3, TimeUnit.SECONDS);
        System.err.println(flag);
    }

    // 任务
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }
    }
}

2.6.线程池拒绝策略

1.线程池何时拒绝新任务

        1.1.线程池调用shutdown或shutdownNow关闭线程池方法后还有新任务提交过来,此时线程池会抛出“RejectedExecutionException”异常去拒绝新任务;

        1.2.线程池最大线程数和工作队列到达上限时会拒绝新任务;

2.线程池的四种拒绝策略

        1.AbortPolicy

                当有新任务想要添加到线程池时直接抛出异常;

        2.DiscardPolicy

                当有新任务想要添加到线程池时不处理也不添加任务到工作队列(什么也不做);

        3.DiscardOldestPolicy

                当有新任务想要添加到线程池时抛弃存放在工作队列中最久的任务并添加新任务到队列;

        4.CallerRunsPolicy        

                当有新任务想要添加到线程池时将新任务交给当前线程去执行(即主线程去执行)。这样做的好处在于不丢弃已有工作队列中的任务,同时给线程池处理已有工作队列中任务的时间,若这段时间线程池处理完一些任务后续再往线程池中添加新任务时就能成功添加);

2.7.线程池的“钩子”方法

        通过线程池的钩子方法我们可以在每个线程执行的前后做一些事;

import java.util.concurrent.*;
/**
 * 演示每个线程池任务执行前后执行自定义的钩子函数
 *       可用于线程的日志搜集;
 */
public class ThreadPoolHookMethod  extends ThreadPoolExecutor {

    public ThreadPoolHookMethod(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public ThreadPoolHookMethod(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public ThreadPoolHookMethod(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public ThreadPoolHookMethod(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }


    /**
     *  钩子方法一:在线程池执行之前做某些事
     * @param t 将要执行任务的线程
     * @param r 将要执行的任务
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        System.out.println("===============  我是线程池的钩子方法beforeExecute(在线程池执行任务前我执行了) 当前线程池名称:" + t.getName() + " ===============");
    }


    /**
     * 钩子方法二:在线程池执行之后做某些事
     * @param r 执行的任务
     * @param t 异常信息
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        System.out.println("===============  我是线程池的钩子方法afterExecute(在线程池执行任务后我执行了)  ===============");
    }


    public static void main(String[] args) {
        ThreadPoolHookMethod pool = new ThreadPoolHookMethod(5,5,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());
        pool.execute(new Task());
    }

    // 任务
    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println("我是线程:" + Thread.currentThread().getName() +" 正在执行run方法中任务...");
        }
    }
}

3.线程池实现原理

        1.线程池组成部分

                1.线程池管理器;

                        用于管理线程池创建或停止线程池;

                2.工作线程;

                        线程池创建出来用于执行任务的那些线程;

                3.任务队列;

                        用于存储提交到线程池的任务;

                4.任务接口(Task);

                        线程池具体的执行任务;

        2.线程池实现任务复用的原理

                相同线程执行不同任务。在线程池内部循环检测能否拿取到工作队列中任务,如果能拿取到任务则调用其任务的run方法去执行任务,此时直接调用run方法并没有去新开线程而是线程池中的当前线程去执行run方法所以实现了线程的复用;

4.线程池状态

        1.RUNNING

                接收新任务并处理工作队列中待处理的任务;

        2.SHUTDOWN

                不接收新任务但处理工作队列中待处理的任务;

        3.STOP

                不接收新任务,也不处理工作队列中待处理的任务,并且向正在执行的任务发出“中断”信号;

        4.TIDYING

                所有任务(正在执行的任务 + 工作队列中待处理的任务)都已终止,并将运行terminate()方法;

        5.TERMINATED

                运行完成;

5.使用线程池注意点

        1.避免任务堆积(响应效率慢且可能出现“内存溢出”问题);

        2.避免线程数过度增加(可能出现资源耗尽等问题);

        3.排查线程泄露(线程泄露指线程执行完毕后不能被回收);

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

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

相关文章

使用Python构建令人瞩目的高频交易算法

大家好&#xff0c;在金融领域&#xff0c;高频交易&#xff08;HFT&#xff09;因其能够以极高的速度执行大量订单的能力而备受关注。高频交易算法旨在识别并利用不同市场间的微小价格差异&#xff0c;因此交易者需要实现低延迟系统来进行套利策略&#xff0c;本文将探索使用P…

我的NPI项目之Android系统升级 - 同平台多产品的OTA

因为公司业务中涉及的面比较广泛&#xff0c;虽然都是提供移动终端PDA&#xff0c;但是使用的场景很多时候是不同的。例如&#xff0c;有提供给大型物流仓储的设备&#xff0c;对这样的设备必需具备扫码功能&#xff0c;键盘&#xff08;戴手套操作&#xff09;&#xff0c;耐用…

大数据求职心得

........................................................................................................................................................... 大数据求职心得 ...................................................................................…

写一个随机点名的程序

获取方式&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1fdCJ_3IYUl7v7x6I1zAWgg 提取码&#xff1a;1234 这里面用到JS当中的数组&#xff0c;random以及window.setInterval&#xff08;&#xff09;回调函数来进行实现的.

性能测试-jemeter:安装 / 基础使用

一、理解jemeter 官网-Apache JMeter-Apache JMeter™ JMeter是一款开源的性能测试工具&#xff0c;主要用于模拟大量用户并发访问目标服务器&#xff0c;以评估服务器的性能和稳定性。 JMeter可以执行以下任务序号用途描述1性能测试通过模拟多个用户在同一时间对服务器进行…

Pytorch从零开始实战14

Pytorch从零开始实战——DenseNet SENet算法实战 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——DenseNet SENet算法实战环境准备数据集模型选择开始训练可视化总结 环境准备 本文基于Jupyter notebook&#xff0c;使用Python3.8&#x…

搭建FTP服务器与计算机端口介绍

FTP介绍 FTP&#xff08;File Transfer Protocol&#xff09;是一种用于在计算机网络上进行文件传输的协议。它允许用户通过客户端与服务器进行通信&#xff0c;从服务器下载文件或将文件上传到服务器。 FTP使用客户端-服务器模型。用户使用FTP客户端软件连接到FTP服务器&…

人工智能_机器学习077_Kmeans聚类算法_亚洲国家队自动划分类别_3维可视化实现---人工智能工作笔记0117

然后我们上一节使用聚类算法对,2006年世界杯,2010年世界杯,2007年亚洲杯,足球队进行了自动类别划分,然后 这一节,我们使用代码对,聚类算法的划分结果,进行一下可视化 plt.figure(figsize=(12,9)) 首先指定画布大小 ax=plt.subplot(111,projection=3d) 然后指定111,表示画布的,…

【电商项目实战】基于SpringBoot完成首页搭建

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《电商项目实战》。&#x1f3af;&#x1f3af; &am…

STM32F4系列单片机库函数模板工程创建

目录 一、工程配置 1、新建工程 2、芯片选择 3、工程子文件夹创建 &#xff08;1&#xff09;FWLIB文件夹添加文件 &#xff08;2&#xff09;CORE文件夹添加文件 &#xff08;3&#xff09;USER文件夹添加文件 4、工程设置 &#xff08;1&#xff09;工程中添加文件夹…

Temu和Shein争端再起:海外电商“围城”下,一场厮杀正在酝酿

两家中国电商出海“双子星”&#xff0c;争端再起。 最近&#xff0c;美国法院最新公开临时限制令显示&#xff0c;跨境电商平台Temu&#xff08;特木&#xff09;的男装、休闲装、运动服等50款产品涉侵权时尚电商平台Shein&#xff08;希音&#xff09;&#xff0c;并向Temu旗…

【halcon深度学习】dev_display_dl_data 移植到C# 上篇

效果展示 前言 在研究halcon深度学习的时候,会发现halcon的例程里面用到了大量的二次封装库函数。这些库函数内部也是由基础的算子组成。我们在halcon的开发环境里面用的很爽,但是一旦要在C#中使用,就会报错。 一开始,我想避开这个移植过程,直接使用halcon引擎(HDevEngi…

网络通信-Linux 对网络通信的实现

Linux 网络 IO 模型 同步和异步&#xff0c;阻塞和非阻塞 同步和异步 关注的是调用方是否主动获取结果 同步:同步的意思就是调用方需要主动等待结果的返回 异步:异步的意思就是不需要主动等待结果的返回&#xff0c;而是通过其他手段比如&#xff0c;状态通知&#xff0…

【算法题】矩阵顺时针旋转90° (js)

力扣链接&#xff1a;https://leetcode.cn/problems/rotate-matrix-lcci/description/ 本人题解&#xff1a; /*** param {number[][]} matrix* return {void} Do not return anything, modify matrix in-place instead.*/ var rotate function (matrix) {const x matrix.le…

【基础篇】六、自定义类加载器打破双亲委派机制

文章目录 1、ClassLoader抽象类的方法源码2、打破双亲委派机制&#xff1a;自定义类加载器重写loadclass方法3、自定义类加载器默认的父类加载器4、两个自定义类加载器加载相同限定名的类&#xff0c;不会冲突吗&#xff1f;5、一点思考 1、ClassLoader抽象类的方法源码 ClassL…

【Linux】进程查看|fork函数|进程状态

&#x1f984; 个人主页——&#x1f390;开着拖拉机回家_Linux,大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&am…

ADRC-跟踪微分器TD的Maltab实现及参数整定

目录 问题描述&#xff1a; 跟踪微分器TD基本概念&#xff1a; Matlab及其实现&#xff1a; 跟踪效果&#xff1a; 例1&#xff1a;跟踪信号 sin(t) 0.5*rand(1,1)。 例2&#xff1a;跟踪部分时段为方波的信号&#xff0c;具体形式见代码get_command。 参数整定&#xf…

⭐Unity 读取本地图片再区域裁剪

现在需求是将本地的图片读取之后再区域截图成新的图片 话不多说直接上代码 using UnityEngine; using System.IO;public class LocalRegionCapture : MonoBehaviour {public string fullScreenImagePath "Assets/SavedImages/fullScreenScreenshot.png";public str…

蓝桥杯2020年5月青少组Python程序设计国赛真题

1、 上边是一个算法流程图,最后输出的b的值是() A.377 B.987 C.1597 D.2584 2、 3、如果整个整数X本身是完全平方数,同时它的每一位数字也都是完全平方数我们就称X 是完美平方数。前几个完美平方数是0、1、4、9、49、100、144......即第1个完美平方数是0,第2个是 1,第3个…

问卷调查反应偏差消除技巧:提高数据准确性的实用方法

回应偏差会对您的调查结果产生不利影响。以下是您可以采取的措施来对抗偏差。调查之所以成为一个有力的工具&#xff0c;是因为它能够获取关于目标受众的真实信息和数据&#xff0c;而不是依靠试错方法。然而&#xff0c;为了使这些数据有用&#xff0c;它必须是无误差的&#…
最新文章