2022金九银十Java面试基础天花板,肝完这份八股文,高薪资妥了(硬文 建议收藏)

2022金九银十要来了,很多小伙伴收获不错,拿到了心仪的offer。

1 java基础

1.1 基础

  1. 为什么String定义为final。

字符串常量池的要求,创建字符串时,如果该字符串已经存在于池中,则将返回现有字符串的引用,而不是创建新对象。多个String变量引用指向同一个内地地址,如果字符串是可变的,用一个引用更改字符串将导致其他引用的值错误。这是很危险的

  1. 说下枚举类型,底层实现原理,项目中是如何使用的。
  2. 详细描述Error和Exception(运行期和编译期)的区别。
  3. 深拷贝和浅拷贝区别
  4. 序列化和反序列化
  5. java内部类的区别(成员内部类、静态嵌套类、方法内部类、匿名内部类 )

1.2 HashMap

  1. HashMap中常用的方法有哪些,什么时候会触发树化
  2. jdk1.7和1.8实现的差异,1.7的实现为什么会出现死锁,画图说明下。
  3. HashMap和TreeMap的区别。

1.3 并发

  1. 创建线程的方式,线程的生命周期
  2. ThrealLocal实现原理,为什么会出现内存泄漏
  3. volatile关键字原理,项目中是如何使用的
  4. synchronized和lock的区别,底层实现原理
  5. AQS队列实现原理,用了哪些设计模式。公平锁和非公平锁、独占锁和共享锁、读写锁分别是如何实现的,为什么说非公平锁比公平锁性能高。
  6. java线程池参数描述,线程池工作原理,线程池如何调优
  7. 主线程到达一个条件,需要多个子线程去执行任务,等子任务都执行完后再往下走(CountDownLatch)
  8. 写个程序,两个线程交叉打印1到100的数字,需要多种实现方式
  9. >>篇幅较长 完整版传送门<<<----、

2 JVM

2.1 JVM运行时数据区域和内存模型描述,jdk8为什么移除方法区

jdk8移出方法区:

  • 对永久代的调优过程非常困难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动

  • 而在JDK8中,类的元数据保存在本地内存中,元空间的最大空间-XX:MaxMetaspaceSize,占用的是系统内存空间,可以避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存

2.2 四种引用区别

  • 强引用:正常new出来对象就是强引用,当内存不够的时候,JVM宁可抛出异常,也不会回收强引用对象。
  • 软引用(SoftReference):软引用生命周期比强引用低,在内存不够的时候,会进行回收软引用对象。软引用对象经常和引用队列ReferenceQueue一起使用,在软引用所引用的对象被GC回收后,会把该引用加入到引用队列中。
  • 弱引用(WeakReference):弱引用生命周期比软引用要短,在下一次GC的时候,扫描到它所管辖的区域存在这样的对象: 一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向,,不管当前内存是否够,该对象都会被回收。弱引用和软引用一样,也会经常和引用队列ReferenceQuene一起使用,在弱引用所引用的对象被GC回收后,会把该引用加入到引用队列中。
  • 虚引用(PhantomReference):又叫幻象引用,与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象变为不可达时,自己就被加入到引用队列,用作记录该引用指向的对象已被销毁。因此,无论对象是否覆盖了finalize方法,虚引用对象都没办法复活。虚引用已经加入了引用队列,自然就没办法将其放在F-Queue队列,无法再执行finalize方法。

2.3 GC

  1. 垃圾回收算法和垃圾回收器描述,在工作中,新生代和老年代分别用的什么垃圾回收器。

    略。

  2. 新生代和老年代什么时候会触发GC。

    • 新生代:Eden区内存不够的时候就会触发MinorGC
    • 老年代:
    • 新生代的对象晋身入老年代,导致空间不够用时触发
    • 大对象直接进入老年代时,老年代空间不够时触发

    老年代空间不够时抛出OOM

  3. CMS垃圾回收过程描述,CMS有哪些缺点,对比G1。

    CMS采用标记-清除算法进行垃圾回收,会产生内存碎片。

  4. GC调优步骤,有实操过吗。

    通过 jstat 等工具查看 GC 等相关状态

    查看对象生命周期的分布情况:如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大

2.4 描述下JVM类加载过程,如何自定义类加载器。

JVM类加载过程

加载 ----> 连接 -----> 初始化。

  • 加载:将.class文件中的二进制数据读到内存中的方法区,堆中生成一个class对象,指向方法区内的class数据
  • 连接:
    • 验证:二进制文件的正确性
    • 准备:为类的静态变量分配空间、默认值,为常量赋值
    • 解析:符号引用替换为指针
  • 初始化:为类的静态变量赋初始值

自定义类加载器

如果想要编写自己的类加载器,只需要两步:

  • 继承ClassLoader类
  • 覆盖findClass(String className)方法,在findClass方法中,调用父类CLassLoader的defineClass方法,向虚拟机提供字节码。

2.5 描述下双亲委派模型,为什么需要双亲委派模型。

略。

2.6 泛型是如何实现的。

Java的泛型只是编译期的泛型,一旦编译成字节码,泛型就被擦除了,和用Object强转的字节码是一样的。

2.7 逃逸分析

逃逸分析是JVM对对象逃逸的分析,有两种:

1、方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中; 2、线程逃逸:如类变量或实例变量,可能被其它线程访问到;

当JVM判断对象不会逃逸后,会进行以下优化:同步消除、标量替换和栈上分配。

同步消除

线程同步本身比较耗,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,则可以消除对该对象的同步锁,通过-XX:+EliminateLocks可以开启同步消除。

