Java多线程基础-11:工厂模式及代码案例之线程池

news2024/9/21 10:42:38

在Java中,xx池的概念是很常见的,比如之前遇到过的常量池、数据库连接池等等。 

线程池是一种常用的多线程处理方式,它可以重复利用已创建的线程,从而减少线程的创建和销毁开销,并提高程序的性能。

通俗来说,线程池就是提前把线程准备好。当有任务要执行时,原本我们会直接创建一个线程(从系统申请一个线程的资源),然后让这个线程来执行这个任务。但有了线程池后,创建线程不是直接从系统申请,而是从池子里拿;线程不用了,也是还给池子。

池存在的目的是为了提高效率。线程的创建虽然比进程轻量,但是在频繁创建的情况下,开销也是不可忽略的。

线程池最大的好处就是减少了每次启动、销毁线程的损耗。

其实解决频繁创销线程带来的开销这个问题,可以用线程池,也可以用协程。协程是轻量级的线程,不过Java的标准库还不支持(Java只在第三方库的层面上支持协程)。

目录

一、为什么从线程池中获取线程更高效?

⭐线程池的优点

二、Java标准库中的线程池 ExecutorService

1、Executors类与线程池的创建

(1)调用Executors类静态方法创建线程池

2、工厂模式

3、ThreadPoolExecutor类

4、标准库提供的4种拒绝策略⭐

二、代码实现线程池

1、代码解析

2、实际开发中如何给线程池设置合适的线程数量


一、为什么从线程池中获取线程更高效?

为什么从池子里拿线程要比从系统创建线程更高效?

这是因为,从人程池拿线程是纯粹的用户态操作;而从系统创建线程,就涉及到用户态和内核态之间的切换(真正的创建是要在内核态完成的)。

用户态、内核态是操作系统中的基本概念。

一个操作系统 = 内核 + 配套的应用程序

内核是操作系统最核心的功能模块的集合,如硬件管理,各种驱动,进程管理,内存管理,文件系统等等。这个内核要给上层的应用程序提供支持。比如在显示器上打印hello的操作: println("hello"),应用程序就要调用系统内核,告诉内核我要进行一个打印字符串操作,内核再通过驱动程序操作显示器,完成上述功能。

然而,同一时刻应用程序可能有很多,但内核始终只有一个。内核又要给这么多程序提供服务,这就导致了有的时候服务不一定那么及时。

想象一个银行的工作场景:

这里的柜台内部就相当于内核态,而大厅相当于用户态。

有个办业务的滑稽大哥和柜台的工作人员说,想办一张银行卡,但却发现没有带身份证复印件。这时,工作人员给了他两个方案:要么自己拿着身份证到大厅的复印机复印,要么把身份证交给工作人员,由工作人员在柜台内的复印机复印。

这两种方式在效率上是有差别的:

1、如果自己去复印,快去快回,复印完之后立即就回来了。

2、如果由工作人员复印,他可能去复印的同时还顺便干点别的,比如喝口水,上个厕所,完成一下上级给的别的任务,和同事唠唠嗑……最终确实也能给滑稽大哥复印,但可能就没那么及时了。

总结起来就是,纯用户态的操作,时间是可控的;但涉及到内核态操作,时间就不太可控了。

⭐线程池的优点

  1. 降低资源消耗:减少线程的创建和销毁带来的性能开销。

  2. 提高响应速度:当任务来时可以直接使用,不用等待线程创建。

  3. 可管理性: 进行统一的分配,监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。


二、Java标准库中的线程池 ExecutorService

ExecutorService是 Java 中用于管理和执行异步任务的接口,它是一个高级的线程池管理工具,继承自 Executor 接口。ExecutorService 接口定义了一组方法,可以用于提交任务、执行任务、关闭线程池等。常用的方法如:

  1. submit(Runnable task):提交一个 Runnable 任务给线程池执行。
  2. submit(Callable<T> task):提交一个 Callable 任务给线程池执行。
  3. execute(Runnable task):执行一个 Runnable 任务。
  4. shutdown():平缓地关闭线程池,允许已经提交的任务执行完毕。
  5. shutdownNow():立即关闭线程池,尝试中断正在执行的任务,并返回尚未开始执行的任务列表。

