phaser java_死磕 java同步系列之Phaser源码解析

问题

(1)Phaser是什么?

(2)Phaser具有哪些特性?

(3)Phaser相对于CyclicBarrier和CountDownLatch的优势?

简介

Phaser,翻译为阶段,它适用于这样一种场景,一个大任务可以分为多个阶段完成,且每个阶段的任务可以多个线程并发执行,但是必须上一个阶段的任务都完成了才可以执行下一个阶段的任务。

这种场景虽然使用CyclicBarrier或者CountryDownLatch也可以实现,但是要复杂的多。首先,具体需要多少个阶段是可能会变的,其次,每个阶段的任务数也可能会变的。相比于CyclicBarrier和CountDownLatch,Phaser更加灵活更加方便。

使用方法

下面我们看一个最简单的使用案例:

public class PhaserTest {

public static final int PARTIES = 3;

public static final int PHASES = 4;

public static void main(String[] args) {

Phaser phaser = new Phaser(PARTIES) {

@Override

protected boolean onAdvance(int phase, int registeredParties) {

// 【本篇文章由公众号“彤哥读源码”原创,请支持原创,谢谢!】

System.out.println("=======phase: " + phase + " finished=============");

return super.onAdvance(phase, registeredParties);

}

};

for (int i = 0; i < PARTIES; i++) {

new Thread(()->{

for (int j = 0; j < PHASES; j++) {

System.out.println(String.format("%s: phase: %d", Thread.currentThread().getName(), j));

phaser.arriveAndAwaitAdvance();

}

}, "Thread " + i).start();

}

}

}

这里我们定义一个需要4个阶段完成的大任务,每个阶段需要3个小任务,针对这些小任务,我们分别起3个线程来执行这些小任务,查看输出结果为:

Thread 0: phase: 0

Thread 2: phase: 0

Thread 1: phase: 0

=======phase: 0 finished=============

Thread 2: phase: 1

Thread 0: phase: 1

Thread 1: phase: 1

=======phase: 1 finished=============

Thread 1: phase: 2

Thread 0: phase: 2

Thread 2: phase: 2

=======phase: 2 finished=============

Thread 0: phase: 3

Thread 2: phase: 3

Thread 1: phase: 3

=======phase: 3 finished=============

可以看到,每个阶段都是三个线程都完成了才进入下一个阶段。这是怎么实现的呢,让我们一起来学习吧。

原理猜测

根据我们前面学习AQS的原理,大概猜测一下Phaser的实现原理。

首先,需要存储当前阶段phase、当前阶段的任务数(参与者)parties、未完成参与者的数量,这三个变量我们可以放在一个变量state中存储。

其次,需要一个队列存储先完成的参与者,当最后一个参与者完成任务时,需要唤醒队列中的参与者。

嗯,差不多就是这样子。

结合上面的案例带入:

初始时当前阶段为0,参与者数为3个,未完成参与者数为3;

第一个线程执行到phaser.arriveAndAwaitAdvance();时进入队列;

第二个线程执行到phaser.arriveAndAwaitAdvance();时进入队列;

第三个线程执行到phaser.arriveAndAwaitAdvance();时先执行这个阶段的总结onAdvance(),再唤醒前面两个线程继续执行下一个阶段的任务。

嗯,整体能说得通,至于是不是这样呢,让我们一起来看源码吧。

源码分析

主要内部类

static final class QNode implements ForkJoinPool.ManagedBlocker {

final Phaser phaser;

final int phase;

final boolean interruptible;

final boolean timed;

boolean wasInterrupted;

long nanos;

final long deadline;

volatile Thread thread; // nulled to cancel wait

QNode next;

QNode(Phaser phaser, int phase, boolean interruptible,

boolean timed, long nanos) {

this.phaser = phaser;

this.phase = phase;

this.interruptible = interruptible;

this.nanos = nanos;

this.timed = timed;

this.deadline = timed ? System.nanoTime() + nanos : 0L;

thread = Thread.currentThread();

}

}

先完成的参与者放入队列中的节点,这里我们只需要关注thread和next两个属性即可,很明显这是一个单链表,存储着入队的线程。

主要属性