标量替换

1、标量是指不可分割的量,如java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量; 2、如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换; 3、如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是在栈上创建若干个成员变量; 通过-XX:+EliminateAllocations可以开启标量替换, -XX:+PrintEliminateAllocations查看标量替换情况。

栈上分配

故名思议就是在栈上分配对象,其实目前Hotspot并没有实现真正意义上的栈上分配,实际上是标量替换

2.8 OOM、内存泄漏如何排查,用到哪些工具,如果不用工具如何进行定位。

最常见的OOM错误,堆的内存占用已经达到-Xmx 设置的最大值,解决思路仍然是先从代码层面排查,怀疑存在内存泄露,通过jstack、jmap定位问题。如果说一切都正常,才需要通过调整Xmx的值来扩大内存。

  • 一般的,在启动参数中指定-XX:+HeapDumpOnOutOfMemoryError来自动保存 OOM 时的 dump 文件。
  • 也可以使用 JMAP来导出 dump 文件,然后使用jvisualvm打开dump文件,查看类的实例数量、类的实例内存占用比例来排查堆内存泄露,也可以查看堆转储上的线程。

jmap -dump:format=b,file=filename pid

  • 使用jmap -heap pid查看堆信息、GC状态,或者jmap -histo:live pid查看内存占用、类的实例数、大小。

jconsole是一个可以实时监控JVM的工具,和jvisualvm一样是jdk自带的工具。

内存泄露的原因主要有:文件流未正确关闭、静态的hashMap、List对象中的元素不会被回收,ByteBuffer缓存分配不合理。

2.9 机器负载变高如何排查

cpu负载变高:死循环、频繁GC 死循环

  • top命令查看cpu使用率较高的进程
  • 查看对应进程的所有线程:top -Hp pid -H 显示线程信息 -p 指定pid
  • 将jstack dump到文件中,便于查看。jstack pid/nid > myjstack.log

需要重点关注"java.lang.Thread.State",WAITING、TIMED_WAITING、BLOCKED,这些表示状态阻塞、等待的线程,一般的,死循环的线程处于RUNNABLE状态,而且堆栈信息会出现循环调用

GC

    S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
    2560.0 2560.0 385.7   0.0    4096.0   1078.1   11264.0     7476.1   14848.0 14366.9 1792.0 1679.0     12    0.057   0      0.000    0.057
    12

一次采样信息如上,含义为:

  • 对于S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU ,尾部字符C表示容量,U表示使用量。S0/S1为两个Survivor区,E表示Eden区,O表示老年代,M表示元数据区。
  • 对于YGC YGCT FGC FGCT GCT,尾部字符C表示GC的次数,T表示GC的耗时,YG表示Young GC,FGC表示Full GC,GCT表示所有GC的总耗时。

3 Spring框架

3.1 Spring框架用到了哪些设计模式

  • 代理模式:Spring AOP
  • 单例模式:Spring IOC容器创建的Bean 默认为单例模式
  • 工厂模式:BeanFactory 用来创建对象的实例
  • 观察者模式:如ApplicationListener、ContextEventListener
  • 包装器模式:以Wrapper为后缀的类采用包装器模式,动态的给bean添加额外的职责
  • 适配器模式:Adapter结尾的类采用适配器模式,使得原本不能兼容的接口能兼容。

3.2 Spring生命周期详细描述

spring启动时,先加载beanFactory,创建工厂类,然后创建bean,bean的生命周期主要为:实例化,属性填充,初始化,使用,销毁。

3.3 Spring是如何解决循环依赖的

构造注入在启动时报错,setter注入可以解决循环依赖。主要采用三级缓存+提前曝光的方案解决循环依赖,实例化一个bean的时候,首先递归的实例化其所依赖的所有bean,实例化后的bean会提前暴露至二级缓存earlySingletonObjects中,解决了循环依赖。

  • 第一级缓存:初始化完成的bean,已成功创建
  • 第二级缓存:bean实例化完成,尚未属性填充
  • 第三级缓存:singletonFactories,存放单例工厂bean
一级缓存:
/** 保存所有的singletonBean的实例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

二级缓存:
/** 保存所有早期创建的Bean对象,这个Bean还没有完成依赖注入 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
三级缓存:
/** singletonBean的生产工厂*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
 
/** 保存所有已经完成初始化的Bean的名字(name) */
private final Set<String> registeredSingletons = new LinkedHashSet<String>(64);
 
/** 标识指定name的Bean对象是否处于创建状态  这个状态非常重要 */
private final Set<String> singletonsCurrentlyInCreation =
    Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16));

3.4 Spring扩展点有哪些,项目中是如何应用的

容器启动时,执行refeshContext:

  • 加载beanFactory时,可以添加BeanFactoryPostProcessor,拦截beanFactory的创建。
  • 加载完beanFactory后,开始实例化bean,在实例化bean的前后,分别调用InstantiationAwareBeanPostProcessor的before和after方法。
  • 在bean初始化前后,调用BeanPostProcessor的before和after方法
  • 在容器初始化完成后,调用ContextEventListener。

3.5 Spring IOC、AOP描述

略。

3.6 Spring事务和MySQL事务的区别,Spring事务传播机制介绍,Spring事务失效和解决方案

3.7 Spring全局异常捕获如何编写

3.8 AOP动态代理实现:jdk动态代理和cglib实现差异,cglib性能为什么比jdk动态代理性能高,Fastclass机制描述下,哪些方法不能被动态代理