通过使用 ExecutorService,我们可以将任务提交给线程池,由线程池自动分配和执行任务。线程池会管理线程的创建、复用和销毁,使多线程任务的执行更高效可控。

1、Executors类与线程池的创建

Java中通过调用Executors类的各类静态工厂方法来创建线程池。

这些方法返回 ExecutorService 接口的实例,即线程池实例。ExecutorService 也提供了一些提交任务和管理线程池的方法,如submit()shutdown()等。

(1)调用Executors类静态方法创建线程池

Executors类是 java.util.concurrent(简称 juc) 包下的一个工具类,它提供了创建和管理线程池的各种方法。使用 Executors类 可以方便地创建和管理线程池,实现多线程编程以及控制线程的执行方式、数量和生命周期。

以下是创建线程池的代码示例:创建好线程池后,通过pool.submit()方法,向线程池中注册任务。

//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
//添加任务到线程池中
pool.submit(() -> {
    System.out.println("hello");
});
  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池pool。
  • 返回值类型为 ExecutorService,是Java标准库中所提供的线程池接口。
  • 通过 ExecutorService.submit() 可以注册一个任务到线程池中。

Executors类提供了各种静态工厂方法,用于创建不同类型的线程池,常用的方法有:

  1. newFixedThreadPool(int nThreads)创建一个固定大小的线程池,该线程池中最多同时执行nThreads个任务。

  2. newCachedThreadPool()创建一个线程数目动态增长的线程池。它是一个可缓存的线程池,该线程池根据需要创建新线程,但在先前创建的线程可用时将重用它们。

  3. newSingleThreadExecutor()创建只包含单个线程的线程池。该线程池只会同时执行一个任务。

  4. newScheduledThreadPool(int corePoolSize)创建一个固定大小的线程池,该线程池可以安排任务在指定延迟后执行,或定期执行是进阶版的 Timer。

Executors类提供创建线程池的全部方法

不同方法创建出的线程池具有不同的特性。这些特性决定了当外部线程向线程池提交一个任务后,该线程池将如何调度其中的工作线程来处理这个任务。

值得注意的是:在创建线程池pool时,并非直接new了一个ExecutorService对象,而是通过了Executors类里面的静态方法完成对象的构造,这样的静态方法就是前面提到的静态工厂方法。new操作隐藏在了这个方法背后。这里就涉及到了一种常见的设计模式:工厂模式。

2、工厂模式

工厂模式在创建对象时不再直接new,而是使用一些其他的方法(通常是静态方法)协助我们把对象创建出来。

事实上,工厂模式更多地是用来填构造方法的“坑”的。我们知道,如果同一个类要想提供多种不同的构造对象的方式,就得基于构造方法的重载。但是构造方法是有局限性的。比如下面这种情况:将直角坐标系中的一个点封装成一个类,分别提供两个构造方法,其一是通过横坐标x和纵坐标y来表示这个点的位置,其二是用极坐标来表示这个点的位置。根据不同的构造方式写出的两个构造方法。然而,这两个方法的签名完全相同,它们并不能构成重载。

如果没有遇到这样的问题,那自然而然也就不需要工厂模式。但一旦遇到这样的问题,工厂模式就可以解决。即拿一个普通的static方法来替代。

构造一个工厂类 PointBuilder (一般工厂类的命名都是XxxxBuilder,不过上面的 Executors 是一个例外。)

上面的工厂类中,两个方法的方法名可以自定义。方法名不一样了,上述问题自然迎刃而解。

