探索线程池的威力:优化多线程任务管理与性能提升

news2024/11/22 21:05:55

比喻举例(可以比作工人队伍)

想象一下,如果我们需要完成很多工作,我们可以招募一群工人来协助。然而,如果每个工人都是临时招募的,工作完成后就解雇他们,那么每次都要花时间和精力来招募和解雇工人,效率会很低。

线程池就像是一个团队,其中包含一些固定数量的常驻工人。这些工人一直在那里,准备接受任务。当有新任务到来时,只需要将任务交给线程池,线程池会选择一个可用的工人来执行任务。

这样做的好处是,线程池中的工人可以被重复利用,无需频繁创建和销毁线程。线程创建和销毁是一个开销较大的操作,通过线程池可以减少这部分开销,提高整体的性能和效率。

线程池还可以控制并发的数量。通过设定线程池的大小,我们可以限制同一时间执行的任务数量,避免资源过度消耗和系统负载过重。线程池可以合理地管理和调度工作,保持系统在高并发情况下的稳定运行。

线程池优点

  1. 降低线程创建和销毁的开销:线程的创建和销毁是一个代价较高的操作。通过使用线程池,我们可以避免频繁地创建和销毁线程,从而减少了这些开销。
  2. 提高系统响应速度:线程池中的线程一直保持活跃状态,可以立即响应新任务的到来。与临时创建的线程相比,线程池中的线程不需要等待线程创建的时间,减少了任务执行的延迟,提高了系统的响应速度。
  3. 控制并发线程数量:线程池可以设定最大并发线程数,以限制同时执行的任务数量。这样可以避免系统过载和资源耗尽,并且可以根据系统的性能和负载情况来灵活调整线程池的大小。
  4. 提供线程管理和调度:线程池通过内部的任务队列来管理待执行的任务。当有新任务提交时,线程池会选择一个可用的线程来执行任务。在任务执行完成后,线程又可以重新回到池中,等待下一个任务的到来。这种自动的管理和调度机制使得线程池更加高效和易于使用。

执行原理

首先讲一下ThreadPoolExecutor中的参数含义

    // Public constructors and methods

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters and default thread factory and rejected execution handler.
     * It may be more convenient to use one of the {@link Executors} factory
     * methods instead of this general purpose constructor.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
名称类型含义🔟
corePoolSizeint线程池中保持活跃的线程数,即使它们是空闲状态,除非设置了 allowCoreThreadTimeOut
maximumPoolSizeint线程池允许存在的最大线程数。
keepAliveTimelong当线程数超过 corePoolSize 时,多余的空闲线程在终止之前等待新任务的最大时间
unitTimeUnit参数的时间单位。
workQueueBlockingQueue任务队列,用于存储尚未执行的任务,这个队列只会存储通过 execute 方法提交的 Runnable 任务。
defaultThreadFactoryThreadFactory线程工厂,用于创建新线程
handlerRejectedExecutionHandler拒绝策略,用于在任务被拒绝执行时的处理方式。

看图解释

图片来自 一个会写诗的程序员

img

在了解了这个的前提下我们就可以看下四种类型

线程池类型

固定大小线程池(FixedThreadPool)

固定大小线程池适用于需要固定数量线程的场景。我们可以通过调用Executors.newFixedThreadPool(int nThreads)来创建固定大小线程池,其中nThreads是线程池中的线程数量。

固定大小线程池的核心线程数和最大线程数都是相同的,因此线程数量是固定的。如果所有的工作线程都忙于执行任务,新的任务就会被放入任务队列中等待执行。由于线程数量固定,所以固定大小线程池不会创建新的线程,直到任务队列满了。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomFixedThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        int corePoolSize = 5; // 核心线程数
        int maxPoolSize = 5; // 最大线程数
        long keepAliveTime = 0; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务给线程池执行
        executor.execute(new MyTask());

        // 关闭线程池
        executor.shutdown();
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println("任务正在执行");
        }
    }
}

缓存线程池(CachedThreadPool)

缓存线程池适用于大量短时间任务的场景。我们可以通过调用Executors.newCachedThreadPool()来创建缓存线程池。

