线程安全性的原理分析学习

news2025/3/10 18:54:16

初步认识Volatile

一段代码引发的思考 下面这段代码,演示了一个使用volatile以及没使用volatile这个关键字,对于变量更新的影响 
package com.sp.demo;

/**
 * @author : lssffy
 * @Description :
 * @date : 2024/2/16 18:42
 */
public class VolatileDemo {

    public /*volatile*/ static boolean stop=false;
    public static void main(String[] args) throws
            InterruptedException {
        Thread thread = new Thread(() -> {
            int i = 0;
            while (!stop) {
                i++;
            }
        });
        thread.start();
        System.out.println("begin start thread");
        Thread.sleep(1000);
        stop = true;
    }
}

volatile 的作用

volatile 可以使得在多处理器环境下保证了共享变量的可见性,那么到底什么是可见性呢?不知道大家有没有思考过这个问题 
在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。但是在多线程环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是所谓的可见性 
为了实现跨线程写入的内存可见性,必须使用到一些机制来实现。而volatile就是这样一种机制 volatile 关键字是如何保证可见性的? 我们可以使用【hsdis】这个工具,来查看前面演示的这段代码的汇编指令,具体的使用请查看使用说明文档 在运行的代码中,设置jvm参数如下 【-server -Xcomp -XX:+UnlockDiagnosticVMOptions XX:+PrintAssembly XX:CompileCommand=compileonly,*App.*(替换成实际运行的代码)】 然后在输出的结果中,查找下lock指令,会发现,在修改带有volatile 修饰的成员变量时,会多一个lock指令。lock是一种控制指令,在多处理器环境下,lock 汇编指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果。 为了让大家更好的理解可见性的本质,我们需要从硬件层面进行梳理 。

从硬件层面了解可见性的本质

一台计算机中最核心的组件是CPU、内存、以及I/O设备。在整个计算机的发展历程中,除了CPU、内存以及I/O设备不断迭代升级来提升计算机处理性能之外,还有一个非常核心的矛盾点,就是这三者在处理速度的差异。CPU的计算速度是非常快的,内存次之、最后是IO设备比如磁盘。而在绝大部分的程序中,一定会存在内存访问,有些可能还会存在I/O设备的访问 为了提升计算性能,CPU从单核升级到了多核甚至用到了超线程技术最大化提高 CPU 的处理性能,但是仅仅提升CPU性能还不够,如果后面两者的处理性能没有跟上,意味着整体的计算效率取决于最慢的设备。为了平衡三者的速度差异,最大化的利用CPU提升性能,从硬件、操作系统、编译器等方面都做出了很多的优化
1. CPU增加了高速缓存 
2. 操作系统增加了进程、线程。通过CPU的时间片切换最大化的提升CPU的使用率 
3. 编译器的指令优化,更合理的去利用好CPU的高速缓存 然后每一种优化,都会带来相应的问题,而这些问题也是导致线程安全性问题的根源。为了了解前面提到的可见性问题的本质,我们有必要去了解这些优化的过程

CPU 高速缓存

线程是CPU调度的最小单元,线程设计的目的最终仍然是更充分的利用计算机处理的效能,但是绝大部分的运算任务不能只依靠处理器“计算”就能完成,处理器还需要与内存交互,比如读取运算数据、存储运算结果,这个I/O操作是很难消除的。而由于计算机的存储设备与处理器的运算速度差距非常大,所以现代计算机系统都会增加一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中。

在这里插入图片描述
通过高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题,缓存一致性。
*

缓存一致性

首先,有了高速缓存的存在以后,每个CPU的处理过程是,先将计算需要用到的数据缓存在CPU高速缓存中,在CPU进行计算时,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中。在整个运算过程完成后,再把缓存中的数据同步到主内存。 
由于在多CPU种,每个线程可能会运行在不同的CPU内,并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个CPU中,如果在不同CPU中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题  为了解决缓存不一致的问题,在CPU层面做了很多事情,主要提供了两种解决办法 
1. 总线锁 
2. 缓存锁 
总线锁和缓存锁 
总线锁,简单来说就是,在多cpu下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,这种机制显然是不合适的  
如何优化呢?最好的方法就是控制锁的保护粒度,我们只需要保证对于被多个 CPU 缓存的同一份数据是一致的就行。所以引入了缓存锁,它核心机制是基于缓存一致性协议来实现的。 
缓存一致性协议 
为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有MSI,MESI,MOSI等。最常见的就是MESI协议。接下来给大家简单讲解一下MESI MESI 表示缓存行的四种状态,分别是 
1. M(Modify) 表示共享数据只缓存在当前 CPU 缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致 
2. E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改 
3. S(Shared) 表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致 
4. I(Invalid) 表示缓存已经失效 在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听(snoop)其它Cache的读写操作

