牛客网阿里后端一面面经学习答案整理

面经链接:https://www.nowcoder.com/discuss/842756
来源:牛客网

HashMap的实现(以及一系列追问)

HashMap是我们非常常用的数据结构,由数组和链表组合构成的数据结构。
而数组里面每个地方都存了Key-Value这样的实例,在JDK1.7叫Entry,在Java1.8中叫Node。

HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为 null。
HashMap 也是非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。

CurrentHashMap的实现

⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock ,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤于存储键值对数据。

在1.8中,
ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红⿊⼆叉树。Java 8 在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红⿊树(寻址时间复杂度为 O(log(N)))
synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍。

volatile是怎么实现的

volatile 主要有两个作用:

  1. 保证了变量的可见性
  2. 禁止JVM的指令重排,保证了有序性,在多线程环境下也能正常运⾏。

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

详见:volatile 作用及其实现原理
Java并发编程:volatile关键字解析

TCP、UDP

UDP 在传送数据之前不需要先建⽴连接,远地主机在收到 UDP 报⽂后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是⼀种最有效的⼯作⽅式(⼀般⽤于即时通信),⽐如: QQ 语⾳、 QQ 视频 、直播等等。

而TCP 提供⾯向连接的服务。在传送数据之前必须先建⽴连接,数据传送结束后要释放连接。TCP 不提供⼴播或多播服务。由于 TCP 要提供可靠的,⾯向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握⼿来建⽴连接,⽽且在数据传递时,有确认、窗⼝、重传、拥塞控制机制,在数据传完后,还会断开连接⽤来节约系统资源),这⼀难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的⾸部增⼤很多,还要占⽤许多处理机资源。TCP ⼀般⽤于⽂件传输、发送和接收邮件、远程登录等场景。

三次握手

三次握⼿的目的是建⽴可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握⼿最主要的目的就是双⽅确认自己与对方的发送与接收是正常的。

  • 第⼀次握⼿:客户端 什么都不能确认;服务器确认了对⽅发送正常,自己接收正常
  • 第⼆次握⼿:客户端 确认了自己发送、接收正常,对方发送、接收正常;
    服务器确认了:对方发送正常,自己接收正常
  • 第三次握⼿:客户端确认了:自己发送、接收正常,对⽅发送、接收正常;
    服务器确认了:自己发送、接收正常,对⽅发送、接收正常。

所以三次握⼿就能确认双发收发功能都正常,缺⼀不可。

JVM内存结构

详见:Java虚拟机详解02----JVM内存结构

描述垃圾回收算法

垃圾回收算法我了解到的有四种,标记-清除算法、复制算法、标记-整理算法和分代收集算法。

标记-清除算法

该算法分为标记、清除两个阶段。
首先标记出所以不需要回收的对象,再统一回收掉所有未被标记的对象。
它是最基础的收集算法,后续的算法都是对其不足改进得到的。这种算法有两个明显问题:
效率问题和空间问题(标记清除后会产生大量不连续碎片)

复制算法

解决了刚刚的效率问题。将内存分为大小相同的两块区域,每次使用一块,当这块内存使用完后,将还存活的对象复制到另一块后,将这一块使用过的空间一次清理掉。这样的话,每次回收都是对内存区间的一半进行回收。

标记-整理算法

根据老年代特点提出的。
先标记,之后让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。

分代收集算法

是当前普遍采用的算法。它根据对象存活周期的不同将内存分为几块。根据各个年代的特点选择合适的垃圾收集算法。

列举进程间通信方式

  • 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

详见:https://blog.csdn.net/zhaohong_bo/article/details/89552188

列举线程同步的方式

  • 互斥量 Mutex:互斥量是内核对象,只有拥有互斥对象的线程才有访问互斥资源的权限。因为互斥对象只有一个,所以可以保证互斥资源不会被多个线程同时访问;当前拥有互斥对象的线程处理完任务后必须将互斥对象交出,以便其他线程访问该资源;
  • 信号量 Semaphore:信号量是内核对象,它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。信号量对象保存了最大资源计数当前可用资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就减1,只要当前可用资源计数大于0,就可以发出信号量信号,如果为0,则将线程放入一个队列中等待。线程处理完共享资源后,应在离开的同时通过ReleaseSemaphore函数将当前可用资源数加1。如果信号量的取值只能为0或1,那么信号量就成为了互斥量;
  • 事件 Event:允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。事件分为手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒一个等待中的线程,然后自动恢复为未激发状态。
  • 临界区 Critical Section:任意时刻只允许一个线程对临界资源进行访问。拥有临界区对象的线程可以访问该临界资源,其它试图访问该资源的线程将被挂起,直到临界区对象被释放。

I/O多路复用

IO多路复用(IO Multiplexing)是指单个进程/线程就可以同时处理多个IO请求。

实现原理:用户将想要监视的文件描述符(File Descriptor)添加到select/poll/epoll函数中,由内核监视,函数阻塞。一旦有文件描述符就绪(读就绪或写就绪),或者超时(设置timeout),函数就会返回,然后该进程可以进行相应的读/写操作。