jdk与cglib:

  • JDK是基于反射机制,生成一个实现代理接口的匿名类,然后重写方法,实现方法的增强.它生成类的速度很快,但是运行时因为是基于反射,调用后续的类操作会很慢.而且他是只能针对接口编程的。

  • CGLIB是基于继承机制,继承被代理类,所以方法不要声明为final,然后重写父类方法达到增强了类的作用. 它底层是基于asm第三方框架,是对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理. 生成类的速度慢,但是后续执行类的操作时候很快。

faslclass机制:

jdk代理通过反射找到指定的方法,cglib通过fastclass机制直接调用方法,在创建代理类的时候为其中的方法建立hash索引,通过hash索引调用,提高了效率。

哪些方法不能被动态代理:

equals、hashcode、getClass。

3.9 AOP失效举例,为什么,如何解决

同一个类的内部调用会失效,因为AOP是基于接口和类实现的,内部调用不走代理。

解决:使用@Autowired注入当前类的代理对象,通过该对象调用方法。

3.10 BeanFactory和FactoryBean的区别

- BeanFactory是个bean 工厂,是一个工厂类(接口), 是spring用来管理和装配普通bean的ioc容器。ApplicationContext实现并扩展了该接口,ApplicationContext是扩展后的ioc容器。
- FactoryBean是个bean,在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式,是一个可以生产对象和装饰对象的工厂bean,由spring管理后,生产的对象是由getObject()方法决定的,主要用来实现复杂的bean的创建(比如连接工厂等),类似于@Bean配置。
复制代码

3.11 Spring创建了单例对象,如果多线程并发对属性赋值,造成相互覆盖的情况,如何处理

通过ThreadLocal去解决线程安全的方法,ThreadLocal为每个线程保存线程私有的数据。
复制代码

3.12 SpringMVC和SpringBoot的区别

Spring Boot实现了自动配置,降低了项目搭建的复杂度。
复制代码

3.13 说一下springboot的自动化配置

通过@EnableAutoConfiguration注解实现自动化配置,过程分为三步:

- 获取配置工厂类的全限定名,由EnableAutoConfigurationImportSelector扫描各组件jar的META-INF目录下的spring.factories文件中的配置工厂类(*Configuration.class)的全限定名。
- 配置工厂类的加载,在refreshContext时,SpringFactoriesLoader加载第一步扫描到的工厂类,实例化,将工厂配置bean置于ioc容器中。
- 在springContext环境中,使用beanFactory创建bean实例
复制代码

3.14 Springboot启动过程

Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean。

(1)创建一个SpringApplication对象实例,然后调用这个实例的run方法
(2)创建了应用的监听器SpringApplicationRunListeners,启动监听器
(3)创建并配置environment,environment主要负责读取spring的application.yml配置
(4)创建applicationContext
(5)prepareContext方法将listeners、environment、applicationArguments、banner等重要组件与上下文对象applicationContext关联
(6)refreshContext方法负责spring自动化配置,包括spring.factories的加载,beanFactory的创建,bean的实例化
(7)最后,运行springContext
复制代码

4 MySQL

4.1 事务ACID

事务是一组原子性的SQL语句,这组SQL语句要么全部执行,要么回滚失败

事务是必须满足4个条件(ACID):

  • 原子性(Atomicity): 事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
  • 一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏
  • 隔离性(Isolation):多个事务并发执行时相互隔离,事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
  • 持久性(Durability):事务处理结束后,对数据的修改是永久的

隔离性通过MVCC实现,而MVCC通过undolog实现,持久性通过redolog实现

4.2 事务隔离级别

MySQL有四种事务隔离级别:Read uncommited、Read commited、Repeatable read、Serializable,默认的是Repeatable read

4.2.1 MVCC机制

事务的隔离级别依赖于MVCC(Multi-Version Concurrency Control)实现,用于提高读操作的并发量。

原理:每个事务都会有一个事务ID,事务ID随时间自增,每行记录有两个隐藏列,维护两个版本号(事务ID),每一行记录可以存在多个版本(记录在undolog中),增删查改时围绕这两个版本号操作。

  • create_version:创建该行的事务版本号
  • delete_version:删除该行数据的事务版本号。

以RR级别为例,MVCC的读取、插入、删除、更新:

  • 读取:只会读取版本号比当前事务版本号小,删除版本号必须比当前事务版本号大的行记录。

  • 插入:生成一行新记录并记录当前事务的版本号至create_version。

  • 删除:将当前的系统版本号设置为这一行的delete_version。

  • 更新:拷贝旧记录,生成该行数据的新拷贝,将当前事务的版本号设置为新行的create_version、旧行的delete_version

在RR级别下,Mysql对该行在事务开始时的版本号做一个快照,以后只读取该快照。在RC隔离级别下,MySQL只读取最新的版本号。一个称为快照读、一个称为当前读。

MVCC的优点:支持无锁的并发读,提高并发量

MVCC的缺点:对于同一行记录需要维护多个版本,耗费更多空间

4.2.2 幻读与可重复读

Repeatable read隔离级别存在的问题是有可能会出现幻读。幻读与可重复读是完全不同的概念

数据库中存在一些记录,其中appId为testappid的数据有10条。

事务B查询appId为testappid的数据,发现不存在,事务B准备后续插入testappid的记录 事务A插入一条appId为testappid的数据 事务B执行插入appId为testappid的数据,失败,此时事务B再去执行查询,发现确实没有appId为testappid的数据,但就是插入不进去,这就是幻读。 为了解决幻读的问题,有以下两种方法:

  • 在读取数据的事务开启时,锁定整张表。这就是事务隔离的最高级别:Serializable,序列化。
  • 在Repeatable read级别下,添加共享锁或排他锁,innoDB引擎会主动加间隙锁,从而避免幻读。