总而言之,工厂模式(Factory Pattern)是一种创建型设计模式,它提供了一种封装对象创建过程的方式。在工厂模式中,我们通过使用工厂类来创建对象,而不是直接在客户端代码中使用 new 关键字实例化对象。

工厂模式的主要目标是将对象的创建和使用分离,通过引入一个工厂类来负责创建对象。这样可以降低客户端代码和具体对象之间的耦合度,使得代码更具灵活性和可维护性。

这里Java中创建线程池也是类似,通过工厂模式来构造出了线程池。

进入 Executors.newFixedThreadPool() 的源码可以看到,确实是在工厂类中new了一个对象:

3、ThreadPoolExecutor类

ThreadPoolExecutor类是 Java 中用于创建和管理线程池的一个强大工具类。它是 ExecutorService 接口的一个实现,提供了更灵活的线程池功能。

ThreadPoolExecutor类是原装的线程池类,上述所有的工厂方法都是对这个类的对象做了进一步封装。

打开Java官方文档,找到 java.util.concurrent,再找其下的 ThreadPoolExecutor 类。这里就能看到Java官方对这个类的详细说明。

ThreadPoolExecutor是ExecutorService接口的一个具体实现,它提供了更加灵活的线程池功能。它有多个构造方法:

这些构造方法中都有比较多的参数。我们以最后一个构造方法为例,解读一下该构造方法中的各个参数。

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

参数说明:

  • corePoolSize:核心线程数,即线程池中始终保持存活的线程数(即使它们处于空闲状态)。
  • maximumPoolSize:线程池中允许的最大线程数,包括核心线程和非核心线程。
  • keepAliveTime:非核心线程保持存活的时间。非核心线程的空闲时间超过该值后将被终止。
  • unit:用于指定空闲时间的时间单位。
  • workQueue:用于保存等待执行任务的阻塞队列。线程池要管理很多任务,这些任务也是通过阻塞队列来组织的。程序员可以手动指定给线程池一个队列,次吃程序员就能很方便地控制或获取队列中的信息了。(submit()方法其实就是把任务放到该队列中。)
  • threadFactory:工厂类,一个用来创建线程的辅助的类(可以用来自定义线程池中线程的属性、命名、优先级等)。
  • handler:线程池的拒绝策略。即:如线程池已经满了,继续向里添加任务,如何拒绝。

在理解时,我们可以把线程池想象成一个公司,公司中有两类员工,一类是正式员工,另一类是实习生或临时工。线程池中核心线程,指的就是正式员工;非核心线程,指的则是实习生。忙的情况下,公司就多招几个实习生干活,就好比线程池中多创建几个非核心线程共同完成任务;等到闲下来了,为了节省资源,公司又将实习生辞退,相应地非核心线程也会被杀死。

corePoolSize就相当于正式员工的数量,而maximumPoolSize相当于正式员工+实习生的数量。

正式员工是签了劳动合同的,不能随意辞退,所以即使核心线程处于空闲状态也不会被销毁;而实习生并没有签劳动合同,只是签了实习合同,是可以随时辞退的。keepAliveTime就规定了实习生线程保持存活的时间。

上面的这些参数中,线程池的拒绝策略是重点。

4、标准库提供的4种拒绝策略⭐

下面是标准库提供的四种拒绝策略。

  • ThreadPoolExecutor.AbortPolicy 直接抛异常:如果线程池满了还在继续加任务,添加操作就直接抛出异常,新任务和老任务都执行不了了。(你一个礼拜周一到周日满课,天天早八到晚十,结果班长还要求你去打扫办公室。你一听这个消息,直接绷不住了,哇得一声嚎啕大哭。)
  • ThreadPoolExecutor.CallerRunsPolicy 添加任务的线程自己负责执行这个任务,即在哪个线程中写了submit()或execute()等向线程池中添加任务的方法,就由哪个线程来执行它要添加的任务。(你直接怼回去:我才不去呢,要去你自己去。)
  • ThreadPoolExecutor.DiscardOldestPolicy 丢弃最老的任务,即阻塞队列队首元素,不执行了,直接删除。(你看了一下课表,决定把最后一节课鸽了去打扫。)
  • ThreadPoolExecutor.DiscardPolicy 丢弃最新的任务,只做原来的任务。(你还是继续上你的课,打扫办公室这个任务就直接丢弃了。)