在这里插入图片描述
在这里插入图片描述

对于MESI 协议,从 CPU 读写角度来说会遵循以下原则: CPU读请求:缓存处于M、E、S状态都可以被读取,I状态CPU只能从主存中读取数据 
CPU写请求:缓存处于M、E状态才可以被写。对于S状态的写,需要将其他CPU中缓存行置为无效才可写 使用总线锁和缓存锁机制之后,CPU对于内存的操作大概可以抽象成下面这样的结构。从而达到缓存一致性效果。

在这里插入图片描述

总结可见性的本质

由于CPU高速缓存的出现使得 如果多个cpu同时缓存了相同的共享数据时,可能存在可见性问题。也就是CPU0修改了自己本地缓存的值对于 CPU1 不可见。不可见导致的后果是 CPU1 后续在对该数据进行写入操作时,是使用的脏数据。使得数据最终的结果不可预测。 希望想在代码里面去模拟一下可见性的问题,实际上,这种情况很难模拟。因为我们无法让某个线程指定某个特定CPU,这是系统底层的算法, JVM 应该也是没法控制的。还有最重要的一点,就是你无法预测CPU缓存什么时候会把值传给主存,可能这个时间间隔非常短,短到你无法观察到。最后就是线程的执行的顺序问题,因为多线程你无法控制哪个线程的某句代码会在另一个线程的某句代码后面马上执行。 
所以我们只能基于它的原理去了解这样一个存在的客观事实 。了解到这里,大家应该会有一个疑问,刚刚不是说基于缓存一致性协议或者总线锁能够达到缓存一致性的要求吗?为什么还需要加 volatile 关键字?或者说为什么还会存在可见性问题呢? 

MESI 优化带来的可见性问题

MESI 协议虽然可以实现缓存的一致性,但是也会存在一些问题。 就是各个CPU缓存行的状态是通过消息传递来进行的。如果CPU0 要对一个在缓存中共享的变量进行写入,首先需要发送一个失效的消息给到其他缓存了该数据的CPU。并且要等到他们的确认回执。CPU0 在这段时间内都会处于阻塞状态。为了避免阻塞带来的资源浪费。在cpu中引入了Store Bufferes。 

在这里插入图片描述
CPU0只需要在写入共享数据时,直接把数据写入到store bufferes 中,同时发送invalidate 消息,然后继续去处理其他指令。
当收到其他所有CPU发送了invalidate acknowledge消息时,再将 store bufferes 中的数据数据存储至 cache line中。最后再从缓存行同步到主内存。
在这里插入图片描述
但是这种优化存在两个问题
1. 数据什么时候提交是不确定的,因为需要等待其他 cpu给回复才会进行数据同步。这里其实是一个异步操作
2. 引入了storebufferes 后,处理器会先尝试从storebuffer中读取值,如果 storebuffer 中有数据,则直接从storebuffer 中读取,否则就再从缓存行中读取 。
在这里插入图片描述
exeToCPU0和exeToCPU1分别在两个独立的CPU上执行。假如CPU0 的缓存行中缓存了isFinish 这个共享变量,并且状态为(E)、而Value可能是(S)状态。 那么这个时候,CPU0在执行的时候,会先把value=10的指令写入到storebuffer中。并且通知给其他缓存了该value变量的CPU。在等待其他CPU通知结果的时候,CPU0会继续执行isFinish=true 这个指令。 而因为当前CPU0缓存了isFinish并且是Exclusive状态,所以可以直接修改isFinish=true。这个时候CPU1发起read操作去读取isFinish的值可能为true,但是value的值不等于10。
这种情况我们可以认为是CPU的乱序执行,也可以认为是一种重排序,而这种重排序会带来可见性的问题 这下硬件工程师也抓狂了,我们也能理解,从硬件层面很难去知道软件层面上的这种前后依赖关系,所以没有办法通过某种手段自动去解决。 所以硬件工程师就说: 既然怎么优化都不符合你的要求,要不你来写吧。
所以在 CPU 层面提供了 memory barrier(内存屏障)的指令,从硬件层面来看这个 memroy barrier就是CPU flush store bufferes 中的指令。软件层面可以决定在适当的地方来插入内存屏障。