缓存线程池的核心线程数为0,最大线程数为整数最大值。当有任务到来时,如果有空闲线程可用,则直接使用。如果没有空闲线程可用,就会创建新的线程。新线程将在60秒钟不活动后被回收。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池
        int corePoolSize = 0; // 核心线程数
        int maxPoolSize = Integer.MAX_VALUE; // 最大线程数
        long keepAliveTime = 60; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            keepAliveTime,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()
        );

        // 创建多个任务
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1正在执行");
            }
        };

        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2正在执行");
            }
        };

        Runnable task3 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务3正在执行");
            }
        };

        // 提交任务到线程池
        executor.execute(task1);
        executor.execute(task2);
        executor.execute(task3);

        // 关闭线程池
        executor.shutdown();
    }
}

//如果想要实现有序的情况下 可以考虑使用executor.submit的方式
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池
        int corePoolSize = 2; // 核心线程数
        int maxPoolSize = 5; // 最大线程数
        long keepAliveTime = 60; // 空闲线程的存活时间(单位:秒)

        ExecutorService executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务
        executor.execute(() -> {
            System.out.println("任务1正在执行");
        });

        executor.execute(() -> {
            System.out.println("任务2正在执行");
        });

        // 关闭线程池
        executor.shutdown();
    }
}

单线程池(SingleThreadExecutor)

单线程池适用于需要按照顺序执行任务的场景。我们可以通过调用Executors.newSingleThreadExecutor()来创建单线程池。

单线程池只有一个工作线程,它会按照任务的提交顺序依次执行任务。如果当前工作线程因为异常退出,线程池会创建一个新的线程来替代它。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomSingleThreadExecutorExample {
    public static void main(String[] args) {
        // 创建单线程池
        int corePoolSize = 1; // 核心线程数
        int maxPoolSize = 1; // 最大线程数
        long keepAliveTime = 0; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务给线程池执行
        executor.execute(new MyTask());

        // 关闭线程池
        executor.shutdown();
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println("任务正在执行");
        }
    }
}

定时线程池(ScheduledThreadPool)

定时线程池用于延迟执行或定时执行任务的场景。我们可以通过调用Executors.newScheduledThreadPool(int corePoolSize)来创建定时线程池。

定时线程池可以按照给定的延迟时间或固定的时间间隔周期执行任务。它使用了内部的ScheduledExecutorService来执行定时任务。

//对于延迟执行任务的实例
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledTaskExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

        // 创建任务1
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1正在执行");
            }
        };

        // 创建任务2
        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2正在执行");
            }
        };

        // 安排任务1在延迟3秒后执行
        long delay1 = 3;
        executor.schedule(task1, delay1, TimeUnit.SECONDS);

        // 安排任务2在延迟5秒后执行
        long delay2 = 5;
        executor.schedule(task2, delay2, TimeUnit.SECONDS);

        // 关闭线程池
        executor.shutdown();
    }
}

//固定时间间隔
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledTaskExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

        // 创建任务
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务正在执行");
            }
        };

        // 安排任务每隔1秒执行一次
        long initialDelay = 0;
        long period = 1;
        executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);

        // 关闭线程池
        executor.shutdown();
    }
}

注意点

为什么不用Executors的方式去建立线程池

  1. 默认参数可能不适合特定的需求:Executors 提供的工厂方法通常使用一组默认的线程池参数,如核心线程数、最大线程数和队列类型等。这些参数对于某些特定的应用场景可能不够合适。如果没有显式地配置这些参数,可能会导致线程池在面对特定负载时表现不佳。
  2. 随意创建线程可能导致资源过度占用:使用 Executors 创建可缓存的线程池时,线程池的最大线程数可以根据需要无限地增长,这意味着可以创建大量的线程。如果不加限制地创建线程,可能会导致系统资源被过度占用,导致性能下降或资源耗尽的风险。
  3. 默认拒绝策略可能不符合特定的需求:当线程池中的线程数达到最大值并且任务队列已满时,Executors 提供了一些默认的拒绝策略,例如抛出异常或丢弃任务。然而,这些默认策略可能不适合特定的应用场景,可能需要自定义拒绝策略来处理满载时的任务。
  4. 隐式创建的线程池可能难以管理和监控:使用 Executors 创建的线程池是隐式的,没有提供直接访问底层线程池实例的方法。这可能导致在需要对线程池进行精确管理、监控和调优时存在困难。

