一面:
集合有哪些:
List(ArrayList Linklist ) set(Set Treeset Hashset) map(Hashmap currentHashmap hashtable )
arraylist和linkedlist区别
一个是基于数组的实现 一个是基于的链表的实现
hashmap怎么扩容(多线程扩容为什么会死循环),put过程
出现的是链表的闭环。
concurrentHashMap 1.7和1.8
1.7是采用采用的还是分段锁的机制 1.8采用的是CAS机制来实现的。
接口和抽象类区别
JVM内存分区
新生代:
eden,survivor_from, survivor_to
垃圾回收算法:
三种 标记清除 复制算法 标记整理算法
PretenureSizeThreshold,maxTenuringThreshold(默认15)
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
1线程池状态
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;
下面的几个static final变量表示runState可能的几个取值。
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
2任务的执行
ThreadPoolExecutor类中其他的一些比较重要成员变量:
rivate final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小//、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
private volatile long keepAliveTime; //线程存货时间
private volatile boolean allowCoreThreadTimeOut//是否允许为核心线程设置存活时间
private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize; //线程池最大能容忍的线程数
private volatile int poolSize; //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount; //用来记录已经执行完毕的任务个数
1)首先,要清楚corePoolSize和maximumPoolSize的含义;
2)其次,要知道Worker是用来起到什么作用的;
3)要知道任务提交给线程池之后的处理策略,这里总结一下主要有4点:
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
3线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
4任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务
5任务拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
6线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
7线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
如果是不采用是这个那就在队列中的线程是不可能出队列的,就是如果是的非公平的锁的话那就永远不能出队列。那可能能执行不到该线程。
JVM调优(不太会)
Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
如何判断对象是否应该被回收(引用计数法,可达性分析)
root根包括哪些:对象头
CMS回收过程,优缺点
并行收集垃圾
初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行:
G1回收过程
类加载过程(加载,验证,准备,解析,初始化)
双亲委派优点
七层模型
物理层 数据链路层 网络层 传输层 表示层 应用层 会话层
四次挥手过程(中间状态也要答)
HttpTCP 在传输之前会进行三次沟通,一般称为“三次握手”,传完数据断开的时候要进行四次沟通,一般称为“四次挥手”。(就是Http的连接和断开的模式)
第一次握手:主机 A 发送位码为 syn=1,随机产生 seq number=1234567 的数据包到服务器,主机 B由 SYN=1 知道, A 要求建立联机;
第 二 次 握 手 : 主 机 B 收 到 请 求 后 要 确 认 联 机 信 息 , 向 A 发 送 ack number=( 主 机 A 的seq+1),syn=1,ack=1,随机产生 seq=7654321 的包
第三次握手: 主机 A 收到后检查 ack number 是否正确,即第一次发送的 seq number+1,以及位码ack 是否为 1,若正确, 主机 A 会再发送 ack number=(主机 B 的 seq+1),ack=1,主机 B 收到后确认。
四次挥手:
TCP 建立连接要进行三次握手,而断开连接要进行四次。这是由于 TCP 的半关闭造成的。因为 TCP 连接是全双工的(即数据可在两个方向上同时传递)所以进行关闭时每个方向上都要单独进行关闭。这个单方向的关闭就叫半关闭。当一方完成它的数据发送任务,就发送一个 FIN 来向另一方通告将要终止这个方向的连接。
1关闭客户端到服务器的连接:首先客户端 A 发送一个 FIN,用来关闭客户到服务器的数据传送,然后等待服务器的确认。其中终止标志位 FIN=1,序列号 seq=u
2服务器收到这个 FIN,它发回一个 ACK,确认号 ack 为收到的序号加 1。
3关闭服务器到客户端的连接:也是发送一个 FIN 给客户端。
4客户段收到 FIN 后,并发回一个 ACK 报文确认,并将确认序号 seq 设置为收到序号加 1。
首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
为什么TCP能保证不丢失
(滑动窗口,拥塞控制)
HTTP和HTTPS的区别
GET和POST区别
安全性:get 不安全 post 相对安全
传输的大小:get的传输较小 POST的传输较大
数据的来源范式Get是从服务器上获得数据,而Post则是向服务器传递数据的。
mysql全家桶又来了,索引数据结构
采用是B+树,B+的设计底层数据结构和相关的索引的知识。
为什么用B+树而不用hash和B-Tree
二叉树(可能出现全部在左边和右边的数据)——>AVL(平衡二叉树数据大量的时候平衡的时间太多,)——>B Tree(多路平衡查找树)(数据表中的数据都是存储在页中的,所以一个页中能存储多少行数据呢指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低)——>B+ Tree的一个演变的过程来进行分析,为什么使用B+ Tree的?B+ Tree,都放在了叶子节点上。提高了检索的效率。预读原理,因为B+ Tree无 data 域,其实就是因为没有date域了,但是每次IO的页的大小是固定的,每次IO读取若干个块块中包含的Key域的值肯定更多啊,B+树单次磁盘IO的信息量大于B树,从这点来看B+树相对B树磁盘 IO 次数少。利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。1、B+Tree中因为数据都在叶子节点,所以每次查询的时间复杂度是固定的,因为稳定性保证了2、而且叶子节点之间都是链表的结构,所以B+Tree也是可以支持范围查询的,而B树每个节点 key 和 data 在一起,则无法区间查找。
InooDB和MyISAM的区别(事务,聚集索引,锁的粒度等)
| MyISAM | Innodb |
存储结构 | 每张表被存放在三个文件:frm-表格定义、MYD(MYData)-数据文件、MYI(MYIndex)-索引文件 | 所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB |
存储空间 | MyISAM可被压缩,存储空间较小 | InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引 |
可移植性、备份及恢复 | 由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作 | 免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了 |
文件格式 | 数据和索引是分别存储的,数据.MYD,索引.MYI | 数据和索引是集中存储的,.ibd |
记录存储顺序 | 按记录插入顺序保存 | 按主键大小有序插入 |
外键 | 不支持 | 支持 |
事务 | 不支持 | 支持 |
锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的) | 表级锁定 | 行级锁定、表级锁定,锁定力度小并发能力高 |
SELECT | MyISAM更优 | |
INSERT、UPDATE、DELETE | | InnoDB更优 |
select count(*) | myisam更快,因为myisam内部维护了一个计数器,可以直接调取。 | |
索引的实现方式 | B+树索引,myisam 是堆表 | B+树索引,Innodb 是索引组织表 |
哈希索引 | 不支持 | 支持 |
全文索引 | 支持 | 不支持 |
回表,联合索引查询会不会用到索引系列问题
下面我们来假设一种情况,一个表有三个字段 ID ,name ,age,我将ID设置成主键索引,name设成辅助索引。然后来看一下下面的sql:
1.select * from t where id='5';
2.select * from t where name='张三';
两个很简单的Sql,第一个sql不用说,直接通过主键索引,从树上直接可以得到结果,那第二个sql:首先name,mysql并不能得到所有列的信息(也就是*),他只能得到主键ID,然后他会根据ID在进行二次查询,这就引发了--回表问题。这就是为啥不能使用*的原因。那么怎么解决那:第一不要写*,第二利用组合索引,也就是说你根据业务实际需要,将需要的字段形成组合索引。
所以是会用到的索引的。
最左匹配是什么意思,联合索引建立索引过程
在Mysql建立多列索引(联合索引)有最左前缀的原则,即最左优先。假设我们有两列a,b,a和b是联合索引,他的顺序是a,b,我们在where语句中调用a=? and b=?的时候就会走联合索引,如果调用where a = ?的时候也会走索引,但是当我们使用where b = ?的时候就不会走这个联合索引。
成因:mysql创建复合索引的规则是首先会对复合索引的最左边,也就是索引中的第一个字段进行排序,在第一个字段排序的基础上,在对索引上第二个字段进行排序,其实就像是实现类似order by 字段1,字段2这样的排序规则,那么第一个字段是绝对有序的,而第二个字段就是无序的了,因此一般情况下直接只用第二个字段判断是用不到索引的,这就是为什么mysql要强调联合索引最左匹配原则的原因。
独占所,共享锁,乐观锁讲一下
写锁是独占锁 ,在这个期间是不允许的任何线程来操作对象的。 读锁就是共享锁,可以使让其他线程的来读取的,但是不允许有修改。乐观锁是共享锁的一种,在通过乐观锁的时候获取对象的时候先比较一下乐观锁的版本号。如果版本号是正确的,那就可以获取对象。如果是版本不对的话。那就是不允许修改的。
mysql分库分表?(不太会,随便说了一下)
垂直拆分
垂直分库是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联
垂直拆分的优点是:
可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)
可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
数据维护简单
垂直拆分缺点是:
主键出现冗余,需要管理冗余列
会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join减少数据库压力
依然存在单表数据量过大的问题(需要水平拆分)
事务处理复杂
水平拆分
水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表 库内分表,仅仅是单纯的解决了单一表数据过大的问题,由于没有把表的数据分布到不同的机器上,因此对于减轻MySQL服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的IO、CPU、网络,这个就要通过分库来解决
水平拆分的优点是:
不存在单库大数据和高并发的性能瓶颈
应用端改造较少
提高了系统的稳定性和负载能力
缺点是:
分片事务一致性难以解决
跨节点Join性能差,逻辑复杂
数据多次扩展难度跟维护量极大
分片原则
能不分就不分,参考单表优化
分片数量尽量少,分片尽量均匀分布在多个数据结点上,因为一个查询SQL跨分片越多,则总体性能越差,虽然要好于所有数据在一个分片的结果,在必要的时候进行扩容,增加分片数量
分片规则需要慎重选择做好提前规划,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题,最近的分片策略为范围分片,枚举分片,一致性Hash分片,这几种分片都有利于扩容
尽量不要在一个事务中的SQL跨越多个分片,分布式事务一直是个不好处理的问题
查询条件尽量优化,尽量避免Select * 的方式,大量数据结果集下,会消耗大量带宽和CPU资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。
通过数据冗余和表分区赖降低跨库Join的可能。
这里特别强调一下分片规则的选择问题,如果某个表的数据有明显的时间特征,比如订单、交易记录等,则他们通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤,比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。
总体上来说,分片的选择是取决于最频繁的查询SQL的条件,因为不带任何Where语句的查询SQL,会遍历所有的分片,性能相对最差,因此这种SQL越多,对系统的影响越大,所以我们要尽量避免这种SQL的产生。
sql优化(不太会,只说了什么时候不会用到索引和慢查询)
1字段的优化
2查询的优化
3索引的优化
4读写分离
5分库分表的操作
6数据库集群的操作
NIO是什么?buffer底层说一下(不会)
线程和进程概念(共享哪些区域)
1.堆
几乎所有对象实例被分配到这里,也是垃圾收集器管理的主要区域。Java堆可以被分为新生代和老生代。进一步划分,则有Eden空间、From Survivor空间、To Survivor空间等。无论如何划分,都是为了更好地回收内存、更快的分配内存。
2. 方法区
方法区由于存储虚拟机加载的类的信息、常量、静态变量、JIT编译后的代码等。
虚拟内存讲一下(分页)
synchronized和Lock的区别
一个是通过指令集来实现锁住的对象的头来实现加锁的。
一个是通过设置一个标志位置来锁住独享的。
volatile的作用(锁的东西没怎么问)
JMM内存模型和缓存一致性协议还有就是的一个是保持可见性的
算法题:存储有[0,n)的数组,数组长度为len。只能交换数组里n和0的位置进行排序
sql题:查询每个班级分数前三的学生sql
/*查询学生表中姓名、学号,并以学号降序排序*/
select name,StuID from Students_information order by StuID desc /**order by 以什么排序,默认为升序,desc是降序*/
/*查询学生表中前5名学生的姓名,学号,并以学号升序排列*/
select top 5 name,StuID from Students_information order by StuID /*order by 默认为升序*/
二面:
项目问题10分钟,问到了Hash冲突
利用是数组+链表来解决hash冲突
synchronized底层实现(markWord,entrySet,waitSet)
通过锁住对象的头部来实现对对象加锁,synchronize的关键字在以前是使用的指令来实现的
他属于独占式的悲观锁,同时属于可重入锁。代码块同步是使用monitorenter和monitorexit指令实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。?通过一个标志位来判断 两个位置00表示的4种锁
AQS底层实现(非公平锁,公平锁)
底层采用的是双链表的来实现了的,公共锁的定义是所有的对象在获取锁的时候都是需要进入队列的。非公平锁是在对象获取锁的时候是采用是首先查看锁是否为空,如果是空的话,那就可以对获取,如果是有对象持有的话,那就进入队列进行排队。
Spring ICO,AOP介绍
SpringIOC是spring对提供了对类的全生命周期的管理的一种思想,利用反射机制来实现的对Bean的实例化产生和创建和销毁这样的机制。来实现对类对的属性的控制,这个过程中Bean实例和生命周期是SpringIOC中最重要的。Spring的Bean产生详细请见其他。
SpringAOP是一种切向编程的思想。在传统的过程时候由于是在传统的架构中都是垂直的流程体系。但是在这个过程中经常产生一些横向问题,比如log日志记录,权限验证,事务处理,性能检查的问题,为了遵循软件的开闭原则。就是对原来不修改进而扩展原累的方法和功能。SpringAOP就是实现了这样一种思想。通过对原方法和类在不修改代码的情况下而进行了类的增强的方式。
Spring用到了什么设计模式
工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
单例模式:Bean默认为单例模式。
代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
单例为什么加锁,volatile什么作用
单例模式中有一种是懒汉模式。这样的模式是会产生的线程安全的问题。volatile是让变量在多线程的情况下保持对其他线程的可见。
hashmap什么时候用到了红黑树
当链表的节点超过8个时候采用红黑树来实现存储。
介绍红黑树特点,为什么不用AVL树
红黑树属于平衡二叉树。它不严格是因为它不是严格控制左、右子树高度或节点数之差小于等于1,但红黑树高度依然是平均log(n),且最坏情况高度不会超过2log(n)。红黑树的插入效率比AVL的数要高。
红黑树不追求"完全平衡",即不像AVL那样要求节点的 |balFact| <= 1
,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多
算法题:一个链表:奇数序号升序,偶数序号降序,要求做这个链表的整体升序排序
private static ListNode[] splitList(ListNode head) {
ListNode cur = head;
ListNode head1 = null;
ListNode head2 = null;
ListNode cur1 = null;
ListNode cur2 = null;
int num = 1;
while (head != null) {
if (num % 2 == 1) {
if (cur1 != null) {
cur1.next = head;
cur1 = cur1.next;
} else {
cur1 = head;
head1 = cur1;
}
} else {
if (cur2 != null) {
cur2.next = head;
cur2 = cur2.next;
} else {
cur2 = head;
head2 = cur2;
}
}
head = head.next;
num++;
}
cur1.next = null;
cur2.next = null;
ListNode[] heads = new ListNode[]{head1, head2};
return heads;
}
private static ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
private static ListNode mergeLists(ListNode head1, ListNode head2) {
if (head1 == null && head2 == null) {
return null;
}
if (head1 == null || head2 == null) {
return head1 == null ? head2 : head1;
}
ListNode first = new ListNode(-1);
ListNode cur = first;
while (head1 != null && head2 != null) {
if (head1.val < head2.val) {
cur.next = head1;
head1 = head1.next;
} else {
cur.next = head2;
head2 = head2.next;
}
}
cur.next = head1 == null ? head2 : head1;
return first.next;
}
三面:
介绍了两个项目
看过阿里电商的项目结构吗?(没有,随便说了说我的项目怎么做的)
怎么解决超卖(答:redis + mysql乐观锁)
职业规划 + 想成为tech lead应该应该具备什么条件
现在有哪些offer
标签:ListNode,索引,美团,面试,任务,线程,20200424,分片,null From: https://blog.51cto.com/u_13643065/6169257