CPU 层面的内存屏障

什么是内存屏障?从前面的内容基本能有一个初步的猜想,内存屏障就是将 store bufferes 中的指令写入到内存,从而使得其他访问同一共享内存的线程的可见性。 X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)Store Memory Barrier(写屏障) 告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的 
Load Memory Barrier(读屏障) 处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的 Full Memory Barrier(全屏障) 确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作 有了内存屏障以后,对于上面这个例子,我们可以这么来改,从而避免出现可见性问题。

在这里插入图片描述
总的来说,内存屏障的作用可以通过防止CPU对内存的乱序访问来保证共享数据在多线程并行执行下的可见性 但是这个屏障怎么来加呢?回到最开始我们讲 volatile 关键字的代码,这个关键字会生成一个Lock的汇编指令,这个指令其实就相当于实现了一种内存屏障这个时候问题又来了,内存屏障、重排序这些东西好像是和平台以及硬件架构有关系的。作为Java语言的特性,一次编写多处运行。我们不应该考虑平台相关的问题,并且这些所谓的内存屏障也不应该让程序员来关心。

JMM

什么是JMM 
JMM全称是Java Memory Model.  什么是JMM呢? 
通过前面的分析发现,导致可见性问题的根本原因是缓存以及重排序。 而JMM实际上就是提供了合理的禁用缓存以及禁止重排序的方法。所以它最核心的价值在于解决可见性和有序性。 
JMM属于语言级别的抽象内存模型,可以简单理解为对硬件模型的抽象,它定义了共享内存中多线程程序读写操作的行为规范:在虚拟机中把共享变量存储到内存以及从内存中取出共享变量的底层实现细节 通过这些规则来规范对内存的读写操作从而保证指令的正确性,它解决了CPU多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可见性。需要注意的是,JMM并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序,也就是说在JMM中,也会存在缓存一致性问题和指令重排序问题。只是JMM把底层的问题抽象到JVM层面,再基于CPU层面提供的内存屏障指令,以及限制编译器的重排序来解决并发问题 
JMM抽象模型分为主内存、工作内存;主内存是所有线程共享的,一般是实例对象、静态字段、数组对象等存储在堆内存中的变量。工作内存是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成。
Java 内存模型底层实现可以简单的认为:通过内存屏障(memory barrier)禁止重排序,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于volatile,编译器将在volatile 字段的读写操作前后各插入一些内存屏障。 
JMM是如何解决可见性有序性问题的 
简单来说,JMM提供了一些禁用缓存以及进制重排序的方法,来解决可见性和有序性问题。这些方法大家都很熟悉:volatile、synchronized、final; 以及 JMM如何解决顺序一致性问题 

重排序问题

为了提高程序的执行性能,编译器和处理器都会对指令做重排序,其中处理器的重排序在前面已经分析过了。所谓的重排序其实就是指执行的指令顺序。 编译器的重排序指的是程序编写的指令在编译之后,指令可能会产生重排序来优化程序的执行性能。 从源代码到最终执行的指令,可能会经过三种重排序。

在这里插入图片描述
2 和3属于处理器重排序。这些重排序可能会导致可见性问题。
编译器的重排序,JMM提供了禁止特定类型的编译器重排序。
处理器重排序,JMM会要求编译器生成指令时,会插入内存屏障来禁止处理器重排序
当然并不是所有的程序都会出现重排序问题 编译器的重排序和CPU的重排序的原则一样,会遵守数据依赖性原则,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序,比如下面的代码,
a=1; b=a;
a=1;a=2;
a=b;b=1;
这三种情况在单线程里面如果改变代码的执行顺序,都会导致结果不一致,所以重排序不会对这类的指令做优化。这种规则也成为 as-if-serial。不管怎么重排序,对于单个线程来说执行结果不能改变。比如
int a=2;
int b=3;
int rs=a*b;
//1
//2
//3
1 和3、2和3存在数据依赖,所以在最终执行的指令中,3 不能重排序到1和2之前,否则程序会报错。由于1和2不存在数据依赖,所以可以重新排列1和2的顺序。

