《知识点扫盲 · 线程池基础篇》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录

    • 线程池运用背景
    • 线程池技术简介
    • 线程池核心参数
    • Executors 四种线程池
    • 线程池知识补充

CSDN.gif

线程池运用背景

|- 为什么用?
创建一个新的线程可以通过继承Thread类或者实现Runnable接口来实现,这两种方式创建的线程在运行结束后会被虚拟机销毁,进行垃圾回收,如果线程数量过多,频繁的创建和销毁线程会浪费资源,降低效率。而线程池的引入就很好解决了上述问题,线程池可以更好的创建、维护、管理线程的生命周期,做到复用,提高资源的使用效率,也避免了开发人员滥用new关键字创建线程的不规范行为。

说明:阿里开发手册中明确指出,在实际生产中,线程资源必须通过线程池提供,不允许在应用中显式的创建线程。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

|- 传统的线程使用
需要使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间(即线程创建和销毁的时间都超过了任务本身的执行时间,那还不如直接同步执行任务)。若线程数超过一定数量,还可能导致有的线程无法得到执行,甚至程序挂掉。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
Tips:和上面一个

|- 线程池的作用
线程池就是用来管理线程的工具。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
在开发过程中,合理地使用线程池能够带来3个好处:
第一:降低资源消耗,通过重复利用已创建的线程,降低在创建和销毁线程上所花的时间以及系统资源的开销;
第二:提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行;
第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。可以根据系统的承受能力,调整线程池中工作线线程的数目,使用线程池可以进行统一分配、调优和监控(默认情况下,一个线程的栈要预留1M的内存空间)。

总结:一句话就是线程池可以更合理的利用线程,控制线程数量,提升系统稳定新。

|- 什么场合要使用线程池
1、单个任务处理的时间比较短 ;
2、将需处理的任务的数量大 ;
说明:即常说的高并发低耗时的情况,像长时间开着的某个任务就不适合线程池。
通俗来说,线程池,就是在调用线程的时候初使化一定数量的线程,有线程过来的时候,先检测初使化的线程还有空的没有,没有就再看当前运行中的线程数是不是已经达到了最大数,如果没有,就新分配一个线程去处理,就像餐馆中吃饭一样,从里面叫一个服务员出来;但如果已经达到了最大数,就相当于服务员已经用于了,那没得办法,另外的线程就只有等了,直到有新的“服务员”为止。线程池的优点就是可以管理线程,有一个高度中枢,这样程序才不会乱,保证系统不会因为大量的并发而因为资源不足挂掉。


线程池技术简介

|- JDK1.5与线程池
线程的使用在Java中占有极其重要的地位,在JDK1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在JDK1.5之后这一情况有了很大的改观。JDK1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。

|- 线程池涉及的类
image.png
1、Executor 是一个顶层接口,在它里面只声明了一个方法 execute(Runnable),返回值为void,参数为Runnable类型,代表用来执行传进去的任务,严格意义上讲,Executor并不是一个线程池,而只是一个执行线程的工具;

package java.util.concurrent;public interface Executor {void execute(Runnable var1);
}

2、ExecutorService 继承并扩展了Executor接口,提供了 submit、shutdown等方法扩展;
3、AbstractExecutorService 抽象类实现了 ExecutorService 接口,基本实现了 ExecutorService 中声明的所有方法;
4、ThreadPoolExecutor 是线程池的核心实现类,继承了类 AbstractExecutorService,在 ThreadPoolExecutor 类中有几个非常重要的方法,比如 execute,实际上是Executor中声明的方法,在 ThreadPoolExecutor 进行了具体的实现,通过这个方法可以向线程池提交一个任务,交由线程池去执行;
5、ScheduledExecutorService继承ExecutorService接口,并定义延迟或定期执行的方法;
6、ScheduledThreadPoolExecutor继承ThreadPoolExecutor并实现了ScheduledExecutorService接口,是延时执行类任务的主要实现;

Tips:ThreadPoolExecutor 是线程池的核心实现类,最常用。

|- 线程池运行原理
image.png


线程池核心参数