因此,在需要更精确的线程池配置和控制时,手动创建 ThreadPoolExecutor 可以提供更好的灵活性和控制。这样可以更好地满足特定需求,避免不合理的资源占用,并实现更好的性能和可靠性。

优缺点(线程池)

优点缺点
优化资源管理:线程池可以优化线程的创建与销毁,避免频繁创建和销毁线程带来的开销。资源占用:线程池会占用一定的系统资源,包括内存和CPU资源。
提高执行效率:线程池可以重用线程并行执行多个任务,从而提高执行效率。复杂性:线程池的实现较复杂,需要考虑线程池的大小、任务排队策略等。
控制并发数量:线程池可以限制同时执行的线程数量,从而避免资源过度使用和系统负载过高。任务阻塞:线程池在任务队列满时可能会导致新提交的任务等待执行,造成一定的延迟。
统一管理和监控:线程池提供统一的接口来管理和监控线程的执行情况,方便调优和排查问题。适用条件:线程池适用于需要频繁执行、数量较多的任务场景。对于任务较少或执行时间较长的场景,使用线程池可能没有明显的优势。

面试题

什么是线程池?为什么要使用线程池?

  • 线程池是一种用于管理和复用线程的机制。它通过预先创建一组线程,将任务交给这些线程来执行,从而避免了频繁创建和销毁线程的开销。线程池可以提高系统的性能和资源利用率。

Java中常见的线程池有哪些?它们之间有什么区别?

  • Java中常见的线程池有 FixedThreadPoolCachedThreadPoolSingleThreadExecutorScheduledThreadPool
  • FixedThreadPool 在初始化时创建固定数量的线程,适用于长期执行的任务。
  • CachedThreadPool 根据任务的需求动态地调整线程数量,适用于执行大量耗时较短的任务。
  • SingleThreadExecutor 只有一个线程的线程池,适用于需要保证任务按顺序执行的场景。
  • ScheduledThreadPool 可以延迟或定时执行任务的线程池。

线程池中的核心线程数、最大线程数和任务队列有什么作用?

  • 核心线程数是线程池保持的最小线程数量,即使线程是空闲的也会保持存活。
  • 最大线程数是线程池允许的最大线程数量。当线程池已经存在核心线程数,并且任务队列已满时,会创建新的线程直到达到最大线程数。
  • 任务队列用于存储尚未被执行的任务。线程池在执行任务时会从任务队列中取出任务进行处理。

线程池如何处理任务队列已满时的情况?

  • 当任务队列已满且核心线程数达到最大时,线程池会根据指定的拒绝策略来处理新提交的任务。

线程池的拒绝策略有哪些?请分别解释它们的作用。

  • 线程池的拒绝策略有:AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
  • AbortPolicy 是默认的拒绝策略,它在队列已满的情况下直接抛出 RejectedExecutionException
  • CallerRunsPolicy 将任务交给提交任务的线程去执行,这样可以降低提交任务的速度。
  • DiscardPolicy 直接丢弃无法处理的任务。
  • DiscardOldestPolicy 丢弃队列中最早的未处理任务,尝试重新提交当前任务。

什么是线程池的预启动?如何实现线程池的预启动?

  • 线程池的预启动是指在线程池初始化后,提前创建并启动一定数量的核心线程,使它们处于等待任务的状态。
  • 在 Java 中,通过在创建线程池时使用 prestartCoreThread()prestartAllCoreThreads() 方法来实现线程池的预启动。

线程池中的空闲线程如何管理?它们的存活时间是如何控制的?

  • 线程池中的空闲线程由线程池的控制机制来管理。它们会根据设定的存活时间,在任务执行完毕后保持存活并等待新任务的到来。
  • 存活时间由 keepAliveTime 参数来控制,当空闲线程的存活时间超过设定值时,线程池会销毁这些线程。

在使用线程池时,如何优雅地处理异常?

  • RunnableCallablerun() 方法中,可以使用 try-catch 语句来捕获并处理任务执行过程中的异常。
  • 此外,可以通过实现 ThreadFactory 接口,在创建线程时自定义线程的异常处理机制。