所以,避免幻读更好的方法是第二种:RR+间隙锁

4.2.3 间隙锁是什么,如何避免?

间隙锁:锁定一个范围,但不包括记录本身,在>RR级别下生效,带锁操作时(带锁读、更新、删除),InnoDB会主动使用间隙锁,根据匹配条件的不同,锁的范围如下:

  • 匹配条件为范围,锁定该范围
  • 匹配条件为等值"==",匹配结果不为空,锁定被匹配的记录所在的开区间
  • 匹配条件为等值"==",匹配结果为空,锁定该记录左右两侧的开区间

索引B+树叶子节点的数据是顺序排列的,两个叶子之间的数据称为间隙,在RR级别下带锁读取时,Mysql将符合查询条件的间隙锁起来,避免别的事务在叶子之间插入新的数据破坏RR。

如何避免间隙锁:

  • 使用唯一索引,等值匹配时,Mysql不会再添加间隙锁
  • 隔离级别设置成RC
  • Mysql主动使用插入意向锁。对于插入操作,mysql尝试先加一把插入意向锁,它不会阻止其他事务的插入操作,提高了并发插入的效率。获取插入意向锁的前提是没有间隙锁,一旦有select ... for update、select ... lock in share mode、delete、update语句,插入意向锁就失效了。

4.3 left join和inner join的区别,嵌套子查询如何优化。

left join(左联接) 关键字会从左表 (table_name1) 那里返回所有的行,即使在右表 (table_name2) 中没有匹配的行。 right join(右联接) 关键字会右表 (table_name2) 那里返回所有的行,即使在左表 (table_name1) 中没有匹配的行。 inner join(等值连接) 只返回两个表中联结字段相等的行

嵌套子查询的效率低:执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,多了一个创建和销毁临时表的过程

嵌套子查询的优化方案:1、使用join,join比嵌套子查询更高效,数据量较大时,无需真正带入不同参数循环迭代 2、拆分为多个查询语句

4.4 慢查询如何定位和解决

  1. 临时开启慢查询日志set global slow_query_log=1

  2. 找到慢查询日志文件路径show variables like 'slow_query_log_file

  3. 使用msql提供的日志分析工具mysqldumpslow 分析找出查询时间最慢的十条sql:

    mysqldumpslow -s 10 /mysql/mysql01_slow.log

  4. 使用explain分析这10条sql,如:explain SQL_NO_CACHE select * from emp where name = 'Jefabc'

    SQL_NO_CACHE 指明了该查询不走缓存,避免了查询速度时高时低,影响判断。

possible_keys:表示查询时,可能使用的索引

key:表示实际使用的索引

rows:扫描出的行数(估算的行数)

Extra:执行情况的描述和说明

ps:使用explain一般看一看索引使用是否正确,尽量避免回表。

5. 根据prifile进一步分析,show profile for query id,可以清楚的看到该sql的所有执行阶段,如锁等待、执行、优化、发送数据、内存排序,在下图中可以看到发送数据耗时1.39s。慢查询主要原因是网络IO。

4.5 binlog与主从复制

主从复制用来实现读写分离,写主库,读从库,由binlog实现。

4.5.1 binlog格式

MySQL中的binlog是一个二进制文件,它记录了所有的增删改操作。节点之间的复制就是依靠binlog来完成的

binlog具有三种模式:

  • Row模式 日志中会记录成每一行数据被修改的日志,然后在slave端再对相同的数据进行修改
  • statement模式 记录每一条会修改数据的sql。slave在复制的时候sql Thread再次执行master执行过的sql
  • mixed模式 Mixed即混合模式,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。

4.5.2 主从复制流程

主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL,这样就可以保证自己跟主库的数据是一样的。

4.5.3 主从复制的延迟与数据丢失问题

  • 延时:从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。
  • 数据丢失:如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了

解决方案:半同步复制,用来解决主库数据丢失问题;并行复制,用来解决主从同步延时问题

半同步复制,主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。

并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

如果并发量仍然很高,就要考虑将一个主库拆分为多个主库了(分库分表),一般的,单个主库的写并发控制在2000/s。

4.6 分库分表。

一般的,单表到几百万条数据、单库的并发达到2000/s,就要考虑分库分表了,分库与分表是两回事,可能是光分库不分表,也可能是光分表不分库,也可能同时分库分表。只不过,在平时的设计中,分库分表是同时进行的。

4.6.1 垂直切分与水平切分

垂直切分:按照列拆分表,拆分后每个表都包含部分字段,一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,访问频率高的行字段越少,就可以在缓存里缓存更多的行。

水平切分:按照行拆分表,一个表的数据拆分到多张表中,比如单表3000W条数据,切分为3张表,每张表1000W条。

4.6.2 分库分表的方式

有些类似于MongoDB的集群分片策略。

  • 范围拆分:每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了
  • hash拆分:按照某个字段 hash 一下均匀分散,这个较为常用

优缺点:

范围拆分扩容更方便,比如按照时间范围,一个月一个库,只要预备好新库,后续的数据都会保存在新库中

hash拆分可以平均分配每个库的压力,坏处是扩容比较麻烦,数据迁移时需要rehash。

4.6.3 分库分表中间件

Sharding-jdbc

当当开源的,属于 client 层方案,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)

