服务端开发之Java备战秋招面试7

努力了那么多年,回头一望,几乎全是漫长的挫折和煎熬。对于大多数人的一生来说,顺风顺水只是偶尔,挫折、不堪、焦虑和迷茫才是主旋律。我们登上并非我们所选择的舞台,演出并非我们所选择的剧本。继续加油吧!

目录

1、用两个栈实现一个队列

2、两个链表的第一个公共节点

3.Try catch 和 finally return执行顺序?

4.Nginx负载均衡?

5.Redis中几种数据结构?如何实现消息队列?

6.讲一下Jvm的内存模型?jvm原理?jvm调优?

7.CAS和synchronize有什么区别?应用场景对应哪些?

8.线程的几种状态?线程可以手动设置等待嘛?

9.Spingboot启动做那些事?

10.单例Bean和多例Bean有什么区别?

11.Synchronized的实现原理,锁升级机制?

12.Redis为什么执行效率快?

13.ReentrantLock如何实现可重入?

14.解决线程安全要使用那些容器?

15.treemap底层是有什么实现的?


1、用两个栈实现一个队列

题目链接:用两个栈实现队列_牛客题霸_牛客网

import java.util.Stack;

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
       stack1.push(node) ;
    }
    
    public int pop() {
        if(stack2.isEmpty()){
        while(!stack1.isEmpty()){
            stack2.push(stack1.pop()) ;
        }
        }
        return stack2.pop() ;
    }
}

2、两个链表的第一个公共节点

题目链接:两个链表的第一个公共结点_牛客题霸_牛客网

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        int len1 = 0, len2 = 0 ;
        ListNode cur1 = pHead1 ;
        ListNode cur2 = pHead2 ;
        while(pHead1 != null){
            len1 ++ ;
            pHead1 = pHead1.next ;
        }
        while(pHead2 != null){
            len2 ++ ;
            pHead2 =  pHead2.next ;
        }
        int len = len1 <= len2 ? len1 : len2 ;
        int d = len1 <= len2 ? len2 - len1 : len1 - len2 ;
        for(int i=0; i<d; i++){
            if(len1 >= len2){
                cur1 = cur1.next ;
            }else{
                cur2 = cur2.next ;
            }
        }
        for(int i=0; i<len; i++){
            if(cur1 == cur2){
                return cur1;
            }
            cur1 = cur1.next ;
            cur2 = cur2.next ;
        }
        return null ;
    }
}

3.Try catch 和 finally return执行顺序?

1、finally中的代码总会被执行。
2、当try、catch中有return时,也会执行finally,但要注意return的返回值类型,是否受到finally中代码的影响。
3、finally中有return时,会直接在finally中退出,导致try、catch中的return失效。

4.Nginx负载均衡?

负载均衡是将请求分配到不同服务单元,既可以是同一台服务器的不同进程,也可以是不同服务器,这样既保证服务的高可用性,又保证高并发情况下得响应速度,能够给用户更好的体验。

常用的负载均衡的策略:

1)基于轮询的负载均衡(默认)

每个请求,按时间顺序逐一分配到不同的后端应用服务器节点,如果后端服务出现故障,nginx能够自动剔除该节点。

2)基于权重(weight)的负载均衡

权重(weight)默认值为1,权重越高,被分配的请求数量越多

3)基于IP HASH的负载均衡

每个请求,按照访问IP的hash结果分配,由于hash值为不重复的唯一值,因此每个请求能够固定访问同一个后端服务器,这样可以做到会话保持,解决session同步问题。

4)基于fair方式

根据后端服务器的响应时间来分配请求,响应时间短的节点被优先分配。
 

5.Redis中几种数据结构?如何实现消息队列?

它支持数据结构,如 字符串,散列,列表,集合,带有范围查询的排序集(sorted sets),位图(bitmaps),超级日志(hyperloglogs),具有半径查询和流的地理空间索引。

MQ应用有很多,比如ActiveMQ,RabbitMQ,Kafka等,但是也可以基于redis来实现,可以降低系统的维护成本和实现复杂度,本篇介绍redis中实现消息队列的几种方案。

1)基于List的 基于异步消息队列 的实现

使用rpushlpush操作入队列,lpoprpop操作出队列。

List支持多个生产者和消费者并发进出消息,每个消费者拿到都是不同的列表元素。

但是当队列为空时,lpop和rpop会一直空轮训,消耗资源;所以引入阻塞读blpop和brpop(b代表blocking),阻塞读在队列没有数据的时候进入休眠状态,一旦数据到来则立刻醒过来,消息延迟几乎为零。

2) PUB/SUB,订阅/发布模式

SUBSCRIBE,用于订阅信道;PUBLISH,向信道发送消息;UNSUBSCRIBE,取消订阅

此模式允许生产者只生产一次消息,由中间件负责将消息复制到多个消息队列,每个消息队列由对应的消费组消费。

3) 基于Sorted-Set的实现