如何监控和调优线程池的性能?

  • 可以使用线程池提供的监控接口,如 getTaskCount()getActiveCount()getCompletedTaskCount() 来查看线程池的运行状态和执行任务的情况。
  • 可以通过调整线程池的参数,如核心线程数、最大线程数和任务队列的大小,以及合理选择拒绝策略来调优线程池的性能。

在什么情况下应该使用自定义线程池,而不是直接使用线程池工厂提供的默认线程池?

  • 使用自定义线程池可以更好地满足特定的业务需求。例如,对于执行长时间任务的场景,可以为线程池设置较大的最大线程数,以提高任务的处理速度。

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

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

相关文章

CVPR2023论文及代码合集来啦~

以下内容由马拉AI整理汇总。 下载&#xff1a;点我跳转。 狂肝200小时的良心制作&#xff0c;529篇最新CVPR2023论文及其Code&#xff0c;汇总成册&#xff0c;制作成《CVPR 2023论文代码检索目录》&#xff0c;包括以下方向&#xff1a; 1、2D目标检测 2、视频目标检测 3、…

油画欣赏|《纯洁的梦乡》,心中的宇宙

《纯洁的梦乡》 80x60cm 陈可之2021年绘 油画《纯洁的梦乡》&#xff0c;以绮丽的想象&#xff0c;精湛的技艺&#xff0c;描绘出梦境中的蜻蜓、山、水、星空等事物&#xff0c;展现出一个纯洁美好的心灵宇宙世界。 油画采用水平构图的方式&#xff0c;在位置偏低的山脚设置水…

LeetCode--HOT100题(37)

目录 题目描述&#xff1a;104. 二叉树的最大深度&#xff08;简单&#xff09;题目接口解题思路代码 PS: 题目描述&#xff1a;104. 二叉树的最大深度&#xff08;简单&#xff09; 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远…

全流程R语言Meta分析核心技术

Meta分析是针对某一科研问题&#xff0c;根据明确的搜索策略、选择筛选文献标准、采用严格的评价方法&#xff0c;对来源不同的研究成果进行收集、合并及定量统计分析的方法&#xff0c;最早出现于“循证医学”&#xff0c;现已广泛应用于农林生态&#xff0c;资源环境等方面。…

java 中的 JSP

程序框架 1. c/s框架&#xff1a; 通过客户端程序访问服务器 2.B/S框架 通过浏览器访问应用程序 &#xff1a;采用请求/相应模式进行交互 URL Tomcat服务器 1.开源项目 2.轻量级应用服务器 3.开源&#xff0c;稳定&#xff0c;资源占用小 Tomcat目录介绍 1.bin &#xff1a;存…

无涯教程-PHP - preg_match_all()函数

preg_match_all() - 语法 int preg_match_all (string pattern, string string, array pattern_array [, int order]); preg_match_all()函数匹配字符串中所有出现的模式。 它将按照您使用可选输入参数order指定的顺序将这些匹配项放置在pattern_array数组中。有两种可能的类…

EasyExcel 导出报空指针NullPointerException

java.lang.NullPointerException: null at sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1264) at sun.awt.FontConfiguration.readFontConfigFile(FontConfiguration.java:219) 这是jdk缺少字体库问题 在官网也给出解决答案&#xff1a; 1.安装少了字体库…

三星Galaxy S23与iPhone 15的对比分析:谁会胜出?

三星Galaxy S23与iPhone 15的对决将于下个月进入高潮,这将是今年智能手机中最大的一场较量。毕竟,这是两家领先的移动设备制造商的旗舰手机。他们的手机的比较将在很大程度上决定谁能获得最佳手机的称号。 我们已经知道有利于三星Galaxy S23的情况,该产品自春季以来一直在推…

github访问加速--工具

1.Fetch GitHub Hosts https://github.com/Licoy/fetch-github-hosts 点击启动即可访问github加速了 二、同类软件 SwitchHosts https://github.com/Python3WebSpider/ProxyPool

300 倍的性能提升!PieCloudDB Database 优化器「达奇」又出新“招”啦

随着数字化进程的加快&#xff0c;全球数据量呈指数级增长&#xff0c;对数据库系统的性能提出了巨大的挑战。优化器作为数据库管理系统中的关键技术&#xff0c;对数据库性能和效率具有重要影响&#xff0c;它通过对用户的查询请求进行解析和优化来生成高效的执行计划&#xf…

