1.1.1 什么是并发工具包
Java并发工具包是指java.util.concurrent(简称JUC),在Java 5 版本中添加。JUC中包含了大量在并发应用开发中非常实用的工具类。
行业中说的JUC一般包含java.util.concurrent包、java.util.concurrent.atomic包及java.util.concurrent.locks包中的内容。
1.1.2 并发工具包的构成
JUC中主要包括5个模块的内容,分别是atomic(原子操作)、locks(锁和条件变量)、collections(阻塞队列和线程安全集合)、executor(线程池)和tools(线程同步工具)。
?
1.2.1 Java同步工具类概述
Java同步工具类提供了一些用于多线程编程的高级工具,用于解决并发场景中的线程同步和协调问题。下面介绍三个常用的Java并发工具类:CountDownLatch、CyclicBarrier和Semaphore,它们都可以用来协调多个线程之间的执行。
1、CountDownLatch是一个计数器,它允许一个或多个线程等待其他线程完成操作。
假设有一个场景,有三名学生在考试,而老师负责等待所有学生考试完成后计算学生的平均分。这时,我们可以使用CountDownLatch来实现。
2、CyclicBarrier是一个同步屏障,它允许多个线程相互等待,直到到达某个公共屏障点,才能继续执行。
假设有一个场景,三名学生约好在某个地点碰面,然后一起去看电影。我们希望所有人都到达后再一起出发,这时可以使用CyclicBarrier来实现。
3、Semaphore是一个计数信号量,它允许多个线程同时访问共享资源,并通过计数器来控制访问数量。它通常用来实现一个线程需要等待获取一个许可证才能访问共享资源,或者需要释放一个许可证才能完成操作的操作。
假设有一个场景,6名学生约好去参观名人故居,但是该名人故居实行限流,同时仅能容纳2人参观。这时可以使用Semaphore来实现。
1.2.2 CountDownLatch
CountDownLatch是Java并发工具类之一,用于实现线程间的等待和协调。它基于一个计数器,可以让一个或多个线程等待其他线程完成操作后再继续执行。
CountDownLatch的核心概念是一个初始计数值,即在创建CountDownLatch对象时指定的计数值。每个线程在完成自己的任务后,都会调用CountDownLatch的countDown()方法将计数器减一。而等待的线程可以调用await()方法来阻塞等待,直到计数器变为零。
?1.2.3 【案例】CountDownLatch示例
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) {
int numStudents = 3;
CountDownLatch latch = new CountDownLatch(numStudents);
for (int i = 0; i < numStudents; i++) {
Student student = new Student(latch, "Student " + (i + 1));
new Thread(student).start();
}
try {
// 等待所有学生考试完成
latch.await();
System.out.println("所有学生结束考试");
// 计算学生平均分
int totalScore = 0;
for (int i = 0; i < numStudents; i++) {
totalScore += Student.scores[i];
}
double averageScore = (double) totalScore / numStudents;
System.out.println("所有学生的平均分为: " + averageScore);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Student implements Runnable {
private final CountDownLatch latch;
private final String name;
public static int[] scores = new int[3];
public Student(CountDownLatch latch, String name) {
this.latch = latch;
this.name = name;
}
@Override
public void run() {
// 模拟学生考试
int score = (int) (Math.random() * 100);
System.out.println(name + "结束考试,分数为:" + score);
// 将学生的成绩保存到数组中
int index = Integer.parseInt(name.split(" ")[1]) - 1;
scores[index] = score;
// 考试完成,计数器减1
latch.countDown();
}
}
}
1.2.4 CyclicBarrier
CyclicBarrier(循环屏障)是Java并发工具类之一,用于实现线程间的同步,使一组线程能够等待彼此达到一个共同的屏障点后再继续执行。
CyclicBarrier的核心概念是一个屏障点和一个计数器。创建CyclicBarrier对象时,需要指定计数器的初始值和在达到屏障点之前需要执行的任务(可选)。每个线程在到达屏障点前调用await()方法进行等待,每当有线程调用了await()方法时,计数器递减。当计数器变为零时,所有线程同时被唤醒,可以继续执行后续操作,计数器也会被重置为初始值。
可以比喻为军训队列报数:报数总人数满足教官认为的总数时,教官才会安排后面的训练。
?1.2.5 【案例】CyclicBarrier示例
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3; // 线程数量
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
// 所有线程到达屏障点后执行的任务
System.out.println("所有同学已到达集合地点");
});
// 创建并启动计算线程
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(new Student(barrier, i + 1));
thread.start();
}
}
static class Student implements Runnable {
private final CyclicBarrier barrier;
private final int threadId;
public Student(CyclicBarrier barrier, int threadId) {
this.barrier = barrier;
this.threadId = threadId;
}
@Override
public void run() {
try {
// 模拟计算耗时
Thread.sleep(threadId * 1000);
System.out.println("Thread " + threadId + " 已到达集合地点");
// 线程到达屏障点,等待其他线程
barrier.await();
// 执行后续操作
System.out.println("Thread " + threadId + " 出发去电影院");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
1.2.6 Semaphore
Semaphore(信号量)是Java并发工具类之一,用于控制对资源的访问权限。
Semaphore的核心概念是许可证和计数器:
1.2.7 【案例】Semaphore示例
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int numStudents = 6;
int numPermits = 2; // 名人故居只能容纳2人参观
Semaphore semaphore = new Semaphore(numPermits);
for (int i = 0; i < numStudents; i++) {
Student student = new Student(semaphore, "Student " + (i + 1));
new Thread(student).start();
}
}
static class Student implements Runnable {
private final Semaphore semaphore;
private final String name;
public Student(Semaphore semaphore, String name) {
this.semaphore = semaphore;
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " 已到达名人故居,申请参观");
// 申请参观许可,如果没有可用的许可,则需要等待
semaphore.acquire();
System.out.println(name + " 开始参观名人故居");
// 参观时间
Thread.sleep((long) (Math.random() * 5000));
// 离开名人故居,释放许可
System.out.println(name + " 结束参观,离开名人故居");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.2.8 【案例】Semaphore实现多线程打印ABC示例
请基于Semaphore设计实现多线程交替打印ABC案例,要求将三个线程的名字分别设置为A、B和C,线程打印时,输出的是当前线程的名称。
import java.util.concurrent.Semaphore;
public class SemaphoreExample2 {
public static void main(String[] args) {
Semaphore semaphoreA = new Semaphore(1); // 初始有1个许可证
Semaphore semaphoreB = new Semaphore(0); // 初始没有许可证
Semaphore semaphoreC = new Semaphore(0); // 初始没有许可证
Thread threadA = new Thread(new PrintTask("A", semaphoreA, semaphoreB));
Thread threadB = new Thread(new PrintTask("B", semaphoreB, semaphoreC));
Thread threadC = new Thread(new PrintTask("C", semaphoreC, semaphoreA));
threadA.start();
threadB.start();
threadC.start();
}
static class PrintTask implements Runnable {
private final String content;
private final Semaphore currentSemaphore;
private final Semaphore nextSemaphore;
public PrintTask(String content, Semaphore currentSemaphore, Semaphore nextSemaphore) {
this.content = content;
this.currentSemaphore = currentSemaphore;
this.nextSemaphore = nextSemaphore;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
// 获取当前信号量的许可证
currentSemaphore.acquire();
System.out.print(content);
// 释放下一个信号量的许可证
nextSemaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2.1.1 线程池概述
在计算机编程中,线程池是一种软件设计模式,用于在计算机程序中实现并发执行。线程池维护多个线程,等待监督程序分配任务并并发执行。通过维护线程池,该模型提高了性能,并避免了由于频繁创建和销毁线程而导致的执行延迟。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步(多线程各自执行各自的)或同步执行任务的程序都可以使用线程池。
在开发过程中,合理地使用线程池的优势如下:
2.1.2 ExecutorService
ExecutorService是一个Java API,可以简化异步模式下运行的任务(Task)。一般来说,ExecutorService 会自动提供一个线程池和一个用于为其分配任务的API。
ExecutorService名称中的Executor表示一个任务的执行器。开发者可以使用Runnable接口的实现类来封装线程的工作单元(线程启动后执行的具体逻辑),将该工作单元看作一个要被执行的任务,委派给Executor来执行。
ExecutorService接口继承Executor接口,在Executor的基础上扩展了对Executor进行控制的方法:如提供的submit() 方法用于提交Runnable任务,invokeAll()方法用于批量提交任务,shutdown()方法用于停止启动新的任务等。
在Java中,ExecutorService接口的实现类基于线程池来实现,这也是ExecutorService在很多场景下被简单地看成一个线程池工具的原因。常用的ExecutorService接口的实现类包括ThreadPoolExecutor和ScheduledThreadPoolExecutor。
创建ExecutorService实例的方法如下:
ExecutorService接口常用的方法如下:
2.1.3 【案例】ExecutorService示例
编写代码,练习ExecutorService的使用。代码示意如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
// 创建一个包含了4个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
MyRun1 run1 = new MyRun1(); // 创建任务对象
for(int i =1;i<=4;i++) {
executorService.execute(run1); // 提交4次任务
}
// 关闭线程池,已提交的任务继续执行,不能再提交新的任务
executorService.shutdown();
// executorService.execute(run1); // 抛出RejectedExecutionException
}
}
class MyRun1 implements Runnable {
int num = 0;
@Override
public void run() {
while (true){
synchronized (this){
if (num >1000){
break;
}
String name = Thread.currentThread().getName();
System.out.println(name + ": " + num);
num++;
}
}
}
}
2.1.4 【案例】ScheduledExecutorService示例
编写代码,练习ScheduledExecutorService的使用。代码示意如下:
import java.time.LocalTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceDemo2 {
public static void main(String[] args) {
// 创建一个包含了4个线程的线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
Runnable run1 = () -> { // 任务1
String name = Thread.currentThread().getName();
System.out.println(name+": "+ LocalTime.now());
};
Runnable run2 = () -> { // 任务2
String name = Thread.currentThread().getName();
System.out.println("====>"+name+": "+ LocalTime.now());
};
// 指定延迟1秒后,每个2秒执行1次任务1
service.scheduleAtFixedRate(run1, 1, 2, TimeUnit.SECONDS);
// 指定延迟2秒后,每个1秒执行1次任务2
service.scheduleAtFixedRate(run2, 2, 1, TimeUnit.SECONDS);
// 任务1和任务2共用线程池中的4个线程
}
}
2.2.1 获取并发任务的结果概述
前面的课程中讲述了创建线程的2种方式:一种是直接继承Thread,另外一种是实现Runnable接口。这2种方式都有一个缺陷:在任务执行完成后无法直接获取任务的执行结果。想要获取任务执行结果,必须通过共享变量或者使用线程通信的方式来达到效果,这种实现方式比较复杂。
如上图所示:如果用多线程方式,随机生成100个10以内的数字,并求和,如何确定执行了100次,是个难题。解决办法是:用变量 count 来记载执行的次数,并不断查询变量 count 的值。这种实现方式比较麻烦,而且在解决复杂业务时,代码复杂度也会直线上升。
2.2.2 Callable接口
为简化获取并发任务结果的编码, Java 5版本中增加了Callable接口和Future接口,通过它们可以在并发任务执行完毕之后便捷的得到任务执行结果。
使用Callable接口被称为Java中创建线程的第三种方式。
?
Callable接口与Runnable接口相似,代表线程的工作单元,该接口中只有一个call()方法,含义与run()方法相似。
与run()方法不同的是,call()方法可以返回指定的泛型类对象,并且可以抛出Exception类及其子类异常。call()方法的设计为返回并发任务的结果提供了可能。
2.2.3 Future接口
Future接口表示异步计算的结果,它提供了检查计算是否完成、等待其完成以及检索计算结果的方法:
2.2.4 【案例】Callable和Future示例
编写代码,练习Callable和Future的使用。代码示意如下:
import java.util.Random;
import java.util.concurrent.*;
public class CallableDemo1 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
MyCall cd = new MyCall();
Future<Integer> future1 = service.submit(cd); // 提交任务
System.out.println("isDone(): "+future1.isDone());
try {
int result = future1.get();
System.out.println("get(): "+result);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("isDone(): "+future1.isDone());
// 关闭ExecutorService
service.shutdown();
}
}
class MyCall implements Callable<Integer> {
Random random = new Random();
int count =1; // 控制计算次数
int result = 0; // 保存计算结果
public Integer call(){
while(true){
synchronized (this){
if (count>100){ // 计算100次
break;
}
int num = random.nextInt(10);
result += num;
String name = Thread.currentThread().getName();
// 输出多线程执行的过程
System.out.println(name+", num="+num+",result="+result);
count++;
}
Thread.yield();
}
return result;
}
}
2.2.5 FutureTask
FutureTask 为 Future接口提供了基础实现,如get()方法的具体逻辑、cancel()方法的具体逻辑以及其他。
?
FutureTask常用来封装Callable和Runnable,也可以作为一个任务提交到线程池中执行。
2.2.6 【案例】FutureTask示例
编写代码,练习FutureTask的使用。代码示意如下:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class FutureTaskDemo1 {
public static void main(String[] args) {
int numStudents = 3;
FutureTask<Integer>[] ftArray = new FutureTask[numStudents];
for (int i = 0; i < numStudents; i++) {
Student student = new Student("Student " + (i + 1));
FutureTask<Integer> ft = new FutureTask<>(student);
ftArray[i] = ft;
new Thread(ft).start();
}
// 计算学生平均分
int totalScore = 0;
for (int i = 0; i < numStudents; i++) {
try {
totalScore += ftArray[i].get();
} catch (Exception e) {
e.printStackTrace();
}
}
double averageScore = (double) totalScore / numStudents;
System.out.println("所有学生的平均分为: " + averageScore);
}
static class Student implements Callable<Integer> {
private final String name;
public Student(String name) {
this.name = name;
}
@Override
public Integer call() throws Exception {
// 模拟学生考试
int score = (int) (Math.random() * 100);
System.out.println(name + "结束考试,分数为:" + score);
return score;
}
}
}
3.1.1 并发中的锁
在计算机科学中,锁(Lock)是一种同步原语:一种在有许多执行线程时强制限制对资源的访问的机制。锁旨在强制执行互斥并发控制策略,但是在控制的细节上会有很多差异,这种差异称为锁的特性。
根据锁的特性,可以将锁分为很多不同的类型,例如:公平锁/非公平锁、悲观锁/乐观锁、可重入锁/不可重入锁、共享锁/排他锁等,如下图所示。
?
这里需要注意,由于一个锁可能同时具备多个特性,可能被同时划入不同的类型。
3.1.2 从synchronized看锁的分类
可以从不同的维度来看synchronized锁的特性:
3.2.1 Lock接口
Lock接口是Java 5版本在并发工具包中新增的内容,主要用来实现锁功能,其实现类提供了各种类型的锁,应用场景非常丰富。
Lock接口提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字不具备的同步特性。
Lock的使用方式非常简单:
Lock lock = new XXXLock();
lock.lock(); // 获取锁
lock.unlock(); // 释放锁
Lock接口提供的并且synchronized关键字所不具备的主要特性如下:
Lock接口的常用方法如下:
3.2.2 【案例】Lock示例
编写代码,测试Lock锁的使用。代码示意如下:
public class LockDemo1 {
public static void main(String[] args) {
MyRun2 run2 = new MyRun2();
ExecutorService service = Executors.newFixedThreadPool(4);
for(int i=1; i<=4; i++){
service.execute(run2);
}
service.shutdown();
}
}
class MyRun2 implements Runnable {
int i = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
String name = Thread.currentThread().getName();
try {
if (i > 50) {
break;
}
System.out.println(name + " " + i);
i++;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
3.2.3 ReentrantLock
ReentrantLock,顾名思义,就是支持冲进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,如下图所示:
?
同时,该锁还支持获取锁时的公平和非公平性选择:
?
3.2.4 【案例】公平锁和非公平锁示例
编写代码,测试公平锁和非公平锁的使用。代码示意如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo1 {
public static void main(String[] args) throws InterruptedException {
boolean fairFlag = true; // 是否使用公平锁
// 基于fairFlag创建公平锁或非公平锁
Lock lock = new ReentrantLock(fairFlag);
MyRun3 run3 =new MyRun3(lock, fairFlag? "fair": "nonFair");
ExecutorService service = Executors.newFixedThreadPool(8);
for(int i=1;i<=16;i++){
service.execute(run3);
TimeUnit.MILLISECONDS.sleep(10);
}
service.shutdown();
}
}
class MyRun3 implements Runnable {
Lock lock;
String label;
public MyRun3(Lock lock, String label){
this.lock = lock;
this.label = label;
}
@Override
public void run() {
lock.lock();
String name = Thread.currentThread().getName();
System.out.println(label + ": "
+ "name: " + name + "获取到锁");
try {
TimeUnit.MILLISECONDS.sleep(100); // 休眠1秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.unlock();
}
}
3.2.5 一写多读场景
一写多读,顾名思义,是指对数据的并发读取频率较高,而并发写入频率较低的场景,是现实中非常普遍的一种并发场景。
例如,某电影网站的电影实时票房量:
在这个案例中,实时票房量是共享资源,被多个线程并发的访问,其中包含读线程,也包含写线程。为了防止出现线程安全问题,需要对共享资源进行加锁。
3.2.6 读写锁
但是目前讲到的锁都是排他锁:即同一时间仅能有一个线程获取锁。在一写多读场景中,读线程之前也会阻塞,同一时间仅能有一个线程读取票房数据,这大大降低了读的效率:
在一些多读场景中,读线程和写线程应该互斥,写线程和写线程之间应该互斥,但是读线程和读线程之间可以不互斥。行业中通常使用“读写锁“的方案来实现上述需求。
读写锁维护了一对锁:一个读锁和一个写锁。通过分离读锁和写锁,使得并发性相比一般的排他锁有很大的提升。
3.2.7 ReadWriteLock
Java中使用ReadWriteLock接口作为读写锁的父接口,该接口定义了两个方法:
ReentrantReadWriteLock作为ReadWriteLock接口的实现类,除了实现了上述的2个方法外,还提供了一些便于外界监控器内部工作状态的方法:
3.2.8 【案例】读写锁示例
编写代码,测试读写锁的使用。代码示意如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
StudentInfoManagementSystem system = new StudentInfoManagementSystem();
Runnable readTask = () -> {
for(int i = 0; i <5 ;i++){
system.getStudents();
}
};
Runnable writeTask = () -> {
system.addStudent("Tom");
};
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 创建多个读任务读取学生信息
for (int i = 0; i < 10; i++) {
if (i % 4 ==0){
executor.execute(writeTask);
} else {
executor.execute(readTask);
}
}
// 关闭线程池
executor.shutdown();
}
}
class StudentInfoManagementSystem {
private volatile List<String> studentList = new ArrayList<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void addStudent(String studentName) {
lock.writeLock().lock();
try {
// 写入学生信息
studentList.add(studentName);
System.out.println("Added student: " + studentName);
} finally {
lock.writeLock().unlock();
}
}
public void getStudents() {
lock.readLock().lock();
try {
System.out.println("Read students: " + studentList);
} finally {
lock.readLock().unlock();
}
}
}
4.1.1 网络概述
计算机网络是指将地理位置不同的计算机通过通信线路连接起来,实现资源共享和信息传递。网络编程就是通过程序实现两台(或多台)主机之间的数据通信。
?
4.1.2 通信协议
在计算机网络中实现通信必须有一定的规则和约定,将这些规则和约定称为通信协议。通信协议中对计算机的传输速率、传输代码、传输控制步骤和出错标准等做了统一规定。为了让网络中的计算机进行通信,通信双方必须遵守通信协议,才能完成信息交换。不同的应用需要遵守对应的通信协议。
4.1.3 TCP/IP 协议和 UDP 协议
TCP/IP协议是一个协议族,包括IP协议、TCP协议和IMCP协议。
IP协议是TCP/IP协议的核心。IP协议为上层提供统一的IP数据报,提供无连接的、不可靠的、尽力而为的数据报投递服务。
TCP是传输层的一种可靠的、面向连接的传输协议。每个TCP连接只能是点对点的,只能连接两个端点,可以提供全双工通信。由于通信双方必须事先建立连接,其特点是效率低,但数据传输比较安全。
UDP是一种无连接的传输层协议,提供的是非面向连接的、不可靠的数据流传输。“无连接”就是在通信前不必与对方先建立连接,不管对方状态就直接发送。其特点是传输效率高,但数据传输不安全,容易丢包。
?
4.1.4 IP地址和域名
IP地址(IP Address)为互联网上的每个网络和每台主机分配一个逻辑地址,用于标识其网络身份。每个IP地址在整个网络中都是唯一的。IP地址分为IPv4和IPv6两个版本。
IPV4版本的IP地址使用32位数字构成,由4个8位的二进制数组成,每8位之间用圆点隔开,通常用点分十进位表示,如125.123.23.46。
IPv6版本的IP地址使用128位数字表示一个地址,采用十六进制数表示。
有时还会用到一个特殊的IP地址——127.0.0.1。将127.0.0.1称为回送地址,主要用于网络软件测试及本地主机进程间通信,使用回送地址发送数据时,不进行任何网络传输,只在本地主机进程间通信。
域名用于识别 Internet 资源,例如计算机、网络和服务,其基于文本的标签比 Internet 协议中使用的IP地址更容易记住。例如,百度的IP地址为220.181.38.150,百度的域名是www.baidu.com,后者更容易被使用者记住。
4.1.5 端口号
一个IP地址标识一台计算机,每台计算机又有很多网络通信程序在运行,提供网络服务或进行通信,这就需要不同的端口进行通信。如果把IP地址比作电话号码,那么端口就是分机号码,进行网络通信时不仅要指定IP 地址,还要指定端口号。
TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535,小于1024的端口号保留给预定义的服务,如HTTP是80,FTP是21, Telnet是23,Email是25,如下图所示:
?除非要和那些服务进行通信,否则不应该使用小于1024的端口。