注意,这里线程池并没有依赖于阻塞队列的阻塞行为,而是通过额外实现其它逻辑来更好地处理这个场景的操作。就好比班长告诉你你要去打扫卫生,然后他就阻塞住了,他也干不了别的你也干不了别的……最好的情况应当是你立即给出答复。在线程池中,并不希望依赖“满了阻塞”,而更主要是利用“空了阻塞”。

关于各个拒绝策略的具体场景,可以参考这篇文章:

🔗线程池的拒绝策略


二、代码实现线程池

下面代码实现了一个固定线程数的简单的线程池:

import java.util.*;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    //阻塞队列用来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    //提交任务
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    //实现一个固定线程数的线程池
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        //取出线程池中的一个任务
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int num = i;    //lambda表达式的变量捕获规则
            myThreadPool.submit(()->{
                System.out.println("hello " + num);
            });
        }
        Thread.sleep(3000);
    }
}

运行结果:

打印1000次

1、代码解析

核心的数据结构是BlockingQueue,它用于存放各个可执行的任务(runnable):

class MyThreadPool{
    //阻塞队列用来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    //...
}

submit()就像生产者一样,给队列中添加任务:

class MyThreadPool{
    //向线程池中提交任务
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    //...
}

在线程池内部有一组总数为n的工作线程,它就像消费者一样,不停地从队列中取任务然后执行。 

主线程中传入n为10,即线程数固定为10。在线程池构造方法中,通过for循环创建了10个工作线程。这10个工作线程是并发执行,无序调度的。每一个线程的任务都是不断从阻塞队列中获取任务并执行。因此这里为了保证工作线程的活跃,不会在执行完一个任务后立即终止线程,需要给取任务、执行任务的操作加上while(true)。如果没有while(true),线程执行完一个任务后就会终止,导致线程池中的线程数量不足,无法处理后续的任务(运行结果中只能打印10次)。

class MyThreadPool{
    //实现一个固定线程数的线程池
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        //取出线程池中的一个任务
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }

    //...
}

主线程中,创建出线程池,并通过循环向其中添加1000个任务。这1000个任务先后被添加到阻塞队列中,工作线程从阻塞队列中获取任务并执行。注意,这里num的创建是由于lambda表达式(或匿名内部类)的变量捕获规则,它要求lambda表达式中捕获到的变量必须是final或实际final的(即不能被更改),由于变量 i 被更改了,因此重新创建一个变量来保存 i,代替 i 来使用:

public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int num = i;
            myThreadPool.submit(()->{
                System.out.println("hello " + num);
            });
        }
        Thread.sleep(3000);
    }
}

2、实际开发中如何给线程池设置合适的线程数量

实际不同的程序中,线程需要干的活大不相同。一个线程池的线程数量设置成几是比较合适的?这需要结合具体的任务情况,测试而定。

  1. CPU密集型任务,主要做一些计算工作,要在 CPU 上运行。假设一个极端情况,如果你的线程执行的全是使用CPU资源的任务,那么线程数就不应该超过CPU的核心数(指逻辑核心)。
  2. IO密集型任务,主要是等待 IO 操作(等待读写硬盘,读写网卡等),不怎么消耗 CPU 资源。如果你的线程全是使用IO,线程数就可以设置很多,远远超出 CPU 核心数。

不过,实践中很少有这么极端的情况,具体要通过测试的方式来确定,选取一个性能上恰当且资源使用上也恰当的这样一个均衡的结果。

测试的大体思路是:运行程序,通过记录时间戳计算一下执行时间(平均值),同时监测资源使用状态。

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

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