FSC147数据集格式解析

一. 引言 在研究很多深度学习框架的时候&#xff0c;往往需要使用到FSC147格式数据集&#xff0c;若要是想在自己的数据集上验证深度学习框架&#xff0c;就需要自己制作数据集以及相关标签&#xff0c;在论文Learning To Count Everything中&#xff0c;该数据集首次被提出。 …

基于微信小程序的中医体质辨识文体活动的设计与实现(Java+spring boot+MySQL)

获取源码或者论文请私信博主 演示视频&#xff1a; 基于微信小程序的中医体质辨识文体活动的设计与实现&#xff08;Javaspring bootMySQL&#xff09; 使用技术&#xff1a; 前端&#xff1a;html css javascript jQuery ajax thymeleaf 微信小程序 后端&#xff1a;Java s…

如何进行在线pdf转ppt?在线pdf转ppt的方法

在当今数字化时代&#xff0c;PDF文件的广泛应用为我们的工作和学习带来了巨大的便利。然而&#xff0c;有时候我们可能需要将PDF转换为PPT文件&#xff0c;以便更好地展示和分享内容。在线PDF转PPT工具因其操作简便、高效而备受欢迎。如何进行在线pdf转ppt呢?接下来&#xff…

使用Jetpack Compose的镜像效果

使用Jetpack Compose的镜像效果 您是否曾想过在列表或一般情况下为图像创建镜像效果&#xff1f;有了强大的Jetpack Compose UI工具包&#xff0c;这变得简单而容易。 正如您所看到的&#xff0c;此效果包括以下内容 反转图像反转图像的50&#xff05;可见性模糊的反转图像与…

openGauss学习笔记-46 openGauss 高级数据管理-子查询

文章目录 openGauss学习笔记-46 openGauss 高级数据管理-子查询46.1 SELECT语句中的子查询使用46.2 INSERT语句中的子查询使用46.3 UPDATE语句中的子查询使用46.4 DELETE语句中的子查询使用 openGauss学习笔记-46 openGauss 高级数据管理-子查询 子查询或称为内部查询&#xf…

实战演练 | Navicat 导出向导

数据库工具中的导入导出功能是指将数据从一个数据库系统导出到另一个数据库系统&#xff0c;或者将数据从一个文件格式导出到另一个文件格式。导入导出功能可以通过各种方式实现&#xff0c;例如使用SQL语句、数据库管理工具或第三方库和工具。在进行数据迁移时&#xff0c;通常…

AMBA总线协议(6)——AHB(四):传输细节

一、前言 在之前的文章中&#xff0c;我们已经讲述了AHB传输中的两种情况&#xff0c;基本传输和猝发传输。我们进行一个简单的回顾&#xff0c;首先&#xff0c;开始一次传输之前主机需要向仲裁器申请获得总线的使用权限&#xff0c;然后主机给出地址和控制信号&#xff0c;根…

冠达管理:沪指失守3100点 大盘加速赶底

8月21日&#xff0c;A股商场缩量跌落&#xff0c;上证指数尾盘失守3100点。券商股领跌&#xff0c;环保板块逆势掀涨停潮。北向资金净卖出64.12亿元&#xff0c;这是自8月7日以来连续第11个交易日净卖出。 从盘面看&#xff0c;早盘三大指数小幅低开&#xff0c;以券商股为主的…

前端找工作,有没有什么推荐的vue项目?

前言 可以参考一下下面的项目&#xff0c;已经作了vue2和vue3的分类&#xff0c;可以自己去选择适合的&#xff0c;希望对你有帮助~ Vue2 PC项目 1、 Elemen Star&#xff1a;53.4k 是一个基于Vue.js 2.0的UI组件库&#xff0c;由饿了么前端团队开发和维护。该组件库提供了…

文旅景区vr体验馆游乐场vr项目是什么

我们知道现在很多的景区或者游玩的地方&#xff0c;以及学校、科技馆、科普馆、商场或公园或街镇&#xff0c;都会建一些关于游玩以及科普学习的项目。从而增加学习氛围或者带动人流量等等。这样的形式&#xff0c;还是有很好的效果呈现。 普乐蛙VR体验馆案例 下面是普乐蛙做的…