Mycat

属于 proxy 层方案,支持的功能非常完善

二者对比

Sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 Sharding-jdbc 的依赖;

Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

4.6.4 分库分表如何平滑过渡

有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?

双写迁移方案

双写:同时写老库和新库,除了对老库增删改,都加上对新库的增删改

部署系统以后,新库数据落后太多,使用导数据的SQL脚本,读老库写新库。写的时候要比较数据的时间戳,不允许用老数据覆盖新数据。

导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止

4.6.5 分库分表如何做到动态扩容缩容

一次性分32个虚拟逻辑库,每个库32张表,通过管理虚拟库与实际物理机之间的映射关系实现动态扩容缩容,剩下的就是数据迁移的问题了。参考redis的Hash Slot思想。

4.5.6 分库分表的主键id如何处理

其实就是分布式的主键id如何生成的问题,可以采用雪花算法(snowflake 算法)

4.7 索引

索引有哪些种类,建立索引的原则,聚簇索引和非聚簇索引实现区别,联合索引如何使用。

4.7.1 索引下推

假设有联合索引abc。有以下SQL语句:

select * from itemcenter where a like 'aa%' and b=22 and c = 20;
复制代码

根据左侧匹配规则,只能用到"a"检索数据,bc两个字段无法用到,在MySQL 5.6之前,只能一个个回表,到主键索引上找出数据行,再对比b、c字段值。

而MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

4.7.2 change buffer与唯一索引

change buffer:减少磁盘IO

更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB会将这些更新SQL缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作,通过这种方式就能保证这个数据逻辑的正确性。

ps:change buffer在内存中有拷贝,也会被写入到磁盘上

将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。除了访问这个数据页会触发merge外,系统有后台线程会定期merg

唯一索引不能使用change buffer:唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束,要判断表中是否存在这个数据,而这必须要将数据页读入内存才能判断,如果都已经读入到内存了,那直接更新内存会更快,就没必要使用change buffer了。

4.7.3 很长的字段,想做索引如何优化

因为存在一个磁盘占用的问题,索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。

方案是:hash,把字段hash为另外一个字段存起来,使用hash后的字段作为索引,每次校验hash就好了,hash的索引也不大

4.7.4 提高索引的效率还有哪些

  1. 提高区分度,比如身份证号,倒序存储在数据库中,邮箱账号,所有人都是www开头,可以截取后再存到数据库。

4.8 flush时宕机了,如何恢复

mysql写入数据的时候,是先把数据写到缓冲区,然后再flush到磁盘的,如果在flush过程中发生了宕机,数据如何恢复。

由WAL(Write-Ahead Logging)机制和redolog保证,事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,会持久化redo log。

在1-8的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。

若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘

1.start transaction;
2.记录 A=1 到undo log;
3.update A = 3;
4.记录 A=3 到redo log;
5.记录 B=2 到undo log;
6.update B = 4;
7.记录B = 4 到redo log;
8.将redo log刷新到磁盘
9.commit
复制代码

5 Redis

  1. redis数据类型,说下跳跃表是如何实现的,可以用什么数据结构替换。
  2. 删除过期key策略有哪些,内存淘汰策略有哪些,分别什么时候触发。
  3. redis线程模型和内存模型。
  4. redis持久化机制。
  5. redis集群方案。
  6. 让你设计一个redis,你会怎么做,有看过redis源码吗。
  7. 了解一致性hash算法吗,描述下。
  8. 用redis实现一个分布式锁。
  9. 缓存穿透、缓存击穿、缓存雪崩区别和解决方案。
  10. 布隆过滤器知道吗,说下原理。
  11. zset实现原理。跳表作用是什么,dict的作用呢?

5.1 你们的redis哨兵有几个节点?

3个,redis的哨兵一般选择奇数个,它需要投票决策,奇数个才能满足“大多数”的要求

5.2 redis和zookeeper分布式锁

线上用的RedLock作为分布式锁

RedLock

redisson.getLock()分布式锁的缺点:锁丢失。 在Redis的master节点上拿到了锁;但是这个加锁的key还没有同步到slave节点;master故障,发生故障转移,slave节点升级为master节点,导致锁丢失,Redis作者提出了一种更高级的分布式锁的实现方式:Redlock

RedLock代码如下,底层原理是:基于RedLock思想,遍历所有的Redis客户端,然后依次加锁,最后统计成功的次数看是否满足“大多数”来判断是否加锁成功

RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");

RLock redLock = anyRedisson.getRedLock(lock1, lock2, lock3);

// traditional lock method
redLock.lock();

// or acquire lock and automatically unlock it after 10 seconds
redLock.lock(10, TimeUnit.SECONDS);

// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       redLock.unlock();
   }
}
复制代码

zk分布式锁

依据临时顺序节点实现分布式锁:开源的框架有Curator。

N个客户端争抢一个zk分布式锁,原理都是类似的。

  • 大家都是上来直接创建一个锁节点下的一个接一个的临时顺序节点
  • 如果自己不是第一个节点,就对自己上一个节点加监听器
  • 只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。

临时顺序节点的另外一个用意就是,如果某个客户端创建临时节点之后,不小心自己宕机了也没关系,zk感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。

redis分布式锁和zk分布式锁的对比

  • redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能;zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。

  • 如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁。

总之,zk的分布式锁比redis的分布式锁牢靠、而且模型简单易用。

为什么没用ZK:历史遗留问题,之前开发中一直使用redis,更加熟悉,后来对比发现zk更适合做分布式锁,现在也在做迁移。

