蚂蚁金服Java面试题(二)

Posted by Kaka Blog on November 20, 2020
  1. 了解过HashMap底层实现吗,描述下

key -hashCode()-> hashcode -hash()-> h -indexFor()-> 存储下标

HashMap由数组+链表组成的,主干是一个Entry数组,Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。get方法的实现相对简单,key(hashcode)–>hash–>indexFor–>最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。JDK1.8在JDK1.7的基础上针对增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

  1. HashMap线程安全吗?有什么方式进行解决,说下ConcurrentHashMap,CAS是什么,有什么问题,怎么解决

HashMap非线程安全。在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。ConcurrentHashMap 拥有更高的并发性。

CAS:Compare and Swap,即比较再交换。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是悲观锁。CAS是乐观锁。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

ABA问题是CAS机制中出现的一个问题,ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B,然后又重新变成A,另一个线程读取的时候,发现A没有变化,就误以为是原来的那个A,造成cas多次执行的问题。

  1. ArrayList和LinkedList有什么区别,相同数据下谁比较占用内存

一般情况下,LinkedList的占用空间更大,因为每个节点要维护指向前后地址的两个节点,但也不是绝对,如果刚好数据量超过ArrayList默认的临时值时,ArrayList占用的空间也是不小的,因为扩容的原因会浪费将近原来数组一半的容量,不过,因为ArrayList的数组变量是用transient关键字修饰的,如果集合本身需要做序列化操作的话,ArrayList这部分多余的空间不会被序列化。

  1. 多线程有用过吗?说下线程池每个参数及具体意义
  • 核心线程(corePool):线程池最终执行任务的角色肯定还是线程,同时我们也会限制线程的数量,所以我们可以这样理解核心线程,有新任务提交时,首先检查核心线程数,如果核心线程都在工作,而且数量也已经达到最大核心线程数,则不会继续新建核心线程,而会将任务放入等待队列。
  • 等待队列 (workQueue):等待队列用于存储当核心线程都在忙时,继续新增的任务,核心线程在执行完当前任务后,也会去等待队列拉取任务继续执行,这个队列一般是一个线程安全的阻塞队列,它的容量也可以由开发者根据业务来定制。
  • 非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。
  • 线程活动保持时间 (keepAliveTime):线程空闲下来之后,保持存活的持续时间,超过这个时间还没有任务执行,该工作线程结束。
  • 饱和策略 (RejectedExecutionHandler):当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize - 线程池核心池的大小。
  • maximumPoolSize - 线程池的最大线程数。
  • keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
  • unit - keepAliveTime 的时间单位。
  • workQueue - 用来储存等待执行任务的队列。
  • threadFactory - 线程工厂。
  • handler - 拒绝策略。
  1. 讲下线程池的工作流程

Executor接口定义了执行任务的execute(),实现流程还要看是哪个线程池实现的,这里用比较典型的ThreadPoolExecutor举例。ThreadPoolExecutor执行execute方法分下面4种情况:

如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤 需要获取全局锁)。 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。

  1. synchronized和Lock的区别,说下AQS

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

AQS(AbstractQueuedSynchronizer)就是抽象的队列式的同步器 AQS定义了一套多线程访问共享资源的同步器框架 许多同步类实现都依赖于它 AQS是一个Java提供的底层同步工具类 用一个int类型的变量表示同步状态 并提供了一系列的CAS操作来管理这个同步状态 AQS的主要作用是为Java中的并发同步组件提供统一的底层支持 如常用的ReentrantLock/Semaphore/CountDownLatch等等就是基于AQS实现的,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。

  1. JVM了解过吗?说下JVM内存模型

JVM内存空间分为五部分,分别是:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器。

  1. 类的生命周期说一下

java类的生命周期就是指一个class文件从加载到卸载的全过程。一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段。

  • 加载:找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
  • 连接:可以细分为三个步骤:验证、准备和解析。
  • 初始化:如果一个类被直接引用,就会触发类的初始化。按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。
  • 使用:包括主动引用和被动引用,主动引用会引起类的初始化,而被动引用不会引起类的初始化。(被动引用:通过子类引用父类的静态字段,不会导致子类初始化;通过数组定义类引用类,不会触发此类的初始化;常量在编译阶段会存入调用类的常量池中)
  • 卸载:类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