// 状态变量,用于存储当前阶段phase、参与者数parties、未完成的参与者数unarrived_count

private volatile long state;

// 最多可以有多少个参与者,即每个阶段最多有多少个任务

private static final int MAX_PARTIES = 0xffff;

// 最多可以有多少阶段

private static final int MAX_PHASE = Integer.MAX_VALUE;

// 参与者数量的偏移量

private static final int PARTIES_SHIFT = 16;

// 当前阶段的偏移量

private static final int PHASE_SHIFT = 32;

// 未完成的参与者数的掩码,低16位

private static final int UNARRIVED_MASK = 0xffff; // to mask ints

// 参与者数,中间16位

private static final long PARTIES_MASK = 0xffff0000L; // to mask longs

// counts的掩码,counts等于参与者数和未完成的参与者数的'|'操作

private static final long COUNTS_MASK = 0xffffffffL;

private static final long TERMINATION_BIT = 1L << 63;

// 一次一个参与者完成

private static final int ONE_ARRIVAL = 1;

// 增加减少参与者时使用

private static final int ONE_PARTY = 1 << PARTIES_SHIFT;

// 减少参与者时使用

private static final int ONE_DEREGISTER = ONE_ARRIVAL|ONE_PARTY;

// 没有参与者时使用

private static final int EMPTY = 1;

// 用于求未完成参与者数量

private static int unarrivedOf(long s) {

int counts = (int)s;

return (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);

}

// 用于求参与者数量(中间16位),注意int的位置

private static int partiesOf(long s) {

return (int)s >>> PARTIES_SHIFT;

}

// 用于求阶段数(高32位),注意int的位置

private static int phaseOf(long s) {

return (int)(s >>> PHASE_SHIFT);

}

// 已完成参与者的数量

private static int arrivedOf(long s) {

int counts = (int)s; // 低32位

return (counts == EMPTY) ? 0 :

(counts >>> PARTIES_SHIFT) - (counts & UNARRIVED_MASK);

}

// 用于存储已完成参与者所在的线程,根据当前阶段的奇偶性选择不同的队列

private final AtomicReference evenQ;

private final AtomicReference oddQ;

主要属性为state和evenQ及oddQ:

(1)state,状态变量,高32位存储当前阶段phase,中间16位存储参与者的数量,低16位存储未完成参与者的数量【本篇文章由公众号“彤哥读源码”原创,请支持原创,谢谢!】;

13a046deb200d2420501dcc5f2c3ab71.png

(2)evenQ和oddQ,已完成的参与者存储的队列,当最后一个参与者完成任务后唤醒队列中的参与者继续执行下一个阶段的任务,或者结束任务。

构造方法

public Phaser() {

this(null, 0);

}

public Phaser(int parties) {

this(null, parties);

}

public Phaser(Phaser parent) {

this(parent, 0);

}

public Phaser(Phaser parent, int parties) {

if (parties >>> PARTIES_SHIFT != 0)

throw new IllegalArgumentException("Illegal number of parties");

int phase = 0;

this.parent = parent;

if (parent != null) {

final Phaser root = parent.root;

this.root = root;

this.evenQ = root.evenQ;

this.oddQ = root.oddQ;

if (parties != 0)

phase = parent.doRegister(1);

}

else {

this.root = this;

this.evenQ = new AtomicReference();

this.oddQ = new AtomicReference();

}

// 状态变量state的存储分为三段

this.state = (parties == 0) ? (long)EMPTY :

((long)phase << PHASE_SHIFT) |

((long)parties << PARTIES_SHIFT) |

((long)parties);

}

构造函数中还有一个parent和root,这是用来构造多层级阶段的,不在本文的讨论范围之内,忽略之。

重点还是看state的赋值方式,高32位存储当前阶段phase,中间16位存储参与者的数量,低16位存储未完成参与者的数量。

下面我们一起来看看几个主要方法的源码:

register()方法

注册一个参与者,如果调用该方法时,onAdvance()方法正在执行,则该方法等待其执行完毕。

public int register() {

return doRegister(1);

}