Sortes Set(有序列表),类似于java的SortedSet和HashMap的结合体,一方面它是一个set,保证内部value的唯一性,另一方面它可以给每个value赋予一个score,代表这个value的排序权重。内部实现是“跳跃表”。

有序集合的方案是在自己确定消息ID时比较常用,使用集合成员的Score来作为消息ID,保证顺序,还可以保证消息ID的单调递增。通常可以使用时间戳+序号的方案。确保了消息ID的单调递增,利用SortedSet的依据Score排序的特征,就可以制作一个有序的消息队列了。

4) 基于Stream类型的实现

可以参考这篇博文:Redis实现消息队列的4种方案_redis消息队列实现_YHJ的博客-CSDN博客

6.讲一下Jvm的内存模型?jvm原理?jvm调优?

JVM内存模型运行时数据区,用于存储在JVM运行过程中产生的数据,包括:程序计数器、本地方法栈、虚拟机堆、线程栈、方法区(元空间)。

程序计数器:是用于存放下一条指令所在单元的地址。

本地方法栈: 运行本地方法的空间,也就是native本地方法运行时的一块空间。

堆内存:堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收;

线程栈(虚拟机栈):JVM的每一个线程对应一个线程栈,一个线程的每个方法会分配一块栈帧内存空间。栈帧中包含:局部变量表、操作数栈、动态链接和方法出口。

元空间:存储在物理硬盘,主要包括常量、静态变量、类信息、运行时常量池,操作的是直接内存。

jvm原理:

概念:JVM把文件.class字节码加载到内存,对数据进行校验,转换和解析,并初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制

类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。

JVM提供了四种类加载器:

1)启动类加载器(Bootstrap ClassLoader)-----c++编写,加载java核心库 java.*

2)扩展类加载器(Extension ClassLoader)-----java编写,加载扩展库

3) 应用程序类加载器(Application ClassLoader)---------java编写,加载程序所在的目录

4)自定义类加载器(User ClassLoader)------------用户自己定义的类加载器。

类装载器的内部机制-双亲委派机制

原理:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

双亲委派机制的优点:
1.保证安全性
防止重复加载同一个class。通过委托去向上面问,加载过了就不用再加载一遍。保证数据安全。
2.保证唯一性
保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
注:如果不采用双亲委派机制:多个类加载器都去加载类到内存中,系统中将会出现多个不同的类,那么类之间的比较结果及类的唯一性将无法保证。

jvm调优:

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC (对整个堆进行回收) 的次数。

1.监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。

2.生成堆的dump文件

通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

3.分析dump文件

打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件:

  • Visual VM
  • IBM HeapAnalyzer
  • JDK 自带的Hprof工具
  • Mat(Eclipse专门的静态内存分析工具)推荐使用

4.分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。

注:如果满足下面的指标,则一般不需要进行GC:

  • Minor GC执行时间不到50ms;
  • Minor GC执行不频繁,约10秒一次;
  • Full GC执行时间不到1s;
  • Full GC执行频率不算频繁,不低于10分钟1次;

5.调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

6.不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。

JVM调优参数参考
1.针对JVM堆的设置,一般可以限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;

2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。

3.年轻代和年老代设置多大才算合理,在抉择时应该根 据以下两点:

(1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2

(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。

4.在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法.

5.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。

7.CAS和synchronize有什么区别?应用场景对应哪些?

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少)
synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

1.对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2.对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
 

8.线程的几种状态?线程可以手动设置等待嘛?

新建状态、就绪状态、运行状态、阻塞状态、终止状态。

可以,等待wait()和睡眠sleep()
 

9.Spingboot启动做那些事?

1、执行main方法,new了一个SpringApplication对象,使用SPI技术加载加载 ApplicationContextInitializer、ApplicationListener 接口实例。

2、调用SpringApplication.run() 方法。

3、调用createApplicationContext()方法创建上下文对象,创建上下文对象同时会注册spring的核心组件类(ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等)。

4、调用refreshContext() 方法启动Spring容器和内置的Servlet容器。
 

10.单例Bean和多例Bean有什么区别?

在Spring中,bean可以被定义为两种模式:prototype(多例)和singleton(单例)
singleton(单例):只有一个共享的实例存在,所有对这个bean的请求都会返回这个唯一的实例。
prototype(多例):对这个bean的每次请求都会创建一个新的bean实例,类似于new。
Spring bean默认是单例模式。在springBoot项目中如果要配置单例或者多例,可以在对应的bean上加一个@scope()注解

11.Synchronized的实现原理,锁升级机制?

Synchronized的实现是基于进入和退出Monitor对象来实现方法或代码块的同步。在字节码层面是通过成对的MonitorEnter和MonitorExit指令来实现。同步块来说,MonitorEnter指令插入在同步代码块的开始位置,而monitorExit指令则插入在方法结束和抛出异常的地方,JVM保证MonitorEnter必须有对应的MonitorExit。当代码执行到MonitorEnter时,会尝试获取该对象Monitor的所有权,即尝试获得对象锁。

线程竞争锁失败需要进行cpu上文切换进入阻塞状态的锁机制,被称为重量级锁。

为了提高Synchronized的执行效率,Jvm设计了Synchronized的锁升级机制。既是根据需要修改锁状态,逐渐升级锁的类型。锁升级的过程依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁升级过程是不可逆的,一旦升级则不会再发生回退。    
 1)偏向锁,既是它会偏向于第一个访问锁的线程。在程序运行中,只有一个线程访问同步锁,不存在多线程争用的情况。则线程不需要触发同步,直接给该线程加一个偏向锁。