相关文章

【Windows】Redis集群部署

集群是如何进行工作的 Redis采用哈希槽来处理数据与节点之间的映射关系&#xff0c;一个集群共有16384 个哈希槽&#xff0c;每个key通过 CRC16算法计算出一个16bit的值&#xff0c;再对16384取模&#xff0c;得到对应的哈希槽&#xff0c;集群通过维护哈希槽与节点的关系来得…

flutter聊天界面-Text富文本表情emoji、url、号码展示

flutter聊天界面-Text富文本表情emoji、url、号码展示 Text富文本表情emoji展示&#xff0c;主要通过实现Text.rich展示文本、emoji、自定义表情、URL等 一、Text及TextSpan Text用于显示简单样式文本 TextSpan它代表文本的一个“片段”&#xff0c;不同“片段”可按照不同的…

Verilog学习笔记2:反相器

代码&#xff1a; //反相器 timescale 1ns/10ps module inv( A,y); input A; output y; assign y~A; endmodule //testbench module inv_tb;reg A; wire YY;inv inv(.A(A),.y(YY));initial beginA<0;#10 A<1;#10 A<0; #10 A<1;#10 A<0; #10 A<1;#10 A<0;…

从小白到大神之路之学习运维第54天--------ELK日志收集分析

第三阶段基础 时 间&#xff1a;2023年7月6日 地 点&#xff1a;2304教室 授课人&#xff1a;李凤海 参加人&#xff1a;全班人员 内 容&#xff1a; ELK技术堆栈 目录 服务器设置&#xff1a; 部署elasticsearch集群&#xff1a; 配置elasticsearch集群&#xff…

怎样寻找SEO服务商,需要注意那些问题?

网上提供SEO服务的公司或个人多如牛毛&#xff0c;随便在搜索引擎上搜索一下&#xff0c;成千上万的公司就在眼前。大部分网站设计公司也兼职SEO服务&#xff0c;寻找合适的SEO服务商并不简单&#xff0c;鉴别合格的SEO服务商是关键。 1、确定外包任务 如果没有SEO团队&…

C++类相关概念

1. 函数形参默认值 &#xff08;1&#xff09; 建议函数&#xff08;不仅仅是构造函数&#xff09;形参默认值只在函数声明中指定&#xff1b; &#xff08;函数声明和定义写在同一个文件中&#xff0c;则函数声明、定义两者之一或两者都可指定形参默认值&#xff0c;两者都指定…

mybati执行流程源码分析

MyBatis执行流程源码分析 前言 由于之前写过关于mybatis源码的文章,但是感觉还是不够细致,不够完善.没有把一些具体的流程应用写下来,至此又写了一篇.内容可能比较长,希望大家可以打开源码跟着这篇文章一起看源码保证可以收获许多. 1.mybatis基础开发流程 1.引入mybatis相关…

短视频去水印小帮手微信小程序源码带后台

短视频去水印小帮手微信​小程序源码​带后台&#xff0c;短视频去水印系列教程服务端源码。 php版&#xff0c;这里不过多介绍&#xff0c;我假设您有基本的编码基础&#xff0c;并熟悉php语言及laravel框架。

Topaz Gigapixel AI v6 人工智能图像放大

其他软件或插件依赖于插值算法来放大图像&#xff0c;Topaz Gigapixel AI 则与众不同&#xff0c;它使用最先进的深度学习技术来扩大和增强图像。 它通过神经网络分析了数百万张照片&#xff0c;从而了解了不同的照片在放大时细节是如何损失的&#xff0c;以及如何为图像增强和…

四十三、贪心——Huffman树、排序不等式

算法主要内容 一、Huffman树1、题目内容——合并果子2、算法思路&#xff08;1&#xff09;“合并果子”中的Huffman树&#xff08;2&#xff09;算法步骤&#xff08;3&#xff09;状态转移 3、题解 二、排序不等式1、题目内容——排队打水2、算法思路&#xff08;1&#xff0…

