JUC包下面的四大天王+线程池部分知识
一)Semphore:限流器用我就对了
Java中信号量Semphore是把操作系统原生的信号量封装了一下,本质就是一个计数器,描述了 可用资源的个数,主要涉及到两个操作
如果计数器为
0
了,继续Р操作,就会出现阻塞等待的情况
P
操作:申请一个可用资源,计数器-1V
操作:释放一个可用资源,计数器+1停车场门口有一个灯牌,会显示停车位还剩余多少个,每进去一辆车,显示的停车位数量就-1,就相当于进行了一次P操作,每出去一辆车, 显示的停车位数量就+1,就相当于进行了一次V操作,而当停车场的剩余车位为0时,显示的停车位数量就为0了;
1)创建
Semaphore
示例, 初始化为4
, 表示有4
个可用资源.
2)acquire
方法表示申请资源(P
操作),release
方法表示释放资源(V
操作)
public class Main{public static void main(String[] args) {Semaphore semaphore=new Semaphore(10);Runnable runnable=new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"开始申请资源");try {semaphore.acquire();System.out.println(Thread.currentThread().getName()+"已经获取到资源了");semaphore.release();System.out.println(Thread.currentThread().getName()+"开始释放资源了");} catch (InterruptedException e) {throw new RuntimeException(e);}}};for(int i=0;i<10;i++){Thread t=new Thread(runnable);t.start();}} }
public class Main{public static int count=0;public static void main(String[] args) throws InterruptedException {Semaphore semaphore=new Semaphore(1);Thread t1=new Thread(()-> {for (int i = 0; i < 10000; i++) {try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2=new Thread(()->{for(int i=0;i<10000;i++){try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);} }
基于Semphore可以实现限流器:
什么是限流:比如说某一个广场,他的日载流量是6W,那么如果说假设有一天来了10W人,但是只能进去6W人,这个时候就只能排队入园了,因为工作人员始终会将人数控制在6W人
咱们再从生活中的事例回到程序当中,假设一个程序只能为 10W 人提供服务,突然有一天因为某个热点事件,造成了系统短时间内的访问量迅速增加到了 50W,那么导致的直接结果是系统崩溃,任何人都不能用系统了,显然只有少人数能用远比所有人都不能用更符合预期,因此这个时候要使用限流了
Semphore本身是依靠计数器的思想来进行实现的,它可以控制对于共享资源的访问数量,当线程需要访问该资源的时候,他必须先进行获取一个许可,就是从计数器中获取到资源
当计数器本身大于0的时候,线程可以获取到这个可用资源并且能够继续执行
当计数器本身等于0的时候,线程将会被阻塞,直到有其他的线程释放资源
Semphore本身有两个重要的操作,acquire()和realse()操作
1)当线程需要访问共享资源的时候,它会调用acquire方法来获取资源,如果计数器的值大于0,那么acquire()方法会将计数器的值减1,并且允许线程继续运行,如果计数器的值等于0,那么acquire()方法会使得线程阻塞,知道有其他线程释放资源
2)当线程使用完成共享资源以后,该线程可以调用realse方法来释放资源,realse()方法会使得计数器的值+1,表示有一个资源可以使用,其他被阻塞的线程可以有机会获得可用资源并且+1;
关于公平模式和非公平模式:
在这里面所谓的公平模式就是说线程调用acquire的先后顺序来获取到这个可用资源的,公平模式遵循先进先出原则,所以非公平模式是抢占式的,也就是说有可能一个新的获取线程恰好在一个许可证释放以后得到了这个许可证,但是这个已经获取许可证的线程前面还存在着一些其他的线程,当然在这里面非公平模式的性能比较高;
假设说,当有时候需要等待某一些线程执行完成了之后,再来执行主线程的代码,此时应该怎么做呢?可能有人会说,简单,用 join() 方法等待线程执行完成之后再执行主线程就行了,当然,如果使用的是 Thread 来执行任务,那这种写法也是可行的。然而真实的(编码)环境中我们是不会使用 Thread 来执行多任务的,而是会使用线程池来执行多任务,这样可以避免线程重复启动和销毁所带来的性能开销;
二)CountDownLatch:别急,等人齐了在开团
撞线:调用latch.countDown()
比赛结束,统计成绩:latch.await(),只要还存在着有任意的一个选手不进行撞线,那么比赛就无法结束,只有说所有的选手比赛撞了线,那么最终的比赛才可以结束
public class Main {public static void main(String[] args) throws InterruptedException {CountDownLatch latch=new CountDownLatch(10);for(int i=0;i<10;i++){Thread t=new Thread(()->{System.out.println("线程"+Thread.currentThread().getName()+"开始起跑");try {Thread.sleep(new Random().nextInt(10000));System.out.println("线程"+Thread.currentThread().getName()+"开始撞线");latch.countDown();} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}latch.await();System.out.println("比赛完成");} }
1)使用CountDownLatch可以实现等待所有任务执行完成以后再来执行主任务的功能,他就是类似于说好像比赛中等待所有运动员都完成比赛以后再来公布排名一样,当然咱们在大玩着荣耀的时候也是一样,只有说所有人集合完毕以后在开团
2)而CountDownLatch就是通过计数器来实现等待功能的,当创建CountDownLatch的时候会创建一个大于0的计数器,每一次调用countDown()方法的时候计数器的值会减1,直到计数器的值变成0以后,等待的任务就可以继续执行了
3)countDownLatch在底层实现的时候是依靠内部创建并维护了一个voltaile的计数器,当调用countDown()方法的时候,会尝试将整数计数器-1,CountDownLatch 在创建的时候需要传入一个整数,在这个整数“倒数”到 0 之前,主线程需要一直挂起等待,直到其他的线程都执行之后,主线才能继续执行
public static void main(String[] args) throws InterruptedException {//创建CountDownLatch实现两个计数器CountDownLatch latch=new CountDownLatch(2);//创建线程池执行任务ExecutorService service= Executors.newFixedThreadPool(2);service.submit(new Runnable() {@Overridepublic void run() {System.out.println("我是线程池提交的第一个任务");latch.countDown();}});service.submit(new Runnable() {@Overridepublic void run() {System.out.println("我是线程池提交的第二个任务");latch.countDown();}});latch.await();System.out.println("线程池中的任务已经全部执行完成");}
三)循环栅栏(Cycbarrier):人齐了老司机就可以发车了
循环栅栏实现一个可以循环利用的屏障
https://img-blog.csdnimg.cn/img_convert/f10e1adb034e3ebaa2c02b11596386ee.gif
1)CycliBarrier作用是让一组线程之间可以相互等待,当到达一个共同点的时候,所有之前等待的线程会冲破栅栏,一起向下执行
2)现在举个例子来说:伟哥要做末班车回家,公交车站的司机会等待车上面的所有乘客坐满以后再来发车,还有比如说王者荣耀,得等待5个队友游戏都加载完了才可以进入到游戏
3)本质上来说是让多个线程共同相互等待,知道说当所有的线程都到达了屏障点以后,之前的所有线程才可以继续向下执行,CycBarrier本身就象老司机开车一样,如果车上面还有空闲的座位,那么司机就得等着,只有说当作为坐满以后,老司机才发车
public static void main(String[] args) {CyclicBarrier barrier=new CyclicBarrier(10, new Runnable() {@Overridepublic void run() {System.out.println("现在司机上面的人都到齐了开始进行发车");System.out.println("当前线程池中的任务都已经执行完成了");}});ExecutorService service=Executors.newFixedThreadPool(10);for(int i=0;i<10;i++){service.submit(new Runnable() {@Overridepublic void run() {try {Thread.sleep(new Random().nextInt(5000));System.out.println("当前乘客开始上车"+Thread.currentThread().getName());barrier.await();//当前判断线程池中的任务执行完成可以执行多次System.out.println("当前线程下车"+Thread.currentThread().getName());} catch (InterruptedException e) {throw new RuntimeException(e);} catch (BrokenBarrierException e) {throw new RuntimeException(e);}}});}} }
在CycliBarrier底层是基于计数器来实现的,当count不为0的时候,每一个线程在到达屏障点以后会先进行调用await()方法将自己阻塞,此时计数器会减1,此时这个线程会阻塞在这个屏障处,当循环栅栏的计数器被减为0的时候,所有调用await()的线程就会被唤醒,就会冲破栅栏,一起执行,CountDownLatch和CycliBarrier在底层都是依靠计数器来实现的,但是CountDownLatch只能使用一次,但是CycliBarrier却可以使用多次,这就是两者最大的区别
总结:CycliBarrier在底层是依靠ReentranLock来实现计数器的原子性更新的,CycliBarrier最常使用的就是await()方法,使用该方法就会将计数器的值减1,并判断当前的计数器是否为0,如果不是0就阻塞等待,并且当计数器变成0以后,该线程也就是阻塞在循环栅栏的线程才可以继续执行剩余任务;
三)线程池的状态:
1)Running状态:运行状态,线程池创建完成以后就进入到这个状态,如果不手动调用关闭方法,那么线程池在整个程序运行过程中都是这个状态;
2)ShutDown状态:关闭状态,线程池本身不再接受新任务的提交,但是会有先将线程池中已经存在的任务处理完成
3)Stop停止状态:不再接受新任务的提交,并且会中断正在执行的任务,放弃任务队列中已经存在的任务
4)tidying状态:整理状态,所有的任务都执行完成以后,也包括任务队列中的任务执行完成,当前线程池中的活动线程数降为0的状态,到达此状态以后会调用线程池的terminated方法
5)terminated状态:销毁状态,当调用线程池的terminated方法以后会进入到这个状态
ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 10,100, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread(r);}}){@Overrideprotected void terminated() {super.terminated();System.out.println("线程池终止");}};
1)当进行调用shutDown方法的时候,线程池中的状态会由Running状态到达shutDown状态,最后在到达tidying状态,最后到达terminated状态
2)当进行调用shutDownNow方法的时候,线程池中的状态会由running状态到达stop状态,最后在到达tidying状态,最后到达terminated状态
3)进行调用terminated方法,线程池会直接从tidying状态到达terminated状态,可以在阻塞队列的时候重写此方法,默认来说这个方法是空的
四)如何判断线程池中的任务都已经执行完成了?
1)在很多场景下,都希望等待线程池中的所有任务都执行完,然后再来执行下一步操作,对于Thread类来说,这样的实现是很简单的,加上一个join方法就解决了,但是对于线程池的判断就比较麻烦了
2)从上面的执行结果可以看出来,程序先打印了任务执行完成,再来继续打印并执行线程池的任务,这种执行顺序混乱的结果不是我们想要看到的,我们期望的结果就是等到鲜橙汁中的所有任务都执行完成了,再来进行打印线程池执行完成的信息;
3)产生少数问题的原因就是主线程main和线程池是并发执行的,所以说当线程池还没有执行完main现成的打印结果就已经执行了,想要解决这个问题就需要在打印结果之前,先判断线程池中的任务是否已经执行完成,如果没有执行完成就等到任务执行完成再来打印结果
public static void main(String[] args) {ExecutorService service=Executors.newFixedThreadPool(10);for(int i=0;i<10;i++){service.submit(new Runnable() {@Overridepublic void run() {System.out.println("开始执行线程池中的任务");}});}System.out.println("线程池中的所有任务执行完成");}
1)使用isTerminated()方法来判断:
1)使用线程池的终止状态来进行判断线程池中的任务是否已经全部执行完成,但是如果想要让线程之中的状态改变就需要调用shutDown()方法,不然线程池会一直处于Running运行状态那么就没有办法来进行判断是否处于终止状态来判断线程池中的任务是否已经全部执行
2)shutdown方法是启动线程池有序关闭的方法,它在关闭之前会执行完成所有已经提交的任务,并且不会再进行接收新的任务,当线程池中的所有任务都执行完成以后,线程池就处于终止状态了,此时isTerminated()方法返回的结果也就是true了;
缺点:需要关闭线程池
ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 10,100, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread(r);}}){@Overrideprotected void terminated() {super.terminated();System.out.println("线程池终止");}};executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务1");}});executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务2");}});executor.shutdown();while(!executor.isTerminated()){}System.out.println("线程池中的任务已经执行完成了");
2)判断getCompletedTaskCount和getTaskCount是否相等
getTaskCount()返回执行计划任务的总数,但是因为本身任务和线程的状态都在不断地发生变化,因此返回的值是一个近似值;
getCompetedTaskCount()返回完成执行的任务总数,但是因为本身任务和线程的状态都在不断地发生变化,因此返回的值是一个近似值,但是在连续的调用中并不会减少
虽然不需要关闭线程池,但是可能会造成一定的误差
3)调用countDownLatch和CycliBarrier
需要注意的是countDownLatch中的countDown()方法和CycliBarrier中的await()方法需要在线程池的run方法的最后调用
4)使用FutureTask
FutureTask中的优势就是判断比较精准,调用每一个线程的FutureTask的get方法就是等待该任务执行完成的,需要使用submit进行提交:
public static void main(String[] args) throws ExecutionException, InterruptedException {ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0,TimeUnit.SECONDS,new LinkedBlockingDeque<>(100));FutureTask<Integer> task1=new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int a=10;a++;System.out.println("a++完成");return a;}});FutureTask<Integer> task2=new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int b=11;b++;System.out.println("b++完成");return b;}});executor.submit(task1);executor.submit(task2);Integer result1= task1.get();Integer result2=task2.get();System.out.println("线程池中的任务都已经执行完成");}
五)submit和execute的区别:
1)接收到的参数不同:submit方法只能接受到runnable接口的任务,但是submit方法及可以接受到runnable方法的任务,也可以接收到callable,futureTask类型的任务,前者没有返回值,后者可以后返回值;
2)execute()的返回值是void,线程提交后不能得到线程的返回值,submit()的返回值是Future,通过Future的get()方法可以获取到线程执行的返回值,get()方法是同步的,执行get()方法时,如果线程还没执行完,会同步等待,直到线程执行完成
注意:虽然submit()方法可以提交Runnable类型的参数,但执行Future方法的get()时,线程执行完会返回null,不会有实际的返回值,这是因为Runable本来就没有返回值
相关文章:

