《JavaSE-第二十章》之线程 的创建与Thread类

news/2024/4/25 14:33:48/文章来源:https://blog.csdn.net/qq_60308100/article/details/132001910

文章目录

    • 什么是进程?
    • 什么是线程?
      • 为什么需要线程?
    • 基本的线程机制
      • 创建线程
        • 1.实现 Runnable 接口
        • 2.继承 Thread 类
        • 3.其他变形
      • Thread
        • 常见构造方法
          • 1. Thread()
          • 2. Thread(Runnable target)
          • 3. Thread(String name)
          • 4. Thread(Runnable target, String name)
        • 常见属性
        • 常用方法
          • 休眠
          • 优先级
          • 让步
          • 线程等待

前言

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

博客主页:KC老衲爱尼姑的博客主页

博主的github,平常所写代码皆在于此

共勉:talk is cheap, show me the code

作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


什么是进程?

​ 我们平时安装的程序都放在硬盘上,当我们在windows上双击可执行程序(exe)时才会将其运行起来。本质上就是将这个程序加载到内存中,然后CPU才会对该程序的数据和代码进行读取并逐行的执行,一旦将程加载到内存后,此时程序从静态的趟在硬盘上到动态的运行在内存中,我们就将在内存中的程序,称之为进程。

什么是线程?

在一个程序里的一个执行路线就叫做线程。更准确的定义是:线程是进程内部的执行序列。这就好比整个银行就是用一个进程,里面每个柜台的工作人员就是一个线程。

为什么需要线程?

  1. 创建和销毁线程比创建和销毁进程快

  2. 调度线程比调度进程快

  3. 为了让一个程序运行得更快,可以将程序划分成多个任务,然后去充分利用多核CPU

    基本的线程机制

并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的CPU一样。其底层机制是切分CPU时间,但通常你不需要考虑它。

创建线程

1.实现 Runnable 接口

线程可以驱动任务,因此需要一个描述任务的方式,这个由Runnable接口提供,想定义任务,只需要实现Runnable接口并重写run()方法。

public class LiftOff implements Runnable{  protected int countDown  = 10;  private static  int taskCount=0;  private final int id = taskCount++;  public LiftOff(int countDown) {  this.countDown = countDown;  }  public String status() {  return "#"+id+"("  +(countDown>0?countDown:"Liftoff!")+")";  }  public LiftOff() {  }  @Override  public void run() {  while (countDown-- >0){  System.out.println(status());  //建议CPU从一个线程切换到另一线程Thread.yield();  }  }  public static void main(String[] args) {  LiftOff l = new LiftOff();  l.run();//启动线程 }  
}

2.继承 Thread 类

public class MyThread extends Thread{  protected int countDown  = 10;  private static  int taskCount=0;  private final int id = taskCount++;  public MyThread(int countDown) {  this.countDown = countDown;  }  public String status() {  return "#"+id+"("  +(countDown>0?countDown:"MyThreadOff!")+")";  }  public MyThread() {  }  @Override  public void run() {  while (countDown-- >0){  System.out.println(status());  Thread.yield();  }  }  public static void main(String[] args) {  MyThread l = new MyThread();  l.start();  // 线程开始运行}  
}

3.其他变形