private int doRegister(int registrations) {

// state应该加的值,注意这里是相当于同时增加parties和unarrived

long adjust = ((long)registrations << PARTIES_SHIFT) | registrations;

final Phaser parent = this.parent;

int phase;

for (;;) {

// state的值

long s = (parent == null) ? state : reconcileState();

// state的低32位,也就是parties和unarrived的值

int counts = (int)s;

// parties的值

int parties = counts >>> PARTIES_SHIFT;

// unarrived的值

int unarrived = counts & UNARRIVED_MASK;

// 检查是否溢出

if (registrations > MAX_PARTIES - parties)

throw new IllegalStateException(badRegister(s));

// 当前阶段phase

phase = (int)(s >>> PHASE_SHIFT);

if (phase < 0)

break;

// 不是第一个参与者

if (counts != EMPTY) { // not 1st registration

if (parent == null || reconcileState() == s) {

// unarrived等于0说明当前阶段正在执行onAdvance()方法,等待其执行完毕

if (unarrived == 0) // wait out advance

root.internalAwaitAdvance(phase, null);

// 否则就修改state的值,增加adjust,如果成功就跳出循环

else if (UNSAFE.compareAndSwapLong(this, stateOffset,

s, s + adjust))

break;

}

}

// 是第一个参与者

else if (parent == null) { // 1st root registration

// 计算state的值

long next = ((long)phase << PHASE_SHIFT) | adjust;

// 修改state的值,如果成功就跳出循环

if (UNSAFE.compareAndSwapLong(this, stateOffset, s, next))

break;

}

else {

// 多层级阶段的处理方式

synchronized (this) { // 1st sub registration

if (state == s) { // recheck under lock

phase = parent.doRegister(1);

if (phase < 0)

break;

// finish registration whenever parent registration

// succeeded, even when racing with termination,

// since these are part of the same "transaction".

while (!UNSAFE.compareAndSwapLong

(this, stateOffset, s,

((long)phase << PHASE_SHIFT) | adjust)) {

s = state;

phase = (int)(root.state >>> PHASE_SHIFT);

// assert (int)s == EMPTY;

}

break;

}

}

}

}

return phase;

}

// 等待onAdvance()方法执行完毕

// 原理是先自旋一定次数,如果进入下一个阶段,这个方法直接就返回了,

// 如果自旋一定次数后还没有进入下一个阶段,则当前线程入队列,等待onAdvance()执行完毕唤醒

private int internalAwaitAdvance(int phase, QNode node) {

// 保证队列为空

releaseWaiters(phase-1); // ensure old queue clean

boolean queued = false; // true when node is enqueued

int lastUnarrived = 0; // to increase spins upon change

// 自旋的次数

int spins = SPINS_PER_ARRIVAL;

long s;

int p;

// 检查当前阶段是否变化,如果变化了说明进入下一个阶段了,这时候就没有必要自旋了

while ((p = (int)((s = state) >>> PHASE_SHIFT)) == phase) {

// 如果node为空,注册的时候传入的为空

if (node == null) { // spinning in noninterruptible mode

// 未完成的参与者数量

int unarrived = (int)s & UNARRIVED_MASK;

// unarrived有变化,增加自旋次数

if (unarrived != lastUnarrived &&

(lastUnarrived = unarrived) < NCPU)

spins += SPINS_PER_ARRIVAL;

boolean interrupted = Thread.interrupted();

// 自旋次数完了,则新建一个节点

if (interrupted || --spins < 0) { // need node to record intr

node = new QNode(this, phase, false, false, 0L);

node.wasInterrupted = interrupted;

}

}

else if (node.isReleasable()) // done or aborted

break;

else if (!queued) { // push onto queue

// 节点入队列

AtomicReference head = (phase & 1) == 0 ? evenQ : oddQ;

QNode q = node.next = head.get();

if ((q == null || q.phase == phase) &&

(int)(state >>> PHASE_SHIFT) == phase) // avoid stale enq

queued = head.compareAndSet(q, node);

}

else {

try {

// 当前线程进入阻塞状态,跟调用LockSupport.park()一样,等待被唤醒

ForkJoinPool.managedBlock(node);

} catch (InterruptedException ie) {

node.wasInterrupted = true;

}

}

}

// 到这里说明节点所在线程已经被唤醒了

if (node != null) {

// 置空节点中的线程

if (node.thread != null)

node.thread = null; // avoid need for unpark()

if (node.wasInterrupted && !node.interruptible)

Thread.currentThread().interrupt();

if (p == phase && (p = (int)(state >>> PHASE_SHIFT)) == phase)

return abortWait(phase); // possibly clean up on abort

}

// 唤醒当前阶段阻塞着的线程

releaseWaiters(phase);

return p;

}