注意:

通常我们说类加载指的是类的生命周期中加载、连接、初始化三个阶段。 对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。

  1. GC了解过吗?说下哪些能被GC

垃圾检测通常通过建立根对象的集合,并且检查从这些根对象开始的可触及性来实现,如果正在运行的程序可以通过根对象访问到某个对象,那么这个对象就是可触及的,而无法被触及的对象被认为是垃圾。

GC Root的对象主要包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象
  1. GC垃圾算法有几个?说下了解的回收器对应的回收方式

算法:引用计数法、标记-清除算法、复制算法、标记-整理算法、分代收集算法。

垃圾回收器目前分为6种类型, 串行,并行,并发标记,G1。

1、Serial收集器(串行收集器):采用复制算法

2、ParNew收集器:采用复制算法

3、Parallel Scavenge收集器:采用复制算法

4、Serial Old收集器:标记-整理算法

5、Parallel Old收集器:标记-整理算法

6、CMS(Concurrent Mark Sweep)收集器:标记-清除算法

7、G1(Garbage-First)收集器:从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法

串行垃圾回收是最简单的也是效率最低的,如果只是控制台的单线程程序,简单任务,并且机器配置不高,推荐使用。

并行垃圾回收器是64bit server默认的垃圾回收器,一般我们工作和生产上默认不配置,都是并行垃圾回收。对于一般的不要求吞吐的应用,并且硬件资源不是太充足的情况下,并行垃圾回收器差不多能满足需求。

CMS垃圾回收器是对并行垃圾回收器的一个优化,它以CPU和系统资源为代价,换取GC的延迟。不会一GC就STW,而是根据情况STW。一定程度上是资源换取速度。

G1垃圾回收器是针对于大heap的垃圾回收器,如果heap分配的足够大,分的region的优先级回收策略会优先清理垃圾多的region.并且减少了内存空间碎片,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  1. 说下年轻代到老年代的过程

一般情况下,新创建的对象都会被分配到Eden区,(除非一些特别大的对象会直接放到老年代),当Eden没有足够的空间的时候,就会触发jvm发起一次Minor GC,如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中,对象在Survivor区中每熬过一次Minor GC,年龄就会增加一岁,当它的年龄增加到一定程度(15岁)时,就会被移到老年代中,当然晋升老年代的年龄是可以设置的。

  1. 是否每个对象年龄都要到达15才会进入老年代

不是

  1. 数据库的索引结构说一下,B树和B+树的区别

BTREE、HASH和FULLTEXT。

B树每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null。

B+树只有叶子节点存储data,叶子节点包含了这棵树的所有键值,叶子节点不存储指针。B+树上增加了顺序访问指针。

B+树相比B树的优势:

  1.单一节点存储更多的元素,使得查询的IO次数更少;   2.所有查询都要查找到叶子节点,查询性能稳定;   3.所有叶子节点形成有序链表,便于范围查询。

  1. 联合索引字段A,B,C查询A,C,B会走索引吗,为什么

走索引。where里面的条件顺序在查询之前会被mysql自动优化。

  1. 慢查询SQL怎么优化

  2. 说下数据库隔离级别,分别有什么问题

① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

② Repeatable read (可重复读):可避免脏读、不可重复读的发生。

③ Read committed (读已提交):可避免脏读的发生。

④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。

  1. 可重复读是怎么实现的

事务开始,第一次不加锁SELECT时,InnoDB从全局事务链表中,筛选所有活动事务(事务trx_id严格递增),生成当前一致性视图。根据当前一致性视图高低水位,计算事务可见性。根据可见事务redo log,逆向算出历史版本。SELECT快照读,读之前版本数据。

  1. 做过分库分表吗?基于什么进行拆分,拆分后出现了什么问题,怎么解决

  2. 情景题:有一个直播平台,每次有人进来,观看人数+1,怎么实现