wait和sleep方法区别

wait和sleep都可以暂停线程的执⾏,两者最主要的区别在于: sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁
wait() 通常被用于线程间交互/通信, sleep() 通常被用于暂停执⾏。
wait() ⽅法被调用后,线程不会自动苏醒,需要别的线程调用同⼀个对象上的 notify() 或者 notifyAll() 方法。 sleep() ⽅法执⾏完成后,线程会自动苏醒。或者可以使⽤ wait(long timeout) 超时后线程会自动苏醒。

IOC和AOP是如何实现的

IoC

IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 IoC 容器是Spring ⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如何被创建出来的。利⽤ IoC 的话,只需要配置好,然后在需要的地⽅引⽤就⾏了,这⼤⼤增加了项⽬的可维护性且降低了开发难度。

AOP

AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring动态代理的实现方式

  • JDK动态代理: 代理的类必须实现一个接口
    JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口,核心是InvocationHandler接口和Proxy类
  • CGLIB动态代理: 动态生成被代理类的子类
    CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的

描述Spring Bean的生命周期

  • Bean 容器找到配置⽂件中 Spring Bean 的定义。
  • Bean 容器利⽤ Java Reflection API 创建⼀个Bean的实例。
  • 如果涉及到⼀些属性值 利⽤ set() ⽅法设置⼀些属性值。
  • 如果 Bean 实现了 BeanNameAware 接⼝,调⽤ setBeanName() ⽅法,传⼊Bean的名字。
  • 如果 Bean 实现了 BeanClassLoaderAware 接⼝,调⽤ setBeanClassLoader() ⽅法,传⼊ClassLoader 对象的实例。
  • 如果实现了其他 *.Aware 接⼝,就调⽤相应的⽅法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执⾏ postProcessBeforeInitialization() ⽅法
  • 如果Bean实现了 InitializingBean 接⼝,执⾏ afterPropertiesSet() ⽅法。
  • 如果 Bean 在配置⽂件中的定义包含 init-method 属性,执⾏指定的⽅法。
  • 如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执⾏ postProcessAfterInitialization() ⽅法
  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接⼝,执⾏ destroy() ⽅法。
  • 当要销毁 Bean 的时候,如果 Bean 在配置⽂件中的定义包含 destroy-method 属性,执⾏指定的⽅法。
    在这里插入图片描述

详见:https://www.cnblogs.com/zrtqsk/p/3735273.html

MySQL索引、为什么不用哈希索引

详见:https://www.cnblogs.com/agilestyle/p/14419263.html

MySQL分库分表

详见:https://blog.csdn.net/wdcl2468/article/details/102911160

MySQL主从复制

主从复制(Replication)是指数据可以从一个MySQL数据库主服务器复制到一个或多个从服务器,从服务器可以复制主服务器中的所有数据库或者特定的数据库,或者特定的表。默认采用异步模式。

实现原理:

  • 主服务器 binary log dump 线程:将主服务器中的数据更改(增删改)日志写入 Binary log 中;
  • 从服务器 I/O 线程:负责从主服务器读取binary log,并写入本地的 Relay log;
  • 从服务器 SQL 线程:负责读取 Relay log,解析出主服务器已经执行的数据更改,并在从服务器中重新执行(Replay),保证主从数据的一致性

主从复制可以实现

  • 读写分离:主服务器负责写,从服务器负责读
    • 缓解了锁的争用,即使主服务器中加了锁,依然可以进行读操作;
    • 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销;
    • 增加冗余,提高可用性
  • 数据实时备份,当系统中某个节点发生故障时,可以方便的故障切换
  • 降低单个服务器磁盘I/O访问的频率,提高单个机器的I/O性能

MySQL事务持久化机制

详见:https://www.cnblogs.com/jamaler/p/12174517.html

Redis主从复制

主从连接过程:

  1. 从服务器连接主服务器,发送SYNC命令。主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令。
  2. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
  3. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
  4. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
  5. 一旦主机挂了,从机会原地待命,但是使用 salveof no one 命令会使从机反仆为主

**作用:**数据冗余、故障恢复、负载均衡、高可用的基石。使用slave of 命令将某一台redis变为从机。

Redis持久化

  1. RDB:开启一个新的线程来完成往rdb文件中的写操作。主线程继续处理命令。使用单独的子线程来进行持久化。主线程不进行任何的IO操作。保证redis的高性能。缺点是可能会丢失一些数据。
  2. AOF :AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。AOF有一个重写模式,当日志文件过大时可以对其进行压缩。AOF往往效率低于RDB一些。

AOF的追写策略:建议使用每秒同步一次(everysec)策略。

rewrite机制:rewrite会记录上次重写时AOF文件的大小,当AOF文件是上一次大小的二倍且大于64M时触发。

Redis分布式锁

详见:https://blog.csdn.net/my_daidai/article/details/107232107

猜你喜欢

转载自blog.csdn.net/qq_45884783/article/details/123221749