面试实战第二期

介绍下我自己,我是王星星,2021届校招生,目前在阿里巴巴做Java开发。去年秋招的时候拿到了阿里,微信,拼多多,百度,美团,携程等互联网公司的offer。这个系列文章主要是通过总结牛客上的面经,自己梳理答案,然后分享出来,帮助广大实习和秋招同学稳拿offer,成功上岸!(本系列文章问题均得到牛客网友授权,更多面经答案请访问公众号:王星星的魔灯)

原文地址:https://www.nowcoder.com/discuss/699918

一面

一小时

  1. select 和 epoll的区别

    • select和epoll都是同步非阻塞模型的不同形式,同步非阻塞模型可以参考:https://wxxlamp.cn/2021/04/29/my-university-leaning/

    • select和epoll的区别在于:select中需要将用户态的accept数组拷贝到内核态的数组中,同时返回的只是可读fd的个数,性能依旧不高,所以在epoll中,它只是传入的accept数组中修改的部分而不是全部,同时通过异步事件唤醒, 内核仅会将有 IO 事件的文件描述符返回给用户,epoll大大提高了性能。

  2. NIO BIO AIO 分别谈谈

    • 这个问题是上个问题的延伸,所谓的BIO,NIO,和AIO,在Java生态中,同步阻塞IO,1.4引入的非阻塞IO和1.7引入的异步IO

    • 所谓的BIO,就是我们说的同步阻塞模型,当线程发起数据IO的时候,它会一直阻塞等待内核去读写数据,直至内核反馈结果后,该IO线程才会返回

    • 对于NIO来说,当线程发起数据IO的时候,它会一直不断轮询内核是否将数据准备完成,直至内核反馈结果后,该IO线程才会去做其他工作。NIO延伸出来的有select,poll,epoll

    • 对于AIO来说,当线程发起数据IO的时候,内核会立刻反馈给IO线程结果,然后IO线程就可以去做其他的事情,当内核真正在进程中准备好数据之后,会给IO线程发送IO成功的数据

  3. 自旋锁,CAS,有什么弊端 怎么解决

    • 线程加锁的时间一般都很短,所以下一个需要获得锁的线程等一下在阻塞,这个等一下的过程就叫自旋。这属于synchronized的优化。自旋锁的缺点很明显,就是不确定需要自旋多长时间,如果自旋时间过长的话,会导致CPU飙高,影响其他线程执行。解决方式也很简单,就是所谓的适应性自旋锁,加一个自旋时间即可

    • CAS是指compare and set,如果说sync的阻塞是悲观锁的话,那么cas就是乐观锁了,通过版本号的比较,如果版本号相同,则说明没有并发问题,如果版本号不同,则说明有其他线程参与,不能修改。cas的弊端很明显,就是会产生aba问题,解决方法也很简单,通过版本号机制,保证数据只会出现一次

  4. 异步IO、同步IO,什么是异步、同步

    • 所谓的同步IO,即是用户需要等到内核将设备(网络和硬盘)的数据全部拷贝到用户进程中之后才会给用户响应

    • 而异步IO,则是用户发起请求后,内核就会给用户响应

  5. 实习做什么的,实习部门做什么的,遇到什么困难,怎么解决

    • 这个是面试的经典问题,一般情况下要分一下几点:

    • 介绍实习部门背景,以及自己的工作,以及工作的结果:技术上和业务上

    • 在实习中遇到的困难:团队合作,代码熟悉,业务开发,技术bug等等

  6. 说设计模式使用的实际场景

  1. 链表k个一组反转

    class Solution {     // 如果不是额外常数空间,可以用O(n)空间新建k个链表     // 这个题需要常量空间,所以只能用标记     public ListNode reverseKGroup(ListNode head, int k) {         if (head == null || head.next == null) {             return head;         }         ListNode tail = head;         for (int i = 0; i < k; i++) {             //剩余数量小于k的话,则不需要反转。             if (tail == null) {                 return head;             }             tail = tail.next;         }         // 反转前 k 个元素         ListNode newHead = reverse(head, tail);         // 下一轮的开始的地方就是tail         head.next = reverseKGroup(tail, k);          return newHead;     }     /*     左闭又开区间      */      private ListNode reverse(ListNode head, ListNode tail) {         ListNode pre = null, next = null;         while (head != tail) {             next = head.next;             head.next = pre;             pre = head;             head = next;         }         return pre;     } }

二面

45分钟,因为算法简单

  1. AQS基本原理

    • 对于AQS来说,他是一个用来构建锁和同步器的框架,基于CAS,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器

    • 从使用上来说,AQS的功能可以分为两种:独占(如ReentrantLock)和共享(如Semaphore/CountDownLatch CyclicBarrier/ReadWriteLock)。ReentrantReadWriteLock可以看成是组合式,它对读共享,写独占

    • AQS维护一个共享资源state,它的语义有响应的子类来实现,譬如在ReentrantLock中,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()state=0(即释放锁)为止,其它线程才有机会获取该锁。在CountDownLatch中,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作

    • 当加锁时,子类通过调用AQS的acquire()进而调用子类自己实现的tryAcquire()方法(该方法是尝试获得锁,即改变state的状态),在调用tryAcquire()方法的过程中:先尝试获得锁,如果成功则返回,如果获得不成功则把该线程加入到Node队列中(节点为空时,自旋创建节点并设置尾点),然后自旋尝试获得锁(只有老二结点,才有机会去tryAcquire,如果不是且当其前驱节点是SINGAL,则阻塞),之后返回中断状态

    • 当释放锁时,和加锁流程一样。当释放成功后则去唤醒后面的节点

  2. 什么是可重入锁,用源码解释一下

    • 可重入锁即是说线程在获取锁A之后,调用到了另外一个需要锁A的方法,该线程是可以直接获得到该锁的

    • 对于ReentrantLock来说,该锁通过state关键字来记录锁重入的次数

    • ReentrantLock和synchronized都是可重入锁

  3. AQS的应用在哪里举例说明 怎么实现的

    • JUC中有线程池(Worker基于AQS),工具类(基于AQS的semaphore,countDownLock等),并发容器,atomic和locks(AQS,ReentrantLock),可以说,AQS是JUC的基础

    • 参考第一题

  4. 举例jdk里面用过哪些设计模式

    • 单例模式:java.lang.Runtime: 每个Java应用程序都有一个类运行时实例,该实例允许应用程序与运行应用程序的环境进行交互,很显然,只需要有一个Runtime实例即可,采用饿汉式

    • 装饰者模式:JavaIO包中的InputStream和OutputStream

    • 代理模式:基于JDK的动态代理

    • 观察者模式:java.util.Observer

    • 迭代器模式:集合中的Iterator

    • 模板方法模式:AbstractList和JDBCTemplate

  5. 你怎么看云原生,什么是云原生,为什么想做云原生

    • 所谓的云原生,是DevOps+持续交付+微服务+容器的集合

  6. kubernetes和docker了解多少说多少

    • k8s是调度器,docker是被k8s调度的容器

  7. netty做了什么优化 具体点

    • API优化,更利于使用,我们在编写代码的时候会有更适合业务的,更加清晰的语义

    • 通过ChannelHandler可以对网络进行更细节层的定制,譬如说协议解析等等

    • 性能方面,采用Reactor模型,具体可以参考这篇pdf:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf

    • 采用了不同于传统意义上的零拷贝:详情见第8题

  8. 零拷贝原理

    • netty的零拷贝除了节省内核缓冲区和用户缓冲区的拷贝次数,还在Java层做了一些处理

    • 直接在内存区域分配空间,而不是在堆内存中分配。如果使用传统的堆内存分配,当我们需要将数据通过socket发送的时候,就需要从堆内存拷贝到直接内存,然后再由直接内存拷贝到网卡接口层。 Netty提供的DirectBuffer,直接将数据分配到内存空间,从而避免了数据的拷贝,实现了零拷贝。

    • Netty通过Composite Buffers保存了数据的引用,而不是将多个数据通过拷贝的形式组装成大对象,实现了零拷贝。

  9. 写一个生产者消费者

    private static class Producer implements Runnable {      @Override     public void run() {         try {             empty.acquire();             mutex.acquire();             System.out.println("生产" + in);             in = (in + 1) % BUFFER;         } catch (InterruptedException e) {             e.printStackTrace();         } finally {             mutex.release();             full.release();         }     } } private static class Consumer implements Runnable {      @Override     public void run() {         try {             full.acquire();             mutex.acquire();             System.out.println("消费" + out);             out = (out + 1) % BUFFER;         } catch (InterruptedException e) {             e.printStackTrace();         } finally {             mutex.release();             empty.release();         }     } }

三面

1小时,估计是主管,人很好,但问的问题基本。。不知道。。。

  1. 什么是TCP连接 为什么TCP要连接

    • TCP协议是在运输层中有体现,它承接了网络层,同时开启了应用层。TCP通过端口号唯一标识一个连接

    • IP 层是不可靠的,它只负责数据包的发送,但它不保证数据包能够被接收、不保证网络包的按序交付、也不保证网络包中的数据的完整性。如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

  2. socket编程中,三次链接怎么进行的,从哪里拿到连接的,连接成功的标志是什么

    • 当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态

    • 服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;

    • 客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立

  3. 客户端挂了,那服务端能感知到TCP断了吗 不能怎么办,能的话是什么原理?

    • 理论上是不行的,不过目前很多实现均是可以感知到的

    • socket库提供了SO_KEEPALIVE功能,能够通过定时发消息来检测客户端是否挂掉

    • 也可以通过使用select()函数测试一个socket是否可读时,如果select()函数返回值为1,且使用recv()函数读取的数据长度为0 时,就说明该socket已经断开。

  4. MVCC原理

    • 数据库多版本并发控制,即每一行数据都是有多个版本的,每个版本有自己的row trx_id,即当时修改该行的transaction_id

    • 需要用到一致性读视图,即consistent read view,用于支持RC和RR隔离级别的实现,它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”,它其实是一个视图数组,和数据库中显式创建的create view ...不一样

    • 一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

      • 版本未提交,不可见;

      • 版本已提交,但是是在一致性视图创建后提交的,不可见;

      • 版本已提交,而且是在一致性视图创建前提交的,可见

    • 在MVCC中有两种读,上面三种是快照读,还有一种是当前读

      • 当普通的select是快照读

      • 插入,删除,更新属于当前读,需要加锁,遵从两阶段锁协议

  5. 分布式锁 业务服务器挂了 锁怎么释放?

    • 设置锁的存活时间

  6. 除了innodb你还知道什么引擎

    • MyISAM:

      • MyISAM中是不会产生死锁的,因为MyISAM总是一次性获得所需的全部锁,要么全部满足,要么全部等待

      • MyISAM因为是表锁,只有读读之间是并发的,写写之间和读写之间是串行的

      • MyISAM的二级索引和主索引结构没有区别,但是二级索引的key可以不唯一

    • Memory:支持hash索引

  7. 平时怎么调试代码?怎么实现断点的?跑着的线程怎 么打断点让它停下来

    • IDEA的断点功能可以选择all和thread模式,如果选择thread模式的话,就可以针对线程打断点了

  8. 删除倒数n个链表

class Solution {     public ListNode removeNthFromEnd(ListNode head, int n) {         ListNode p1 = head, p2 = head;         for(int i = 0; i < n; i++) {             p1 = p1.next;         }         if(p1 == null) {             return head.next;         }         while(p1.next != null){             p1 = p1.next;             p2 = p2.next;         }         p2.next = p2.next.next;         return head;      } }

#面经##阿里巴巴##校招##java工程师#
全部评论
沾沾喜气,许愿
点赞 回复
分享
发布于 2021-08-12 07:13

相关推荐

5 28 评论
分享
牛客网
牛客企业服务