  • 匿名内部类创建 Thread 子类对象
Thread t = new Thread(){  @Override  public void run() {  System.out.println("Hello World!");  }  
};  
t.start();
  • 匿名内部类创建 Runnable 子类对象
Thread t = new Thread(new Runnable() {  @Override  public void run() {  System.out.println("Hello World");  }  
});  
t.start();
  • lambda 表达式创建 Runnable 子类对象
Thread t = new Thread(() -> System.out.println("Hello World"));  
t.start();  
Thread t2  = new Thread(() -> System.out.println("Hello World"))  
t2.start();

多线程的优势-增加运行速度
我们使用多线程和单线程分别来完成a和b两个变量都自增20亿次,并统计所需时间。

public class ThreadAdvantage {  private static final long count = 10_0000_0000;  public static void main(String[] args) throws InterruptedException {  //并发  concurrency();  //串行  serial();  }  private static void concurrency() throws InterruptedException {  long start = System.currentTimeMillis();  Thread t = new Thread(() -> {  int a = 0;  for (int i = 0; i < count; i++) {  a--;  }  });  Thread t2 = new Thread(() -> {  int b = 0;  for (int i = 0; i < count; i++) {  b--;  }  });  t.start();  t2.start();  t.join();  t2.join();  long end = System.currentTimeMillis();  System.out.println("串行:毫秒:" +(end - start) + "ms");  }  private static void serial() {  long start = System.currentTimeMillis();  int a = 0;  for (int i = 0; i < count; i++) {  a--;  }  int b = 0;  for (int i = 0; i < count; i++) {  b--;  }  long end = System.currentTimeMillis();  System.out.println("串行:毫秒:" +(end - start) + "ms");  }  }

串行和并发所需时间
在这里插入图片描述

Thread

常见构造方法

1. Thread()
public class Demo {  public static void main(String[] args) {  Thread t = new Thread();  t.start();  }  
}

该构造方法创建了一个Thread对象,并没有添加任务,所以启动后啥也没做就正常退出了。

2. Thread(Runnable target)
public class Demo {  public static void main(String[] args) {  Thread t = new Thread(new Runnable() {  @Override  public void run() {  System.out.println("Hello World");  }  });  t.start();  }  
}

控制台输出
Thread(Runnable target).png
该构造方法使用 Runnable 对象创建线程对象

3. Thread(String name)
public class Demo {  public static void main(String[] args) {  Thread t = new Thread("thread1"){  @Override  public void run() {  //得到这个线程的名字System.out.println(Thread.currentThread().getName());  }  };  t.start();  }  
}

控制台输出
ThreadNane.png
该构造方法创建线程对象,并命名。

4. Thread(Runnable target, String name)
  
public class Demo {  public static void main(String[] args) {  Thread t = new Thread(new Runnable() {  @Override  public void run() {  System.out.println(Thread.currentThread().getName());  }  },"thread1");  t.start();  }  
}

运行结果:
在这里插入图片描述

常见属性

属性方法
IDgetId()
名称getName()
转态getStatus()
优先级getPriorigy()
是否是后台线程isDaemon()
是否存活isAive()
是否中断isInterrupted()
ID 是线程的唯一标识,不同线程不会重复

示例代码

public class ThreadDemo {  public static void main(String[] args) {  Thread thread = new Thread(() -> {  for (int i = 0; i < 10; i++) {  try {  System.out.println(Thread.currentThread().getName() + ": 我还活着");  Thread.sleep(1 * 1000);  } catch (InterruptedException e) {  e.printStackTrace();  }  }  System.out.println(Thread.currentThread().getName() + ": 我即将死去");  });  System.out.println(Thread.currentThread().getName()  + ": ID: " + thread.getId());  System.out.println(Thread.currentThread().getName()  + ": 名称: " + thread.getName());  System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());  System.out.println(Thread.currentThread().getName()  + ": 优先级: " + thread.getPriority());  System.out.println(Thread.currentThread().getName()  + ": 后台线程: " + thread.isDaemon());  System.out.println(Thread.currentThread().getName()  + ": 活着: " + thread.isAlive());  System.out.println(Thread.currentThread().getName()  + ": 被中断: " + thread.isInterrupted());  thread.start();  while (thread.isAlive()) {  }        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());  }  
}

运行结果:

main: ID: 20
main: 名称: Thread-0
main: 状态: NEW
main: 优先级: 5
main: 后台线程: false
main: 活着: false
main: 被中断: false
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我还活着
Thread-0: 我即将死去
main: 状态: TERMINATED
Process finished with exit code 0

从任务中产生返回值
Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callabel是一种具有类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它,下面是一个简单示例:

import java.util.concurrent.Callable;
public class TaskWithResult implements Callable<String> {  private int id;  public TaskWithResult(int id){  this.id = id;  }  @Override  public String call() throws Exception {  return "result of TaskWithResult"+id;  }  
}

测试代码

import java.util.ArrayList;  
import java.util.List;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  public class CallableDemo {  public static void main(String[] args) {  ExecutorService executorService = Executors.newSingleThreadExecutor();  List<Future> list = new ArrayList<Future>();  for (int i = 0; i < 10; i++) {  //executorService.submit()会返回Future对象  list.add(executorService.submit(new TaskWithResult(i) ));  }  for (Future<String> future : list) {  try {  System.out.println(future.get());  System.out.println(future.isDone());  } catch (InterruptedException e) {  throw new RuntimeException(e);  } catch (ExecutionException e) {  throw new RuntimeException(e);  }finally {  executorService.shutdown();  }  }  }  
}

运行结果:

result of TaskWithResult0
true
result of TaskWithResult1
true
result of TaskWithResult2
true
result of TaskWithResult3
true
result of TaskWithResult4
true
result of TaskWithResult5
true
result of TaskWithResult6
true
result of TaskWithResult7
true
result of TaskWithResult8
true
result of TaskWithResult9
true
Process finished with exit code 0

常用方法

休眠

影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。

import java.util.concurrent.TimeUnit;  
public class SleepingTask implements Runnable {  private static long count = 10;  @Override  public void run() {  while (count-- > 0) {  try {  //JavaSE5引入了更加通俗的sleep()版本作为TimeUnit类的一部分。//Thread.sleep(1000);  TimeUnit.MILLISECONDS.sleep(100);  System.out.println("线程名字:" + Thread.currentThread().getName() + " 线程Id:" + Thread.currentThread().getId());  } catch (InterruptedException e) {  e.printStackTrace();  }  }  }  public static void main(String[] args) {  for (int i = 0; i < 5; i++) {  Thread t = new Thread(new SleepingTask());  t.start();  }  }  
}

对sleep()的调用可以抛出InterruptedException异常,并且你可以看到,它在run()中被捕获。因为异常不能跨线程传播回main(),所以你必须在本地处理所有在任务内部产生的异常。

优先级

线程的优先级是将告诉调度器这个线程重不重要,重要的话调度器会倾向于优先权最高的线程执行。但是.这个不意味 着优先级低的就得不到执行,只不过就是优先级低的被执行的频率比较低。这个所谓的优先级也不过是语言级别给操作系统的建议,操作系统听不听这还难整~~~ 。在大多数时间里,所有的线程都应该以默认的优先级执行,尝试去通过优先级去影响线程的执行是比较鸡肋的。

下面是一个演示优先级等级的示例,我们可以通过getpriority()来读取现有线程的优先级,并且在任何时刻都可以通过setPriority()来修改优先级。

public class SimpleProiorities implements Runnable {  private int countDown = 5;  //避免编译器优化private volatile double d;  private int priority;  public SimpleProiorities(int priority) {  this.priority = priority;  }  @Override  public String toString() {  return Thread.currentThread() + ":" + countDown;  }  @Override  public void run() {  Thread.currentThread().setPriority(priority);  while (true) {  for (int i = 1; i < 100000; i++) {  d = (Math.PI + Math.E) / (double) i;  if (i % 1000 == 0) {  Thread.yield();  }  }  System.out.println(this);  if (--countDown == 0) {  return;  }  }  }  public static void main(String[] args) {  for (int i = 0; i < 5; i++) {  //最小优先级是1Thread t = new Thread(new SimpleProiorities(Thread.MIN_PRIORITY));  //最大的优先级是10Thread t1 = new Thread(new SimpleProiorities(Thread.MAX_PRIORITY));  t.start();  t1.start();  }  }  
}

运行结果:
在这里插入图片描述

可以看出,优先级高的运行的频率要高一些。在Java中一共有10个优先级,即从1-10。但它与多数操作系统都不能映射得很好。比如,Windows有7个优先级且不是固定的,所以这种映射关系也是不确定的。Sun的Solaris有231个优先级。唯一可移植的方法是当调整优先级的时候,只使用MAX_PRIORITY(10)、NORM_PRIORITY(5)和MIN_PRIORITY(1)三种级别。

让步

当一个线程的主要任务基本完成时,就可以给线程调度机制一个暗示:该线程的活干的差不多了.可以让别的线程使用CPU。这个暗示通过yield()方法作出,不过这也是个鸡肋的操作,因为无法保证百分之百调度机制就会采纳。
示例代码

public class YieLdDemo {  private static int count1 = 0;  private static int count2 = 0;  public static void main(String[] args) {  Thread t1 = new Thread(() -> {  while (true) {  System.out.println("张三:"+count1++);  //Thread.yield();  }  },"t1");  Thread t2 = new Thread(() -> {  while (true) {  System.out.println("王五:"+count2++);  }  },"t2");  t1.start();  t2.start();  }  
}

t1不使用Thread.yield()方法的运行结果:
在这里插入图片描述

t1使用Thread.yield()方法的运行结果:
在这里插入图片描述

可以看出:

  1. 不使用yidId的时候,张三和王五基本五五开
  2. 使用yieId时,王五的数量远远大于张三。
    结论:
    yield 不改变线程的状态, 但是会重新排队。
    后台线程
    后台线程是程序运行时在后台提供的一种通用服务的线程,并且这种线程并不属于程序必不可少的部分。因此,当所有的非后台线程退出时,程序就会终止,并且会杀死进程中的所有非后台线程。比如我们常用的main就是一个非后台线程,其中后台线程退出时不会影响进程的退出,前台线程退出时会影响进程的退出。
    示例代码
import java.util.concurrent.TimeUnit;  public class SimpleDaemons implements  Runnable{  @Override  public void run() {  while (true) {  try {  TimeUnit.MILLISECONDS.sleep(100);  System.out.println(Thread.currentThread()+" " + this);  } catch (InterruptedException e) {  System.out.println(e.getMessage());  }  }  }  public static void main(String[] args) throws InterruptedException {  //main为非后台线程  int i=0;  for (i=0;i<100;i++) {  Thread th = new Thread(new SimpleDaemons());  th.setDaemon(true);//设置为后台线程  th.start();  }  System.out.println("All daemons started");  Thread.sleep(100);  }  
}

运行结果:

All daemons started
Thread[Thread-38,5,main] org.example.thread.SimpleDaemons@1eec3130
Thread[Thread-23,5,main] org.example.thread.SimpleDaemons@63ee6746
Thread[Thread-17,5,main] org.example.thread.SimpleDaemons@22867941
Thread[Thread-22,5,main] org.example.thread.SimpleDaemons@2de94878
Thread[Thread-62,5,main] org.example.thread.SimpleDaemons@63158364
Thread[Thread-5,5,main] org.example.thread.SimpleDaemons@7f25713
Thread[Thread-15,5,main] org.example.thread.SimpleDaemons@72139b8
Thread[Thread-16,5,main] org.example.thread.SimpleDaemons@5c7b595c
Thread[Thread-19,5,main] org.example.thread.SimpleDaemons@8242116
Thread[Thread-20,5,main] org.example.thread.SimpleDaemons@5658172e
Thread[Thread-25,5,main] org.example.thread.SimpleDaemons@31f176f0
Thread[Thread-30,5,main] org.example.thread.SimpleDaemons@234fbf7
Thread[Thread-32,5,main] org.example.thread.SimpleDaemons@365fee59
Thread[Thread-27,5,main] org.example.thread.SimpleDaemons@2f524b71
Process finished with exit code 0

线程等待

线程A可以在线程B上调join()方法,结果就是B线程挂起等待A线程执行结束才能继续执行。
示例代码

public class JoinExample {  public static void main(String[] args) {  Thread thread1 = new Thread(() -> {  for (int i = 1; i <= 5; i++) {  System.out.println("Thread 1 - " + i);  }  });  Thread thread2 = new Thread(() -> {  try {  thread1.join(); // 等待Thread 1结束  } catch (InterruptedException e) {  e.printStackTrace();  }  for (int i = 1; i <= 5; i++) {  System.out.println("Thread 2 - " + i);  }  });  thread1.start();  thread2.start();  }  
}

运行结果
在这里插入图片描述

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

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

相关文章

C语言每天一练----输出水仙花数

题目&#xff1a;请输出所有的"水仙花数" 题解&#xff1a;所谓"水仙花数"是指一个3位数,其各位数字立方和等于该数本身。 例如, 153是水仙花数, 因为153 1 * 1 * 1 5 * 5 * 5 3 * 3 * 3" #define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h&g…

Segmentation fault 利用 core.xxx文件帮助你debug

在没有get到本文介绍的技能之前的时候&#xff0c;以前遇到程序发生了 Segmentation fault 时&#xff0c;也是一筹莫展&#xff0c;看到伴随程序崩溃而生成的 core.xxxx 文件时&#xff08;有时会生成&#xff0c;有时不会生成&#xff0c;留着下面介绍&#xff09;&#xff0…

windows系统之WSL 安装 Ubuntu

WSL windows10 以上才有这个wsl功能 WSL&#xff1a; windows Subsystem for Linux 是应用于Windows系统之上的Linux子系统 作用很简单&#xff0c;可以在Windows系统中获取Linux系统环境&#xff0c;并完全直连计算机硬件&#xff0c;无需要通过虚拟机虚拟硬件 Windows10的W…

TCP网络通信编程之字符流

【案例1】 【题目描述】 【 注意事项】 (3条消息) 节点流和处理流 字符处理流BufferedReader、BufferedWriter&#xff0c;字节处理流-BufferedInputStream和BufferedOutputStream (代码均正确且可运行_Studying~的博客-CSDN博客 1。这里需要使用字符处理流&#xff0c;来将…

C++代码格式化工具clang-format详细介绍

文章目录 clang-format思考代码风格指南生成您的配置运行 clang-format禁用一段代码的格式设置clang-format的设置预览 clang-format 我曾在许多编程团队工作过&#xff0c;这些团队名义上都有“编程风格指南”。该指南经常被写下来并放置在开发人员很少查看的地方。几乎在每种…

MQTT服务器详细介绍:连接物联网的通信枢纽

随着物联网技术的不断发展&#xff0c;MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;协议作为一种轻量级、可靠、灵活的通信协议&#xff0c;被广泛应用于物联网领域。在MQTT系统中&#xff0c;MQTT服务器扮演着重要的角色&#xff0c;作为连接物联网设备和…

常见网关对比

常见网关对比 目前常见的开源网关大致上按照语言分类有如下几类&#xff1a; Nginxlua &#xff1a;OpenResty、Kong、Orange、Abtesting gateway 等 Java &#xff1a;Zuul/Zuul2、Spring Cloud Gateway、Kaazing KWG、gravitee、Dromara soul 等 Go &#xff1a;Janus、fa…

计算机里基本硬件的组成以及硬件协同

文章目录 冯诺依曼体系输入设备输出设备存储器运算器控制器协同工作的流程 冯诺依曼体系 世界上第一台通用计算机&#xff0c;ENIAC&#xff0c;于1946年诞生于美国一所大学。 ENIAC研发的前期&#xff0c;需要工作人员根据提前设计好的指令手动接线&#xff0c;以这种方式输入…

【HDFS】Block、BlockInfo、BlockInfoContiguous、BlockInfoStriped的分析记录

本文主要介绍如下内容: 关于几个Block类之间的继承、实现关系;针对文章标题中的每个类,细化到每个成员去注释分析列出、并详细分析BlockInfo抽象类提供的抽象方法、非抽象方法的功能针对几个跟块组织结构的方法再进行分析。moveBlockToHead、listInsert、listRemove等。一、…

spring5源码篇(13)——spring mvc无xml整合tomcat与父子容器的启动

spring-framework 版本&#xff1a;v5.3.19 文章目录 整合步骤实现原理ServletContainerInitializer与WebApplicationInitializer父容器的启动子容器的启动 相关面试题 整合步骤 试想这么一个场景。只用 spring mvc&#xff08;确切来说是spring-framework&#xff09;&#x…

MySQL 实现分库和分表的备份 2023.7.29

1、分库备份 [rootlocalhost mysql-backup]# cat db_bak.sh #!/bin/bash k_userroot bak_password123456 bak_path/root/mysql-backup/ bak_cmd"-u$bak_user -p$bak_password" exc_db"Database|information_schema|mysql|performance_schema|sys" dbname…

C#,数值计算——对数正态分布(logarithmic normal distribution)的计算方法与源程序

对数正态分布&#xff08;logarithmic normal distribution&#xff09;是指一个随机变量的对数服从正态分布&#xff0c;则该随机变量服从对数正态分布。对数正态分布从短期来看&#xff0c;与正态分布非常接近。但长期来看&#xff0c;对数正态分布向上分布的数值更多一些。 …

基于minio的dababend部署总结

Databend 是一款开源、弹性、低成本&#xff0c;基于对象存储也可以做实时分析的新式数仓。期待您的关注&#xff0c;一起探索云原生数仓解决方案&#xff0c;打造新一代开源 Data Cloud。 Minio搭建 minio 192.168.10.159 cd /data mkdir minio cd minio wget https://dl…

word2vec原理总结

参考文章&#xff1a;https://www.cnblogs.com/pinard/p/7160330.html word2vec是google在2013年推出的一个NLP工具&#xff0c;它的特点是将所有的词向量化&#xff0c;这样词与词之间就可以定量的去度量他们之间的关系&#xff0c;挖掘词之间的联系。 1 词向量编码 1.1 one…

Unity源码分享-黄金矿工游戏完整版

Unity源码分享-黄金矿工游戏完整版 项目地址&#xff1a;https://download.csdn.net/download/Highning0007/88118933

Python-Python基础综合案例--数据可视化 - 地图可视化

版本说明 当前版本号[20230729]。 版本修改说明20230729初版 目录 文章目录 版本说明目录知识总览图Python基础综合案例--数据可视化 - 地图可视化基础地图使用案例效果视觉映射器 疫情地图-国内疫情地图案例效果实操设置全局配置选项 疫情地图-省级疫情地图案例效果实操 知…

如何做好IT类的技术面试

目录 一、IT行业的招聘渠道 二、如何做好技术面试官 三、谈谈IT行业如何做好招聘工作 四、面试IT公司的小技巧 五、面试有哪些常见的问题 六、关于面试的一些建议 面试可能是我们每个人都必须会遇到的事情&#xff0c;而技术面试更具有专业性&#xff0c;以下会从几个方面…

IDEA将本地项目上传到码云

一、创建本地仓库并关联 用IDEA打开项目&#xff0c;在菜单栏点击vcs->create git repository创建本地仓库&#xff0c; 选择当前项目所在的文件夹当作仓库目录。 二、将项目提交本地仓库 项目名右键就会出现“GIT”这个选项->Add->Commit Directory, 先将项目add…

sql server表值函数

一、创建测试表 Employees 二、创建表值函数 -- DROP FUNCTION TableIntSplit;CREATE FUNCTION TableIntSplit(Text NVARCHAR(4000),Sign NVARCHAR(4000)) RETURNS tempTable TABLE(Id INT ) AS BEGIN DECLARE StartIndex INT DECLARE FindIndex INT DECLARE Content VARCHAR(…

玩转Tomcat:从安装到部署

文章目录 一、什么是 Tomcat二、Tomcat 的安装与使用2.1 下载安装2.2 目录结构2.3 启动 Tomcat 三、部署程序到 Tomcat3.1 Windows环境3.2 Linux环境 一、什么是 Tomcat 一看到 Tomcat&#xff0c;我们一般会想到什么&#xff1f;没错&#xff0c;就是他&#xff0c;童年的回忆…