5.3 Redis的bigKey问题

big key即数据量大的 key,由于其数据大小远大于其他key,导致经过分片之后,某个具体存储这个big key的实例内存使用量远大于其他实例,造成,内存不足,拖累整个集群的使用。

redis中key和value的限制是512MB,如果一个value的大小超过了512MB,redis会报错,value过大会造成:内存不足、网络io过长、阻塞时间久。

解决方案:对 big key 存储的数据 (big value)进行拆分,变成value1,value2… valueN

例如:如果big value 是个大list,可以拆成将list拆成:list_1, list_2, list3, listN。

5.4 Redis的hotKey问题

首先要设置hotKey热点期间不过期,避免出现缓存击穿。

hotKey即热点 key,指的是在一段时间内,该key的访问量远远高于其他的 redis key, 导致大部分的访问流量在经过 proxy 分片之后,都集中访问到某一个 redis 实例上。hot key 通常在不同业务中,存储着不同的热点信息。比如

  • 新闻应用中的热点新闻内容;
  • 活动系统中某个用户疯狂参与的活动的活动配置;
  • 商城秒杀系统中,最吸引用户眼球,性价比最高的商品信息;

hotKey带来的问题是它只有一个key导致热点请求全部落到一个实例上。

解决方案:给hotKey加上前缀或者后缀,在集群中所有的节点上都保存hotKey,在查询的时候根据前缀或后缀进行hash路由,找到对应的实例

5.5 缓存雪崩、击穿、穿透

缓存雪崩:大量缓存的key同时失效,同时大批量的请求落到了数据库上,数据库扛不住挂了。(处理方法:失效时间加上一个随机值,避免同时失效)

缓存穿透:用户不断访问缓存和数据库都没有的数据。(处理方式:请求参数校验,将查询不到数据的key放到缓存中,value设为null)

缓存击穿:对于hotKey,刚好缓存失效,这时大量的请求就会落在数据库上(设置热点期间不过期)

6 Dubbo

  1. 描述一下rpc调用过程
  2. 让你实现一个rpc框架,你会怎么做。
  3. 链路跟踪和熔断机制了解吗,框架层如何实现的。
  4. 了解哪些序列化协议,有什么区别,项目中用的是什么协议。
  5. 说下Netty,bio、nio、aio区别,select、poll、epoll区别,什么是零拷贝机制。

7 MQ

7.1 批量处理消息

7.1 1.如何提高mq的吞吐量

批量发送和批量消费,如果我们要在一批发送的消息中对每条消息进行监听处理,可以通过在channel中添加监听器来实现。