增加一个参与者总体的逻辑为:

(1)增加一个参与者,需要同时增加parties和unarrived两个数值,也就是state的中16位和低16位;

(2)如果是第一个参与者,则尝试原子更新state的值,如果成功了就退出;

(3)如果不是第一个参与者,则检查是不是在执行onAdvance(),如果是等待onAdvance()执行完成,如果否则尝试原子更新state的值,直到成功退出;

(4)等待onAdvance()完成是采用先自旋后进入队列排队的方式等待,减少线程上下文切换;

arriveAndAwaitAdvance()方法

当前线程当前阶段执行完毕,等待其它线程完成当前阶段。

如果当前线程是该阶段最后一个到达的,则当前线程会执行onAdvance()方法,并唤醒其它线程进入下一个阶段。

public int arriveAndAwaitAdvance() {

// Specialization of doArrive+awaitAdvance eliminating some reads/paths

final Phaser root = this.root;

for (;;) {

// state的值

long s = (root == this) ? state : reconcileState();

// 当前阶段

int phase = (int)(s >>> PHASE_SHIFT);

if (phase < 0)

return phase;

// parties和unarrived的值

int counts = (int)s;

// unarrived的值(state的低16位)

int unarrived = (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);

if (unarrived <= 0)

throw new IllegalStateException(badArrive(s));

// 修改state的值

if (UNSAFE.compareAndSwapLong(this, stateOffset, s,

s -= ONE_ARRIVAL)) {

// 如果不是最后一个到达的,则调用internalAwaitAdvance()方法自旋或进入队列等待

if (unarrived > 1)

// 这里是直接返回了,internalAwaitAdvance()方法的源码见register()方法解析

return root.internalAwaitAdvance(phase, null);

// 到这里说明是最后一个到达的参与者

if (root != this)

return parent.arriveAndAwaitAdvance();

// n只保留了state中parties的部分,也就是中16位

long n = s & PARTIES_MASK; // base of next state

// parties的值,即下一次需要到达的参与者数量

int nextUnarrived = (int)n >>> PARTIES_SHIFT;

// 执行onAdvance()方法,返回true表示下一阶段参与者数量为0了,也就是结束了

if (onAdvance(phase, nextUnarrived))

n |= TERMINATION_BIT;

else if (nextUnarrived == 0)

n |= EMPTY;

else

// n 加上unarrived的值

n |= nextUnarrived;

// 下一个阶段等待当前阶段加1

int nextPhase = (phase + 1) & MAX_PHASE;

// n 加上下一阶段的值

n |= (long)nextPhase << PHASE_SHIFT;

// 修改state的值为n

if (!UNSAFE.compareAndSwapLong(this, stateOffset, s, n))

return (int)(state >>> PHASE_SHIFT); // terminated

// 唤醒其它参与者并进入下一个阶段

releaseWaiters(phase);

// 返回下一阶段的值

return nextPhase;

}

}

}

arriveAndAwaitAdvance的大致逻辑为:

(1)修改state中unarrived部分的值减1;

(2)如果不是最后一个到达的,则调用internalAwaitAdvance()方法自旋或排队等待;

(3)如果是最后一个到达的,则调用onAdvance()方法,然后修改state的值为下一阶段对应的值,并唤醒其它等待的线程;

(4)返回下一阶段的值;

总结

(1)Phaser适用于多阶段多任务的场景,每个阶段的任务都可以控制得很细;

(2)Phaser内部使用state变量及队列实现整个逻辑【本篇文章由公众号“彤哥读源码”原创,请支持原创,谢谢!】;

(3)state的高32位存储当前阶段phase,中16位存储当前阶段参与者(任务)的数量parties,低16位存储未完成参与者的数量unarrived;

(4)队列会根据当前阶段的奇偶性选择不同的队列;