背景说明
在JDK帮助文档中,有如此一段话:

“强烈建议程序员使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池) Executors.newSingleThreadExecutor()(单个后台线程)它们均为大多数使用场景预定义了设置。”

但是,在观看阿里巴巴开发规约的时候,建议程序员使用精确的属性去构造,如下:

(强制)线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 创建线程池方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

说明:Executors的newFixedThreadPool等方法,实际上也是一些默认参数的ThreadPoolExecutor构造,之所以要直接使用ThreadPoolExecutor还是想让使用者更了解这些参数,选择更合适的方式。

ThreadPoolExecutor 构造方法
Executors 接口提供的四种创建线程池的方法,底层都是调用ThreadPoolExecutor的构造方法。
ThreadPoolExecutor 类中提供了四个构造方法,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。
第四个构造方法的属性也是最全的,方法体代码如下:

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

corePoolSize(基本线程数)
核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。
默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
线程池中最核心的线程池数据量,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。

maximumPoolSize(最大线程数)
线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
线程池中最大的线程数量,如果线程池中的线程数据小于最大数,并且提交新的任务,此时会创建新的线程来执行,直到达到最大线程数,再提交新的任务,此时会执行到对应的队列里面,后面的参数会讲到,队列满了在执行相应的策略,后面的参数会讲到。

keepAliveTime(超过基本线程数的空闲线程多久关闭)
1、表示线程没有任务执行时最多保持多久时间会终止。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
2、线程活动保持时间,线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
3、当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。说的让人感觉比较模糊,总结一下大概意思为:比如说线程池中最大的线程数为50,而其中只有40个线程任务在跑,相当于有10个空闲线程,这10个空闲线程不能让他一直在开着,因为线程的存在也会特别好资源的,所有就需要设置一个这个空闲线程的存活时间,这么解释应该就很清楚了。

unit(上面那个属性的单位)
参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:TimeUnit.DAYS 等;
线程活动保持时间的单位,该时间是针对keepAliveTime的时间单位,可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

workQueue:
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
任务队列,用于保存等待执行的任务的阻塞队列。后面再详细的讲解各个队列的特点。

threadFactory(执行程序创建新线程时使用的工厂):
线程工厂,主要用来创建线程,用于新提交的任务,并且线程池中没有空闲的线程,并且线程池的大小没有达到最大线程池数据量时需要创建的线程,由该线程工程类创建线程。

handler(由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序)
表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
执行策略,执行策略是在一定的前提下执行的,当线程池的线程数量已经达到最大值,并且队列已经达到最大值时,对新提交的线程任务进行的反应策略,后面在详细的讲解执行策略的问题。

Executors 四种线程池

|- 前言
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。|- newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行(先进先出)。
底层实现:new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
底层分析:核心线程数和最大线程数都是1,空闲线程保持时间为0(意思就是不缓存),队列采用LinkedBlockingQueue。|- newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
底层实现:new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
底层分析:核心线程数和最大线程数一样,空闲线程保持时间为0(意思就是不缓存),队列采用LinkedBlockingQueue。
说明:定长线程池的大小最好根据系统资源进行设置,如 Runtime.getRuntime().availableProcessors(),几核就代表需要几个线程;|- newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
底层实现:new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
底层分析:可以看到核心线程数为0,最大线程数无限大,回收空闲线程时间60秒,缓存队列使用SynchronousQueue(只允许放一个即被阻塞),这种线程池有任务来就会一直创建新的线程,对性能损耗较大,但是处理效率最高。
说明:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
说明:不太适合非常频繁的场景,等下服务器挂了。|- newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求(schedule方法,延迟N秒执行)。
底层实现:super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());

线程池知识补充