【MySQL】数据库基础概念

文章目录 前言连接服务器什么是数据库&#xff1f;MySQL和MySQLdLinux中如何看到数据库文件SQL语句分类存储引擎 前言 今天继续讲解MySQL相关内容&#xff0c;本期主要讲解数据库的基础概念&#xff0c;方便后续学习数据库。 连接服务器 mysql -h [ip] -P [port] -u [root] …

IRIS在Linux下通过JDBC操作其他数据库

以前都是在Windows上用cache的SqlGateway通过odbc创建连接操作其他数据库。现在都用Linux了&#xff0c;那么和其他数据进行调用咋办呢。 可以看到是可以创建JDBC的连接的&#xff0c;而Java是跨平台的&#xff0c;所以可以用JDBC操作其他数据库&#xff0c;这次以mysql为例。…

浅谈Node.js中的npm和yarn

官方文档&#xff1a; npmhttps://www.npmjs.cn/ yarnhttps://yarn.bootcss.com/ npm和yarn的作用 yarn和npm都是构建和打包javascript代码的工具 区别 npm&#xff1a; 1npm使得js开发者易于分享其为解决特定问题而编写的代码&#xff0c;且可供其他开发者在他们自己的应…

【阶段学习小总结】

串口通信和CAN总线通信的区别 can总线和串口是两种不同的通信模式&#xff0c;注意CAN总线数据传输所用的虽然是DB9串口线&#xff0c;但它完全不是串口&#xff0c;这一点容易出错。 CAN总线通信是高速&#xff0c;可靠&#xff0c;灵活的一种通信协议。作为一种分布式通信方…

天梯赛注意事项

格式错误 有的时候题目隐含的条件是要你输出两行的&#xff0c;即使第二行什么也没有&#xff0c;也得输出。 答案错误可能涉及特殊值的处理 或者 题意与你本身的思维不一致 map定义在main外面 键自动赋值 it->first it->second mp自动按照下标排序 . 模拟出栈入栈操…

使用GTK创建简易计算器

使用GTK创建简易计算器 本文将介绍如何使用GTK&#xff08;GIMP Toolkit&#xff09;创建一个简单的计算器应用程序。通过这个例子&#xff0c;你将学习如何构建基本的图形用户界面&#xff0c;并了解GTK的一些常用组件和回调函数的使用。 准备工作 首先&#xff0c;确保你已…

Django之中间件

一、中间件介绍 官方的说法&#xff1a;中间件是一个用来处理Django的请求和响应的框架级别的钩子。它是一个轻量、低级别的插件系统&#xff0c;用于在全局范围内改变Django的输入和输出。每个中间件组件都负责做一些特定的功能。 但是由于其影响的是全局&#xff0c;所以需要…

Vivado2020.1 vitis使用:创建hello world项目

前言 之前网上的教程都是基于Vivado2018的&#xff0c;后来接手一个未完工的项目&#xff0c;是使用Vivado2020.1创建的&#xff0c;非常尴尬&#xff0c;只能打开&#xff0c;不能编辑。千辛万苦把2020.1安装好&#xff0c;当然此时不仅仅是Vivado2020.1了&#xff0c;而是vi…

gitee注册以及使用的简单教程

目录 1.gitee是什么&#xff1f; 2. gitee怎么注册? 3.gitee创建仓库 4.gitee怎么提交代码? 5. git的三板斧 1.gitee是什么&#xff1f; 基于Git的代码托管和研发协作平台上面可以托管个人或者公司的代码和开源项目。国外有github&#xff0c;国内有giteegithub经常出现…

(vue)el-table表头、内容居中

(vue)el-table表头、内容居中 效果&#xff1a; 表头、内容都居中 <el-table:data"gridData":header-cell-style"{text-align:center}":cell-style"{text-align:center}" >单个表格的内容居中&#xff1a; el-table-column上加上align‘c…