JUC包下面的四大天王+线程池部分知识
一)Semphore:限流器用我就对了 Java中信号量Semphore是把操作系统原生的信号量封装了一下,本质就是一个计数器,描述了 可用资源的个数,主要涉及到两个操作 如果计数器为0了,继续Р操作,就会出现阻塞等待的情况 P操作:申…...
AGV系统控制位置管理功能
# ファイル: agv_locattion.py # 説明: AGV (Automated Guided Vehicle) の位置情報を管理し、UDPサーバーとして動作するGUIアプリケーションです。 # 必要なライブラリをインポート import tkinter as tk import socket import threading def AGV_handle_submit(canvas, st…...

JavaScript从入门到精通系列第三十三篇:详解正则表达式语法(二)
文章目录 一:正则表达式 1: 检查一个字符串中是否有. 2:第二种关键表达 3:第三种关键表达 编辑4:第四种关键表达 5:第五种关键表达 6:第六种关键表达 二:核心表达二 1&am…...

由于找不到 d3dx9_43.dll,无法继续执行代码。重新安装程序可能会解决此问题
电脑出现d3dx9_43.dll缺失的问题,通常是由于DirectX组件未安装或损坏导致的。为了解决这个问题,我为您提供了以下四个解决方法: d3dx9_43.dll解决方法1. 使用dll修复程序修复 首先,使用系统文件程序dll进行修复操作非常简单&…...
AI全栈大模型工程师(二十一)LangChain和SemanticKernel怎么选
LangChain 和 Semantic Kernel 怎么选? #%% md 划重点: 两者都值得学C#、JavaScript 和 Java 现在没得选做原型,首选 LangChain。功能多,开发快做产品,还是 SK 长期更可依赖建议只用 SK 的 Connectors 和 Plugins 能力…...