2)轻量级锁,当发生锁的竞争时,偏向锁就会升级为轻量级锁。轻量级锁是自旋锁实现。Synchronized升级轻量级锁的设计设计的思想是:如果持有锁的线程能够在很短时间内释放锁,则等待的线程可以不作上下文切换,只是进行数次自旋等一等,持有锁的线程释放锁后即可立即获取锁。因此避免上下文切换的开销(上面提到上下文切换比执行CPU指令的开销要大的多)。
3)重量级锁,线程自旋是需要消耗CPU的,线程不能一直占用CPU做自旋动作。因此,需要设定一个自旋等待的最大时间。当持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,则竞争锁的线程会停止自旋进入阻塞状态,此时升级到重量级锁。

可以参考这篇博文:Synchronized实现原理与锁升级机制_锁的升级机制_风行水上_ZH的博客-CSDN博客

12.Redis为什么执行效率快?

(1)完全基于内存k-v操作,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销。

(2)数据结构简单,对数据操作也简单。Redis中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis正是依赖这些灵活的数据结构,来提升读取和写入的性能。

(3)采用单线程,省去了很多上下文切换的时间以及CPU消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。

(4)使用基于IO多路复用机制的线程模型,可以处理并发的链接。Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。为了解决非阻塞IO不断轮询导致CPU占用升高的问题,出现了IO复用模型。IO复用中,使用其他线程帮助去检查多个线程数据的完成情况,提高效率。

13.ReentrantLock如何实现可重入?

通过Sync类实现,继承AQS(AbstractQueuedSynchronizer),在AQS维护了一个state计算重入次数。

假设: A、B两个线程同时执行lock()方法获取锁,假设A先执行获取到锁,此时state值加1,如果线程A在继续执行的过程中又执行了lock()方法(根据持有锁的线程是否是当前线程,判断是否可重入,可重入state值加1),线程A会直接获取锁,同时state值加1,state的值可以简单理解为线程A执行lock()方法的次数;当线程B执行lock()方法获取锁时,会将线程B封装成Node节点,并将其插入到同步等待队列的尾部,然后阻塞当前线程,等待被唤醒再次尝试获取锁;线程A每次执行unlock()方法都会将state值减1,直到state的值等于零则表示完全释放掉了线程A持有的锁,此时将从同步等待队列的头节点开始唤醒阻塞的线程,阻塞线程恢复执行,再次尝试获取锁。ReentrantLock公平锁的实现使用了AQS的同步等待队列和state。

14.解决线程安全要使用那些容器?

同步容器类:使用了synchronized
1.Vector
2.HashTable

并发容器:
3.ConcurrentHashMap:分段
4.CopyOnWriteArrayList:写时复制
5.CopyOnWriteArraySet:写时复制

Queue:
6.ConcurrentLinkedQueue:是使用非阻塞的方式实现的基于链接节点的无界的线程安全队列,性能非常好。
(java.util.concurrent.BlockingQueue 接口代表了线程安全的队列。)
7.ArrayBlockingQueue:基于数组的有界阻塞队列
8.LinkedBlockingQueue:基于链表的有界阻塞队列。
9.PriorityBlockingQueue:支持优先级的无界阻塞队列,即该阻塞队列中的元素可自动排序。默认情况下,元素采取自然升序排列
10.DelayQueue:一种延时获取元素的无界阻塞队列。
11.SynchronousQueue:不存储元素的阻塞队列。每个put操作必须等待一个take操作,否则不能继续添加元素。内部其实没有任何一个元素,容量是0

Deque:
(Deque接口定义了双向队列。双向队列允许在队列头和尾部进行入队出队操作。)
12.ArrayDeque:基于数组的双向非阻塞队列。
13.LinkedBlockingDeque:基于链表的双向阻塞队列。

Sorted容器:
14.ConcurrentSkipListMap:是TreeMap的线程安全版本
15.ConcurrentSkipListSet:是TreeSet的线程安全版本
 

15.treemap底层是有什么实现的?

TreeMap是一个通过红黑树实现有序的key-value集合,也就是说是一棵自平衡的排序二叉树,这样就可以保证快速检索指定节点。

TreeMap本质是Red-Black Tree,Entry中key比较大小是根据比较器comparator来进行判断的。

猜你喜欢

转载自blog.csdn.net/nuist_NJUPT/article/details/129285975
今日推荐