JMM层面的内存屏障

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器的重排序,在JMM中把内存屏障分为四类 

在这里插入图片描述

HappenBefore
它的意思表示的是前一个操作的结果对于后续操作是可见的,所以它是一种表达多个线程之间对于内存的可见性。

所以我们可以认为在JMM中,如果一个操作执行的结果需要对另一个操作课件,那么这两个操作必须要存在happens-before 关系。这两个操作可以是同一个线程,也可以是不同的线程
JMM中有哪些方法建立happen-before规则
程序顺序规则
1. 一个线程中的每个操作,happens-before于该线程中的任意后续操作; 可以简单认为是as-if-serial。 单个线程中的代码顺序不管怎么变,对于结果来说是不变的 顺序规则表示 1 happenns-before 2; 3 happens before 4
在这里插入图片描述
2. volatile 变量规则,对于volatile 修饰的变量的写的操作,一定happen-before 后续对于volatile 变量的读操作; 根据volatile 规则,2 happens before 3
在这里插入图片描述
3. 传递性规则,如果 1 happens-before 2; 3happens before 4; 那么传递性规则表示: 1 happens-before 4;
在这里插入图片描述
4. start 规则,如果线程A执行操作ThreadB.start(),那么线程A的ThreadB.start()操作 happens-before 线程 B 中的任意操作
在这里插入图片描述
5. join规则,如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
在这里插入图片描述在这里插入图片描述

6. 监视器锁的规则,对一个锁的解锁,happens-before于随后对这个锁的加锁

在这里插入图片描述
假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。

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

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

相关文章

阿里云香港轻量应用服务器是什么线路?cn2?

阿里云香港轻量应用服务器是什么线路?不是cn2。 阿里云香港轻量服务器是cn2吗?香港轻量服务器不是cn2。阿腾云atengyun.com正好有一台阿里云轻量应用服务器,通过mtr traceroute测试了一下,最后一跳是202.97开头的ip,1…

C++学习Day06之继承基本语法

目录 一、程序及输出1.1 没有继承1.2 使用继承 二、分析与总结 一、程序及输出 想象在移动端看资讯,顶部、底部、左侧和中间内容,左侧滑动栏有新闻、体育…,点击不同的新闻,中间内容呈现不同主题的文字叙述,在代码里该…

vivado RAM HDL Coding Techniques

Vivado synthesis可以解释各种RAM编码风格,并将它们映射到分布式RAM中或块RAM。此操作执行以下操作: •无需手动实例化RAM基元 •节省时间 •保持HDL源代码的可移植性和可扩展性从编码示例下载编码示例文件。 在分布式RAM和专用RAM之间的选择块存储器…

在外包干了两年的点点点,人快废了。。。

🍅 视频学习:文末有免费的配套视频可观看 🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 简单的说下,我大学的一个同学,毕业后我自己去了自研的公司,他…

Java面向对象案例之设计用户去ATM机存款取款(三)

需求及思路分析 业务代码需求: 某公司要开发“银行管理系统”,请使用面向对象的思想,设计银行的储户信息,描述存款、取款业务。 储户类的思路分析: 属性:用户姓名、密码、身份证号、账号、帐户余额 方法&a…

对前端限流操作(Redis版本)4种算法

固定时间窗口算法 固定时间窗口算法也可以叫做简单计数算法。网上有很多都将计数算法单独抽离出来。但是笔者认为计数算法是一种思想,而固定时间窗口算法是他的一种实现包括下面滑动时间窗口算法也是计数算法的一种实现。因为计数如果不和时间进行绑定的话那么失去…

【大厂AI课学习笔记】【2.2机器学习开发任务实例】(3)数据准备和数据预处理

项目开始,首先要进行数据准备和数据预处理。 数据准备的核心是找到这些数据,观察数据的问题。 数据预处理就是去掉脏数据。 缺失值的处理,格式转换等。 延伸学习: 在人工智能(AI)的众多工作流程中&#…

2024.02.18作业

1. 使用fgets统计给定文件的行数 #include <stdio.h> #include <stdlib.h> #include <string.h>int main(int argc, char const *argv[]) {if (argc ! 2){puts("input file error");puts("usage:./a.out filename");return -1;}FILE* f…