|- 阻塞队列和非阻塞队列阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列。ConcurrentLinkedDeque 是典型的无界非阻塞队列,是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素(简单了解即可)。BlockingQueue 是典型的阻塞队列,在Java中,BlockingQueue的接口位于java.util.concurrent 包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。在线程池中,底层使用阻塞队列BlockingQueue实现。|- 常见阻塞队列(缓存队列)
SynchronousQueue:队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
LinkedBlockingQueue:阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据。
补充:Integer.MAX_VALUE最大值,值为 2的31次方-1 的常量,它表示 int 类型能够表示的最大值 214748364,基本无限大。|- 任务和线程的区别
线程可以理解为用来执行任务的程序,任务就是你要做的事情,大多时候不用去区分。
并发队列里面存放的是线程而不是任务,可以看到线程池的execute方法的作用就是提交线程任务。
其参数就是一个Runnable接口,也就是线程,意思就是提交一个线程任务,任务内容就是run方法里面的。

CSDN_END.gif

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

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

相关文章

【node】Linux下安装node和npm

Linux下安装node和npm 下面的版本虽然安装失败了&#xff0c;第一次尝试不容易&#xff0c;只需要更换一下node的版本为v16.20.2即可安装成功&#xff0c;20这样的高版本对大部分linux服务器来讲还是版本太高了&#xff0c;GLIBC动态库不支持&#xff0c;升级颇为麻烦&#xff…

她是军统美女特工,色诱汉奸一把好手!一件事之后竟......

一.前言 我们在上一篇里简单了解了什么是树&#xff0c;以及树的一种特殊结构——二叉树。而我们对二叉树息息相关的堆进行了简单的介绍。我们知道了堆是借助二叉树中完全二叉树来实现的。它实现了二叉树的顺序存储。但对于普通的二叉树来说&#xff0c;顺序存储会造成空间浪费…

贪心+背包

这道题比较坑的就是我们的对于相同截止时间的需要排个序&#xff0c;因为我们这个工作是有时间前后顺序的&#xff0c;我们如果不排序的话我们一些截止时间晚的工作就无法得到最优报酬 #include<bits/stdc.h> using namespace std;#define int long long int t; int n; c…

看板项目之vue代码分析

目录&#xff1a; Q1、vue项目怎么实现的输入localhost&#xff1a;8080就能自动跳到index页面Q2、组合饼状图如何实现Q3、vue项目如何实现环境的切换Q4、vue怎么实现vue里面去调用js文件里面的函数 Q1、vue项目怎么实现的输入localhost&#xff1a;8080就能自动跳到index页面 …

数据结构——串

语言&#xff1a;C语言软件&#xff1a;Visual Studio 2022笔记书籍&#xff1a;数据结构——用C语言描述如有错误&#xff0c;感谢指正。若有侵权请联系博主 一、串的基本概念 子串&#xff1a;串中任意连续的字符组成的子序列称为该串的子串。 主串&#xff1a;包含子串的串称…

做一个能和你互动玩耍的智能机器人之三

内容节选自英特尔的开源项目openbot的body目录下diy下的readme&#xff0c;这是一个组装和连线方式的说明文档&#xff0c;接线需要配合firmware固件使用&#xff0c;固件代码的接线柱是对应的。 body目录内部十分丰富&#xff0c;主要介绍了这个项目的背景和硬件以及如何让他…

【SQL 新手教程 4/20】关系模型 --索引

&#x1f497; 关系数据库建立在关系模型上⭐ 关系模型本质上就是若干个存储数据的二维表 记录 (Record)&#xff1a; 表的每一行称为记录&#xff08;Record&#xff09;&#xff0c;记录是一个逻辑意义上的数据 字段 (Column)&#xff1a;表的每一列称为字段&#xff08;Colu…

echarts实现在市级行政区点击县级行政区,显示单个县级行政区地图数据

因需兼容ie&#xff0c;此处所有变量声明都用var。如无需支持&#xff0c;可另做let修改。 这里以常州市为例,我们可以去阿里云提供的地理工具去截取地图json数据DataV.GeoAtlas地理小工具系列 点击所选区域&#xff0c;右侧会对应显示json数据&#xff0c;再次点击右侧红框内…

MySQL 索引相关基本概念

文章目录 前言一. B Tree 索引1. 概念2. 聚集索引/聚簇索引3. 辅助索引/二级索引4. 回表5. 联合索引/复合索引6. 覆盖索引 二. 哈希索引三. 全文索引 前言 InnoDB存储引擎支持以下几种常见索引&#xff1a;BTree索引&#xff0c;哈希索引&#xff0c;全文索引 一. B Tree 索引…