(5)当不是最后一个参与者到达时,会自旋或者进入队列排队来等待所有参与者完成任务;

(6)当最后一个参与者完成任务时,会唤醒队列中的线程并进入下一个阶段;

彩蛋

Phaser相对于CyclicBarrier和CountDownLatch的优势?

答:优势主要有两点:

(1)Phaser可以完成多阶段,而一个CyclicBarrier或者CountDownLatch一般只能控制一到两个阶段的任务;

(2)Phaser每个阶段的任务数量可以控制,而一个CyclicBarrier或者CountDownLatch任务数量一旦确定不可修改。

推荐阅读

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

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

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

相关文章

人工智能将为传统制造业带来什么?

来源 &#xff1a;数据科学与人工智能“物理世界”&#xff08;以制造业设备所代表&#xff09;和“数字世界”&#xff08;由人工智能、传感器等技术代表&#xff09;的碰撞催生了制造业的巨大的转变。两个世界的融合将为下一轮经济发展注入新的动能。以人工智能为代表的新技术…

网络模拟器 eNSP、EVE-NG、GNS3、Packet Tracert

工欲善其事必先利其器&#xff0c;学习网络不可能都有真实的网络环境可以使用&#xff0c;这时就需要使用网络模拟软件&#xff0c;模拟一些网络环境&#xff0c;构建一些网络拓扑&#xff0c;然后学习研究网络技术 初学时不要在模拟器的选择上纠结&#xff0c;PT、GNS3 就足够…

信息技术守护人类文明DNA

来源&#xff1a;中国青年报 作者&#xff1a;胡春艳21世纪最时髦的技术&#xff0c;改变的不仅是未来&#xff0c;在被称为“最古老的研究”的文物与博物馆界&#xff0c;感受到的变化可能更加显著。天津大学文化遗产保护与传承信息技术研究中心主任张加万团队历时10年&…

java 单例方法_Java-单例模式 - 惊觉

单例模式单例(Singleton)模式是设计模式之一&#xff0c;最显著的特点就是一个类在一个JVM中只有一个实例&#xff0c;避免繁琐的创建销毁实例。简单例子先看简单的单例模式实现完整代码&#xff1a;Singleton_Test类使用单例模式 &#xff0c;采用饿汉式方法。public class Si…

DIY协同办公平台(C/S)系列4之通讯录篇

通讯录分为企业通讯录和员工个人通讯录。企业通讯录与企业员工资料相连接&#xff0c;读取其资料。而员工个人通讯录主要是员工的自己业务往来和个人的通讯资料保存。 1.主体toolbar 用于快捷掉出与员工相关的功能&#xff0c;打算支持自定义和本地化&#xff0c;通过对文本的…

iRank: 基于互联网类脑架构的阿尔法鹰眼发展趋势评估

前言&#xff1a;iRank是人工智能学家基于科学院研究团队“互联网类脑智能巨系统架构”和”AI 智商评估模型”等研究成果&#xff0c;对智能产业优秀企业、产品和技术进行评估、发掘和推荐的项目。iRank目前拥有互联网、人工智能、机器人&#xff0c;大数据、通讯技术、芯片、脑…

Java加密与解密的艺术~AES-GCM-NoPadding实现

来源&#xff1a;Java AES加密和解密_一名可爱的技术搬运工-CSDN博客 高级加密标准 &#xff08;AES&#xff0c;Rijndael&#xff09;是一种分组密码加密和解密算法&#xff0c;是全球使用最广泛的加密算法。 AES使用128、192或256位的密钥来处理128位的块。 本文向您展示了一…

STL bitset用法总结

c bitset类用法&#xff1a;http://blog.csdn.net/qll125596718/article/details/6901935 C 参考&#xff1a;http://www.cplusplus.com/reference/bitset/bitset/ http://happyboy200032.blog.163.com/blog/static/46903113201291252033712/ http://blog.csdn.net/e68948…

全球智能制造发展现状及前景预测 工业机器人引领行业发展

来源&#xff1a;前瞻产业研究院全球智能制造发展历程智能制造是伴随信息技术的不断普及而逐步发展起来的。1988年&#xff0c;美国纽约大学的怀特教授&#xff08;P&#xff0e;K&#xff0e;Wright&#xff09;和卡内基梅隆大学的布恩教授&#xff08;D&#xff0e;A&#xf…