单片机学习笔记---AD/DA工作原理(含运算放大器的工作原理)

目录 AD/DA介绍 硬件电路模型 硬件电路 运算放大器 DA原理 T型电阻网络DA转换器 PWM型DA转换器 AD原理 逐次逼近型AD转换器 AD/DA性能指标 XPT2046 XPT2046时序 AD/DA介绍 AD&#xff08;Analog to Digital&#xff09;&#xff1a;模拟-数字转换&#xff0c;将模拟…

【MySQL】学习多表查询和笛卡尔积

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-N8PeTKG6uLu4bJuM {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

JMeter接口测试数据分离驱动应用

步骤&#xff1a; 创建csv文件&#xff0c;编写接口测试用例 新建线程组——创建循环控制器&#xff08;循环次数填用例总数&#xff09; 创建CSV数据文件设置&#xff0c;设置参数。&#xff08;注意&#xff1a;是否允许带引号&#xff1f;&#xff1a;一定要设置为true&a…

10M上下文,仅靠提示就掌握一门语言,Google Gemini 1.5被OpenAI抢头条是真冤

这两天&#xff0c;几乎整个AI圈的目光都被OpenAI发布Sora模型的新闻吸引了去。其实还有件事也值得关注&#xff0c;那就是Google继上周官宣Gemini 1.0 Ultra 后&#xff0c;火速推出下一代人工智能模型Gemini 1.5。 公司首席执行官 Sundar Pichai携首席科学家Jeff Dean等众高…

Python输出改变字体颜色方法(附颜色大全)

一、使用示例 print("\033[31m红色字体\033[0m") print("\033[32m绿色字体\033[0m") print("\033[33m黄色字体\033[0m") print("\033[34m蓝色字体\033[0m") print("\033[35m紫色字体\033[0m") print("\033[36m青色字体…

驶向未来:3D可视化模型重塑我们的道路认知

在科技的浪潮中&#xff0c;每一个革新都是对人类未来生活的深度洞察。而今&#xff0c;当可视化这一技术走进我们的视野&#xff0c;它不仅是一场视觉盛宴&#xff0c;更是一次对未来出行方式的全新探索。 一、从平面到立体&#xff0c;解锁道路新视角 你是否曾站在十字路口&…

【Python如何在列表中随机抽出一个元素】

1、python代码如下&#xff1a; import random a [2, 4, 8, 9, "whats up"] q random.choice(a) # 随机从列表a中输出一个元素 b random.choices(a) # 随机从列表a中取出一个元素输出一个列表 lucky_num random.randint(1, 50) # 随机从1-50中取出一个整数包…

解释 OpenAI Sora 的时空补丁:关键因素

人工智能如何将静态图像转换为动态、逼真的视频&#xff1f;OpenAI 的 Sora 通过创新地使用时空补丁来引入答案。 在快速发展的生成模型领域&#xff0c;OpenAI 的 Sora 脱颖而出&#xff0c;成为一个重要的里程碑&#xff0c;有望重塑我们对视频生成的理解和能力。我们解读了…

【简洁的代码永远不会掩盖设计者的意图】如何写出规范整洁的代码

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;Vir2021GKBS &#x1f43c;本文由…

黑马程序员——移动Web——day04

目录&#xff1a; vw适配方案 vw和vh基本使用vw布局vh问题综合案例-酷我音乐 准备工作头部布局头部内容搜索区域banner区域标题公共样式排行榜内容推荐歌单布局推荐歌单内容下载区域头部固定 1.vw适配方案 vw和vh基本使用 vw和vh是相对单位&#xff0c;相对视口尺寸计算结果…

【数据结构】图的存储与遍历

图的概念 图是由顶点集合及顶点间的关系组成的一种数据结构&#xff1a;G (V&#xff0c; E) 图分为有向图和无向图 在有向图中&#xff0c;顶点对<x, y>是有序的&#xff0c;顶点对<x&#xff0c;y>称为顶点x到顶点y的一条边(弧)&#xff0c;<x, y>和&l…

C++学习:list

1.list的定义和结构 list的使用频率不高&#xff0c;在做题时几乎遇不到需要使用list的情景。list是一种双向链表容器&#xff0c;它是标准模板库(STL)提供的一种序列容器。list容器以节点(node的形式存储元素&#xff0c;并使用指针将这些节点链接在一起&#xff0c;形成一个…