npm install 报错 chromedriver 安装失败的解决办法
npm install chromedriver --chromedriver_cdnurlhttp://cdn.npm.taobao.org/dist/chromedriver...
C语言--每日五道选择题--Day6
第一题 1、声明以下变量,则表达式: ch/i (f*d – i) 的结果类型为( ) char ch; int i; float f; double d; A: char B: int C: float D: double 答案及解析 D 基本数据类型的等级从低到高如下:char-> int-> long-> f…...
element-ui 封装 表格
一、封装表格组件 <template><el-table :data"list" :default-sort"{ prop: date }" style"width: 100%"><template v-for"item in tableColumn"><el-table-columnv-if"item.filters":prop"item…...

数据的使用、表关系的创建、Django框架的请求生命周期流程图
目录 一、数据的增删改查 1. 用户列表的展示 2. 修改数据的逻辑分析 3. 删除功能的分析 二、如何创建表关系 三、Django的请求生命周期流程图 一、数据的增删改查 1. 用户列表的展示 把数据表中得用户数据都给查询出来展示在页面上 查询数据 def userlist(request):&qu…...

Python基础教程:类--继承和方法的重写
嗨喽,大家好呀~这里是爱看美女的茜茜呐 什么是继承 继承就是让类与类之间产生父子关系,子类可以拥有父类的静态属性和方法 继承就是可以获取到另一个类中的静态属性和普通方法(并非所有成员) 在python中,新建的类可…...
Three.js提供了多种类型的灯光
Three.js提供了多种类型的灯光,包括环境光、点光源、平行光源和聚光灯。这些灯光可以用来照亮场景中的物体,使其看起来更加真实。 环境光(AmbientLight):环境光会均匀地照亮场景中的所有物体,没有方向,不能用来投射阴…...
精通Nginx(10)-负载均衡
负载均衡就是将前端过来的负载分发到两台或多台应用服务器。Nginx支持多种协议的负载均衡,包括http(s)、TCP、UDP(关于TCP、UDP负载均衡另文讲述)等。 目录 HTTP负载均衡 负载均衡策略 轮询 least_conn(最少连接) hash(通用哈希) ip_hash(IP 哈希) random(随…...

Hls学习(一)
1:CPU、DSP、GPU都算软件可编程的硬件 2:dsp在递归方面有所减弱,在递归方面有所增强,比如递归啊等,GPU可以同时处理多个进程,对于大块数据,流处理比较适用 3:为了提高运算量处理更多…...

Maven打包引入本地依赖包
Maven打包引入本地依赖包 SpringBoot 工程,Maven 在构建项目时,如何引入本地 Jar 包? 适合场景: 引用的依赖不在 Maven 仓库第三方公司提供的 SDK 包Maven 内网离线开发引入被定制改动过的 Jar 包 解决方法: 在 I…...
Docker常用命令及部署微服务项目
Docker常用命令及部署微服务项目 1、Docker常用命令 1、设置Yum源 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 2、安装docker yum -y install docker-ce 3、启动docker service docker start 4、验证 docker version 5…...
okhttp添加公共参数
在项目开发中很多时候后台都会给一些全局的公共入参,比如携带手机信息或者时间戳等字段。而我们在使用okhttp时,就需要我们单独就行二次封装处理了,对于请求全局参数,每次请求都要去写一次,那是肯定不行的。 所以就要我…...

基于SpringBoot的SSMP整合案例(开启日志与分页查询条件查询功能实现)
开启事务 导入Mybatis-Plus框架后,我们可以使用Mybatis-Plus自带的事务,只需要在配置文件中配置即可 使用配置方式开启日志,设置日志输出方式为标准输出mybatis-plus:global-config:db-config:table-prefix: tb_id-type: autoconfiguration:…...

android studio 修改图标
Android Studio 修改图标 简介 Android Studio 是一款由谷歌推出的用于开发 Android 应用程序的集成开发环境(IDE)。在开发过程中,我们可以根据自己的需求修改 Android Studio 的图标,以个性化我们的开发环境。 本文将介绍如何在…...
pytorch学习之第二课之预测温度
主要有以下几个步骤 第一:导入相应的工具包 第二:导入需要使用的数据集 第三:对导入的数据集输入进行预处理,找出特征与标签,查看数据特征的类型,判断是否需要标准化或者归一化处理 第四:构建神…...

基于Mahony互补滤波的IMU数据优化_学习笔记整理
这周自己被安排进行优化软件 IMU 姿态解算项目,之前自己只简单了解四元数,对IMU数据处理从未接触,通过这一周的学习感觉收获颇丰,在今天光棍节之际,,,用大半天的时间对这一周的收获进行整理&…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...