vb整合多个excel表格到一张_VB合并工作表下载

VB合并工作表最新版是一款功能强大且界面简洁美观的excel表格合并工具&#xff0c;VB合并工作表最新版操作简便且易上手可以运行稳定&#xff0c;能够为大家解决合成打开卡顿等问题&#xff0c;实现多薄多表合并&#xff0c;VB合并工作表官方版还可以有效轻松提升工作效率。软件…

sql server 模糊查询

模糊查询——like select * from 表名where id like %2%select * from 表名where id like d%% escape d 查询时&#xff0c;字段中的内容并不一定与查询内容完全匹配&#xff0c;只要字段中含有这些内容 模糊查询&#xff0d;&#xff0d;is null select * from 表名 where na…

java.security.InvalidKeyException: Illegal key size

来源&#xff1a;https://blog.csdn.net/dling8/article/details/84061948 今天在使用idea打包maven项目时&#xff0c;出现这个错误&#xff1a;java.security.InvalidKeyException: Illegal key size。&#xff08;测试也可能会出现这个问题&#xff09;貌似是因为加密出现了…

NAT 详解

NAT技术(一、二、三、四、五) 系列&#xff1a;https://blog.51cto.com/wwwcisco/category1.html CCNA学习笔记之NAT&#xff1a;http://sweetpotato.blog.51cto.com/533893/1392884 网络地址转换NAT原理及应用&#xff1a;http://blog.csdn.net/xiaofei0859/article/details/…

趋势|人工智能疯狂洗脑,最聪明的钱已转向这16项技术

来源&#xff1a;21世纪商业评论 摘要&#xff1a;Gartner今年发布的技术成熟曲线中&#xff0c;出现了16个处于上升阶段的新兴技术&#xff0c;这些正在崛起的技术&#xff0c;或将成为企业未来几年战略性技术趋势的热点。不久前&#xff0c;信息技术研究公司Gartner发布了201…

OSI七层协议模型和TCP/IP四层模型

TCP/IP 协议栈及 OSI 参考模型详解&#xff1a;https://blog.csdn.net/guobing19871024/article/details/79415846 OSI七层网络模型&#xff0c;TCP/IP四层网络模型与网络协议解析&#xff1a;http://www.360doc.com/content/13/1123/17/7267612_331579105.shtml [网络必学]TCP…

基于java处理ofd格式文件

一、ofd格式介绍 国家发布过一份关于ofd编码格式的资料&#xff0c;本来我想传上去的发现资源重复了&#xff0c;你们可以找找看&#xff0c;没有的话留个邮箱&#xff0c;我看到会发给你们的 ofd本质上其实是一个压缩文件&#xff0c;咱们把他当做一个压缩包来处理就好了&…

一文带你读懂深度学习:AI 认识世界的方式如同小孩

来源&#xff1a;36氪摘要&#xff1a;搞清楚儿童的大脑究竟是如何运转的&#xff0c;然后设计出一个电子版本能够同样有效地运转&#xff0c;可能需要计算机科学家们几十年的努力。如果你经常花时间和小孩子待在一起的话&#xff0c;你会不由得思考小孩子怎么能够学习得如此之…

微信支付PKIX path building failed

异常信息 javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification p ath to requested targetat sun.security.ssl…

Nvidia黄仁勋发布了全球最大GPU

来源&#xff1a;凤凰科技花子健摘要&#xff1a;有竞争在&#xff0c;更大的惊喜才有可能会到来。北京时间3月28日凌晨00:00&#xff0c;英伟达GTC 2018&#xff08;GPU Technology Conference 2018&#xff0c;以下简称GTC&#xff09;在美国圣何塞举行。英伟达再一次在提升计…

计算机网络(4):网络层

网络层提供的两种服务 虚电路服务&#xff08;Virtual Circuit Service&#xff09;和数据报服务&#xff08;Datagram Service&#xff09;是在网络层&#xff08;第三层&#xff09;提供的两种不同的通信服务。它们主要区别在于建立连接的方式和数据传输的方式。 虚电路服务…