channel.addConfirmListener(new ConfirmListener() {
  @Override
  public void handleNack(long deliveryTag, boolean multiple) throws IOException {
    System.out.println("nack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
  }

  @Override
  public void handleAck(long deliveryTag, boolean multiple) throws IOException {
    System.out.println("ack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
  }  
});
复制代码

当生产者收到Broker发送过来的ack消息时就会调用handleAck方法,收到nack时就会调用handleNack方法

7.1.2 rabbitmq如何控制流量的?

使用prefetch_count设置,表示消费者预取消息数量,当超过该值时,rabbitMQ不在给消费者推送消息,每发送一条消息,prefetch_count减去1,消费者每ACK一条消息prefetch_count加上1。

  1. Kafka、RabbitMQ、RocketMQ区别,为什么RabbitMQ时延最低,知道事务消息吗。
  2. Kafka生产者、消费者、协调者、服务端工作机制,描述数据从生产端到消费端到过程。
  3. 如果出现数据丢失或者数据重复消费如何处理。
  4. Kafka为什么高吞吐量。
  5. Kafka是如何实现exactly once语义的。
  6. 让你设计一个消息队列,你会怎么设计。

8 Zookeeper

  1. zookeeper节点类型、服务器角色,watch机制
  2. 描述下ZAB协议。
  3. 应用场景。
  4. 使用zookeeper实现分布式锁和读写锁。

9 Netty

9.1 介绍下NIO,详解select、poll、epoll区别

9.1.1 NIO

NIO即IO多路复用,底层基于select、poll、epoll、kqueue实现。把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求,与来一个请求创建一个线程相比,节省了系统资源。

select/epoll的好处在于单个线程可以同时处理多个网络连接的IO,它的基本原理就是select、poll、epoll会不断的轮询所负责的所有socket,其中任意一个socket有数据到达进入就绪状态了,就通知用户进程,用户进程再调用recvfrom操作,将数据从内核拷贝到用户进程(epoll使用的是零拷贝)

零拷贝:详见9.3.1,通过mmap 内存映射,用户空间可以共享内核空间的数据,减少内核空间 -> 用户空间内存拷贝。它解决了文件数据需要拷贝到JVM才能进行操作的窘境。而是直接在内核空间直接进行操作,省去了内核空间拷贝到用户空间这一步操作。JAVA中DirectByteBuffer就是这块内存的引用。

9.1.2 select、poll、epoll区别

epollselect/poll效率更高,有以下优点:

  • epoll支持的并发连接数更高select最大的缺陷是单个进程打开的socket_fd是有限制的,默认值是1024;而epoll没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024,与内存关系比较大,1GB内存的机器上大约是10w个句柄,具体的值可以通过cat /proc/sys/fs/file-max查看。
  • epoll的IO效率不会随FD数目的增加而线性下降select/poll每次调用都会线性的扫描全部的集合,导致效率线性下降,epoll通过接口回调实现,在每个socket_fd上添加一个callback接口,只有就绪的sokcet才会调用该callback,IO效率不会与FD数量成反比。
  • epoll使用mmap加速内核与用户空间的消息传递:无论是select、poll还是epoll都需要内核把FD消息通知到用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存实现的。

9.2 Netty的线程模型

注册accepter事件处理器到mainReactor线程池中,这样mainReactor会监听客户端向服务端发起的连接请求

当客户端向服务端发起连接请求时,mainReactor监听到了该请求将事件派发给acceptor事件处理器进行处理,可通过accept方法获得连接socketChannel,然后将socketChannel传递给subReactor线程池

subReactor线程池分配一个subReactor线程给这个SocketChannel,监听I/O的read、write操作,相关业务逻辑的处理交给工作线程池来完成

9.3 详细的说说DirectByteBuffer

9.3.1 DirectByteBuffer与JAVA零拷贝

Java中发起一个文件读操作时数据流顺序为:磁盘 ---> 内核态 ---> 堆外内存 ---> JVM内存

再说说DirectByteBuffer是如何优化io性能

其实本质是减少内存之间拷贝的次数,DirectByteBuffer分配的是堆外内存,少了一次堆外内存 ---> JVM内存的拷贝,这只是用户态的优化。

用户态与内核态之间的拷贝也被优化掉了,本质是通过mmap将用户态的逻辑地址与内核态的逻辑地址映射到同一个物理地址上,因此DirectByteBuffer优化掉了内核态 ---> 堆外内存的拷贝。

9.3.2 directByteBuffer指向的堆外内存何时被回收?

在DirectByteBuffer对象被JVM回收后,堆外内存就被回收了。

原理:创建DirectByteBuffer对象时,会为DirectByteBuffer添加一个虚引用:Cleaner对象,当DirectByteBuffer对象被回收时,JVM会将其加入引用队列,然后JVM从引用队列中判断DirectByteBuffer有一个虚引用为Cleaner,JVM会主动调用clean方法清理堆外内存。

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
复制代码

9.3.3 DirectByteBuffer堆外内存泄露

前面提到,DirectByteBuffer分配的直接内存只有在DirectByteBuffer对象被回收掉之后才有机会被回收。 如果DirectByteBuffer对象大部分都移到了老年代,但是一直没有触发Full GC,那么堆外物理内存有可能会被耗尽,因此为了避免这种情况的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,在创建DirectByteBuffer对象申请堆外内存的时候,会判断堆外内存是否达到了阈值,如果达到了,会显示调用System.gc触发一次full gc,回收老年代的DirectByteBuffer对象,以此来触发没有被使用的堆外内存的回收。

9.4 粘包和拆包

TCP是个"流"协议,所谓流,就是没有界限没有分割的一串数据。TCP会根据缓冲区的实际情况进行包划分,一个完整的包可能会拆分成多个包进行发送,也用可能把多个小包封装成一个大的数据包发送。这就是TCP粘包/拆包。

举个例子:假设操作系统已经接收到了三个包,如下:

由于流传输的这个普通属性,在读取他们的时候将会存在很大的几率,这些数据会被分段成下面的几部分: 

也就是读取的数据有可能超过一个完整的数据包或者过多或者过少的半包。因此,作为一个接收方,不管它是服务端还是客户端,都需要把接收到的数据整理成一个或多个有意义的并且能够被应用程序容易理解的数据。

Netty提供的拆包方案:

  • FixedLengthFrameDecoder:消息定长,固定报文长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
  • DelimiterBasedFrameDecoder:包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
  • LengthFieldBasedFrameDecoder:将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段

9.5 Bytebuf

为了减少频繁I/O操作,引进了Buffer的概念,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O 。Java NIO封装了ByteBuffer组件:

  • position:读写指针,代表当前读或写操作的位置,这个值总是小于等于limit的。
  • mark:标记position的恢复位置。
  • capacity:整块ByteBuffer占用内存大小。
  • limit:写的时候代表最大的可写位置,读的时候代表最大的可读位置。
    • 在写操作之前调用clear:将当前的capacity的值给limit
    • 在读操作之前调用flip:将当前的position的值给limit

ByteBuffer具有以下缺陷

  • ByteBuffer只有一个标识位控的指针position,读写的时候需要手工调用 flip() 和 clear() 等;
  • ByteBuffer的API功能有限,高级特性需要使用者自己实现。

Netty为了解决ByteBuffer的缺陷,重写了一个新的数据接口ByteBuf。 与ByteBuffer相比,ByteBuf提供了两个指针 readerIndex 和 writeIndex 来分别指向读的位置和写的位置,不需要每次为读写做准备,直接设置读写指针进行读写操作即可。

9.6 Netty的池化

无论是直接内存还是间接内存,netty频繁的网络io特性决定了它会高频申请-释放内存,为了提高效率,netty使用池化的内存管理,释放内存时,释放到内存池中,并非真正的底层释放,再申请内存时先从内存池中申请,减少了底层malloac、free等函数的调用。

10 HTTPS

10.1 SSL/TLS建立过程

第一步,客户端给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。

第二步,服务端确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。

第三步,客户端确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给服务端。

第四步,服务端使用自己的私钥,获取客户端发来的随机数(即Premaster secret)。

第五步,客户端服务端根据约定的加密方法,使用前面的三个随机数,生成"对话密钥"(session key),用来加密接下来的整个对话过程。

10.2 服务器公钥私钥的作用

握手之后的对话使用"对话密钥"加密(对称加密),服务器的公钥和私钥只用于加密和解密"对话密钥"(非对称加密),无其他作用。

10.3 握手的过程加密吗

不加密,握手的过程是明文传输的,客户端使用服务端给的证书加密第三个随机数,服务端使用私钥解密得到第三个随机数,然后根据协商好的加密算法计算会话密钥,后续的会话才是加密的。

10.4 优化https连接

1.OCSP Stapling

浏览器验证服务端证书有两种方式:CRL、OSCP

CRL 是由 CA 机构维护的一个列表,列表中包含已经被吊销的证书序列号和吊销时间。浏览器可以定期去下载这个列表用于校验证书是否已被吊销。缺点是实时性较差。

OCSP(Online Certificate Status Protocol,在线证书状态协议)OCSP 是一个在线证书查询接口,浏览器可以实时地向CA实时查询每一张证书的有效性,优点是实时性好,缺点是如果在SSL握手地过程中向CA请求证书的有效性,会在得到结果前会阻塞后续流程,这对性能影响很大,严重影响用户体验。

OCSP Stapling:由服务端定期的向CA发送OCSP请求,在客户端与服务端SSL握手时(第二步),由服务端将 OCSP 查询结果通过 Certificate Status 消息发送给浏览器,从而让浏览器跳过自己去验证的过程而直接拿到结果,OCSP 响应本身有了签名,无法伪造,所以 OCSP Stapling 既提高了效率也不会影响安全性。

2.客户端端口复用

一般来说,客户端一个端口释放后会等待2MSL 之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用

3.使用SESSION复用连接

握手阶段用来建立SSL连接。如果出于某种原因,对话中断,就需要重新握手。

这时有两种方法可以恢复原来的session:一种叫做session ID,另一种叫做session ticket。

session ID的思想很简单,就是每一次对话都有一个编号(session ID)。如果对话中断,下次重连的时候,只要客户端给出这个编号,且服务器有这个编号的记录,双方就可以重新使用已有的"对话密钥",而不必重新生成一把。

session ID是目前所有浏览器都支持的方法,但是它的缺点在于session ID往往只保留在一台服务器上。所以,如果客户端的请求发到另一台服务器,就无法恢复对话。session ticket就是为了解决这个问题而诞生的。

客户端不再发送session ID,而是发送一个服务器在上一次对话中发送过来的session ticket。这个session ticket是加密的,只有服务器才能解密,其中包括本次对话的主要信息,比如对话密钥和加密方法。当服务器收到session ticket以后,解密后就不必重新生成对话密钥了。

11 微服务

  1. 微服务网关
  2. 服务治理
  3. 接口的壮硕性

12 算法编程

  1. 无重复字符的最长子串
  2. 二叉树的直径
  3. 二叉树最大宽度
  4. 寻找旋转排序数组中的最小值
  5. 旋转链表
  6. LRU缓存机制
  7. 数据流的中位数
  8. 搜索旋转排序数组

12.1 二叉树的非递归遍历

非递归需要借助栈实现,递归的本质也是栈。前序遍历最简单,后序遍历只需在前序遍历的基础上稍作改动。

12.1.1 前序遍历

  • 从栈中弹出当前节点,保存在list中
  • 将当前节点的右节点入栈、左节点入栈(先右后左,出栈时先左后右)
public List<Integer> orderTraversal(TreeNode root) {
  List<Integer> list = new ArrayList<>();
  if(root == null) return list;

  Stack<TreeNode> stack = new Stack<>();
  stack.push(root);

  while(!stack.isEmpty()) {
    TreeNode cur = stack.pop();
    if(cur.right != null) stack.push(cur.right);
    if(cur.left != null) stack.push(cur.left);
    list.add(cur.val);
  }

  return list;
}
复制代码

12.1.2 后序遍历

前序遍历是根-左-右,后序遍历是左-右-根

因此,实现后序遍历,可以在前序遍历的基础上进行改进:按照根-右-左进行前序遍历,然后每次在list头部插入,这样得到的就是左-右-根

list.add(0, 根); // [根]

list.add(0, 右); // [右,根]

list.add(0, 左); // [左,右,根]

public List<Integer> postorderTraversal(TreeNode root) {
  // 省略代码,与前序遍历一致
  
  while(!stack.isEmpty()) {
    TreeNode cur = stack.pop();
    // 因为是按照 根-右-左 进行前序遍历,所以要将先把左节点入栈
    if(cur.left != null) stack.push(cur.left);
    if(cur.right != null) stack.push(cur.right);
    // list头部插入
    list.add(0, cur.val);
  }

  return list;
}
复制代码

12.1.3 中序遍历

中序遍历就是在栈中保存当前节点,一直向左遍历,直至没有左节点,然后从栈中弹出一个节点,保存在list,然后指向右节点

public List<Integer> inorderTraversal(TreeNode root) {
    Stack<TreeNode> stack = new Stack<>();
    List<Integer> list = new ArrayList<>();
    if(root == null) return list;
    
    TreeNode cur = root;
    while(!stack.isEmpty() || cur != null) {
        while(cur != null) {
          stack.push(cur);
          cur = cur.left;
        }

        cur = stack.pop();
        list.add(cur.val);
        cur = cur.right;
    }

    return list;
}
复制代码

14 设计模式

  1. 单例模式:多种实现方式,double check实现原理,枚举类实现(枚举类为什么不能被反射)
  2. 模版方法设计模式:工程中的应用
  3. 静态代理和动态代理设计模式
  4. 装饰器模式
  5. 适配器模式
  6. 工程方法模式
  7. 责任链模式


 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值