2024巴黎奥运会竟然用AI做这些?

人工智能将成为 2024 年巴黎奥运会的焦点&#xff0c;组织者于四月制定了《奥运会人工智能议程》&#xff0c;这是一个涵盖人工智能对奥运会未来影响的框架。 该议程体现了国际奥委会及其主要合作伙伴的承诺&#xff0c;确保在奥运会上使用人工智能来促进团结、提高可持续性并加…

从零到一使用 Ollama、Dify 和 Docker 构建 Llama 3.1 模型服务

本篇文章聊聊&#xff0c;如何使用 Ollama、Dify 和 Docker 来完成本地 Llama 3.1 模型服务的搭建。 如果你需要将 Ollama 官方不支持的模型运行起来&#xff0c;或者将新版本 llama.cpp 转换的模型运行起来&#xff0c;并且想更轻松的使用 Dify 构建 AI 应用&#xff0c;那么…

网络传输层——UDP与TCP

前言&#xff1a; 1.国际网络体系结构&#xff1a; OSI模型: open system interconnect 理论模型 1977 国际标准化组织 各种不同体系结构的计算机能在世界范围内互联成网。 应用层:要传输的数据信息&#xff0c;如文件传输&#xff0c;电子邮件等…

数据结构:队列(顺序存储和链式存储)

文章目录 1. 队列的概念和结构2. 队列的链式存储实现2.1 初始化2.2 判断队列是否为空2.3 入队列2.4 出队列2.5 取队头数据2.6 取队尾数据2.7 队列有效数据的个数2.8 打印队列数据2.9 销毁2.10 源代码 3. 队列的顺序存储实现(循环队列)3.1 初始化3.2 判断队列是否为空3.3 判断队…

【数据结构之C语言实现动态顺序表】

引 入: 在讲顺序表之前得先了解线性表是什么&#xff1f; 线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表&#xff0c;链表&#xff0c;栈&#xff0c;队列&#xff0c;字符串…… 线性表…

Meta 发布地表最大、最强大模型 Llama 3.1

最近这一两周看到不少互联网公司都已经开始秋招提前批了。不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友解…

【iOS】暑期第一周——ZARA app仿写

目录 前言无限轮播图分栏控件和滚动视图自定义cell遇到的问题调整图标大小单元格附件视图设置 总结 前言 暑假学习的第一周任务是对ZARA app进行仿写&#xff0c;充分运用之前学习的Objective-C语言和UI控件。我在编写demo的过程中遇到了一些问题&#xff0c;特写该博客作为学习…

LLM与搜索推荐

重磅推荐专栏: 《大模型AIGC》 《课程大纲》 《知识星球》 本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经…

VScode连接服务器免密登录

1、生成 SSH 密钥对 打开终端并输入以下命令生成 SSH 密钥对&#xff1a; 直接搜索 cmd&#xff0c;然后输入&#xff1a; ssh-keygen -t rsa -b 4096 一直回车就好了 这时公钥存储在/Users/你的用户名/.ssh/id_rsa.pub文件里&#xff0c;私钥存储在/Users/你的用户名/.ss…

简单的数据结构:栈

1.栈的基本概念 1.1栈的定义 栈是一种线性表&#xff0c;只能在一端进行数据的插入或删除&#xff0c;可以用数组或链表来实现&#xff0c;这里以数组为例进行说明 栈顶 &#xff1a;数据出入的那一端&#xff0c;通常用Top表示 栈底 :相对于栈顶的另一端&#xff0c;也是固…

黑马头条vue2.0项目实战(一)——项目初始化

1. 图标素材&#xff08;iconfont简介&#xff09; 制作字体图标的工具有很多&#xff0c;推荐使用&#xff1a;iconfont-阿里巴巴矢量图标库。 注册账户 创建项目 可以根据项目自定义 class 前缀 上传图标到项目 生成链接&#xff0c;复制 css 代码&#xff0c;在项目中使用…