Java大厂面试题合集---自我总结

文章目录

Java基础

1.ArrayList和LinkedList 和ArrayList的扩容方式

  • ArrayList的底层实现为数组存储在内存中,线程不同步。可通过数组下标的形式进行查找,所以在查询方面的效率较为出色,常用在查询较多的情景下。每次扩容1.5倍
  • LinkedList的底层实现为链表形式,也为线程不同步。而链表的底层也决定了它在查询方面不如数组底层的ArrayList而在指定位置插入等修改操作下,性能优于ArrayList。无扩容机制

2.StringBuffer和StringBuilder的区别

  • 线程安全: StringBuffer:线程安全,StringBuilder:线程不安全。因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有 synchronized 修饰。
  • 缓冲区:StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。
    StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
    所以, StringBuffer 对缓存区优化,不过 StringBuffer 的这个toString 方法仍然是同步的。
  • 性能:既然 StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder 的性能要远大于 StringBuffer。

3. ConcurrentHashMap和HashMap的区别,怎么解决线程不安全?有做什么优化么

  • ConcurrentHashMap线程安全,HashMap线程不安全。ConcurrentHashMap是使用了锁分段技术来保证线程安全的。jdk1.8HashMap采用了数组+红黑树方式

4. HashMap怎么解决哈希冲突和哈希碰撞

  • 用的是拉链法,数组j+链表,jdk1.8进化成了数组+链表->数组+红黑树

5. Double和float为什么不能互相转义

  • 精度不同,一个32位,一个64位

6. 序列化

  • 序列化是为了保持对象在内存中的状态,并且可以把保存的对象状态再读出来。
    1、什么时候需要用到序列化?
    a、数据持久化:比如一个电商平台,有数万个用户并发访问的时候会产生数万个session 对象,这个时候内存的压力是很大的。我们可以把session对象序列化到硬盘中,需要时在反序列化,减少内存压力。
    b、网络传输:我们将系统拆分成多个服务之后,服务之间传输对象,不管是何种类型的数据,都必须要转成二进制流来传输,接受方收到后再转为数据对象。

7. Java的锁,那些类用了乐观锁 ,那些用了悲观锁

  • 悲观锁:
    即排他锁,假设冲突总会存在,即每次拿数据的时候都认为别人会修改,所以每次拿数据都会加锁.比如synchronize
  • 乐观锁:
    假设每次取拿数据的时候,都没有别人在操作,所以不会上锁.但是在更新的时候会判断下再此期间有没有没人去更新过这个数据.常用的有版本号控制/CAS等等.
    乐观锁一般多用于读这种场景,可以提高吞吐量.例如concurrent.atomic包下面的原子变量就是使用了乐观锁的一种基于CAS实现.
    需要一提的是,乐观锁适合竞争冲突不频繁的场景,否则性能还不如使用synchronize。

8. 什么是AQS

  • AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
  • AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

9. Synchronized和ReentrantLock的区别

  • 这两种方式最大的区别就是对于synchronized来说,它是Java语言关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock他是jdk1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句来完成

10. Synchronized和volatile的区别

  • volatile是变量修饰符,其修饰的变量具有可见性(可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到物理内存,当有其他线程需要读取时,可以立即获取修改之后的值)。volatile修饰符的变量则是直接读写物理内存。
    volatile可以禁止进行指令重排,保证有序性。程序执行到volatile变量的读操作或者写操作时,在其前面的语句中,更改操作肯定已经完成,且结果已经对后面的操作可见。
    synchronized则作用于一段代码或方法,使用了该修饰符既可以保证可见性(通过synchronized和Lock也能够保证可见性)也能够保证原子性(原子性表现在要么不执行,要么执行到底)
  • volatile,它能够使变量在值发生改变时能尽快地让其他线程知道
  • synchronized是Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。

11. Synchronized的锁升级策略

  • 所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级
  • 当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
    如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

双亲委派机制、为什么要有?

  • 当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
  • 1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
    2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

循环依赖处理的方式

  • 循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A
  • 三级缓存解决了Bean之间的循环依赖
  • singletonObjects:第一级缓存,里面放置的是实例化好的单例对象;
    earlySingletonObjects:第二级缓存,里面存放的是提前曝光的单例对象;
    singletonFactories:第三级缓存,里面存放的是要被实例化的对象的对象工厂
  • 所以当一个Bean调用构造函数进行实例化后,即使属性还未填充,就可以通过三级缓存向外暴露依赖的引用值(所以循环依赖问题的解决也是基于Java的引用传递),这也说明了另外一点,基于构造函数的注入,如果有循环依赖,Spring是不能够解决的。还要说明一点,Spring默认的Bean Scope是单例的,而三级缓存中都包含singleton,可见是对于单例Bean之间的循环依赖的解决,Spring是通过三级缓存来实现的

MVC处理请求流程,适配器返回ModelAndView可以为NULL吗,为什么可以直接返回JSON给前端,怎么处理的在这里插入图片描述

  • ModelAndView可以为空,源码写的
  • 使用注解@ResponseBody可以转换为json
  • 如果拿到的是JSON字符串,需先解析成JSON对象(eval函数)
    已是JSON对象,遍历并获取JSON对象的属性值和子数组

IOC、AOP

动态代理,一个接口的两个方法都实现了动态代理,调用效果是什么(方法A中调用方法B,方法A中调用的方法B没有增强的效果),为什么会这样(面试时有点懵,没记起来),怎么解决(不会)

IO、NIO、AIO

  • BIO同步阻塞式IO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成
  • NIO(reactor模型,同步非阻塞式IO):线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成
  • AIO(proactor模型,异步非阻塞式IO):线程发起IO请求,立即返回;内存做好IO操作的准备之后,做IO操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做IO操作完成或者失败

多线程

1.初始化线程池的参数

  • int corePoolSize => 该线程池中核心线程数最大值
    int maximumPoolSize 该线程池中线程总数最大值
    long keepAliveTime 该线程池中非核心线程闲置超时时长
    TimeUnit unit keepAliveTime的单位
    BlockingQueue workQueue 该线程池中的任务队列:维护着等待执行的Runnable对象
    ThreadFactory threadFactory 创建线程的方式,这是一个接口,你new他的时候需要实现他的Thread newThread(Runnable r)方法,
    RejectedExecutionHandler handler 抛出异常专用的

2.线程池中可以容纳的最大任务数

  • .net4.0,32位机器最大线程数,bai每核1023个
    .net4.0,64位机器最大线程数,每核32768个

3.如何创建一个线程

  • 第一种是继承自 Thread 类,第二种是实现 Runnable 接口,第三种是实现 Callable口。相比第一种,推荐第二种方式,因为继承自 Thread 类往往不符合里氏代换原则,而实现 unnable 接口可以使编程更加灵活

4. 如何终止一个线程

  • interrupt()这种方式并不是很能及时的停止线程
  • stop()但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果

5.如何让三个线程顺序执行

  • thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B
  • CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。

6. 消息队列

  • “消息队列”是在消息的传输过程中保存消息的容器。
  • 使用场景:三个场景也是消息队列的经典场景,大家基本上要烂熟于心那种,就是一说到消息队列你脑子就要想到异步、削峰、解耦

JVM

1. 是否了解JVM指令

2.类的加载机制,生命周期

  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分可统称为连接(Linking),如图所示
    在这里插入图片描述

3.缓存

  • 缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。由于缓存的数据比内存快的多,所以缓存的作用就是帮助硬件更快的运行。

Spring

1.动态代理,实现方式,(接口实现类实现,CGLIB实现),之间的差异

  • Java动态代理指的是在程序运行时,为已有对象生成代理对象,对原有对象的方法进行功能增强
  • 实现方式:1、JDK实现动态代理:主要使用了Proxy和InvocationHandler两个类。Proxy.newProxyInstance()方法,该方法的官方解释为:返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。2、CGLIB实现动态代理(没接口)使用JDK的Proxy实现动态代理,要求目标类与代理类实现相同的接口,若目标类不存在接口,则无法使用该方式实现。对于没有接口的类,要为其创建动态代理,就要使用CGLIB来实现。CGLIB动态代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。使用CGLIB生成代理类,要求目标类必须能被继承,因此不能是final类。

2.生成的代理类访问的效率是否一样,为什么CGLIB实现更快

  • CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败),在创建代理这一块没有JDK动态代理快,但是运行速度比JDK动态代理要快。
  • 其实jdk1.8时,jdk实现已经不慢了

3. AOP

SQL

1.数据库的索引

  • 是帮助MySql高效获取数据的数据结构。可以理解为排好序的数据结构

2.为什么选择BTree数据结构来创建索引,为什么用B+树会比较快

  • 原因是B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。
    B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作
  • B+树更适合外部存储。由于内结点不存放真正的数据(只是存放其子树的最大或最小的关键字,作为索引),一个结点可以存储更多的关键字,每个结点能索引的范围更大更精确,也意味着B+树单次磁盘IO的信息量大于B树,I/O的次数相对减少。
    MySQL是一种关系型数据库,区间访问是常见的一种情况,B+树叶结点增加的链指针,加强了区间访问性,可使用在区间查询的场景;而使用B树则无法进行区间查找

3. MySQL的默认隔离级别,会产生问题么

  • 共有四个隔离级别:读未提交,读提交,可重复读,串行化(事务一个一个进行)
  • mysql默认是可重复读

4. 最左匹配原则

  • 最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。

5. 联合索引

  • 两个或更多个列上的索引被称作联合索引,联合索引又叫复合索引。建了索引后,索引会自动把这些列进行排序,建了索引再查询时,速度会有显著提升。要注意符合最左匹配原则,explain+SQL可查看是否使用了索引以及使用情况。

Mybatis

1. #{ } ${ }的区别

  • #{}表示一个占位符号
    通过#{}可以实现 preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换,
    #{}可以有效防止 sql 注入。 #{}可以接收简单类型值或 pojo 属性值。 如果 parameterType 传输单个简单类型值,#{}括号中可以是 value 或其它名称。

  • $ { }表示拼接 sql 串通过${}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换, 可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,{}括号中只能是 value。

  • #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #{user_id},如果传入的值是 name , 那么解析成sql时的值为order by “name”, 如果传入的值是id,则解析成的sql为order by “id”.

  • $ 将传入的数据直接显示生成在sql中。如:order by ${user_id},如果传入的值是name, 那么解析成sql时的值为order by name, 如果传入的值是id,则解析成的sql为order by id.

  • 综上所述,$ {}方式会引发SQL注入的问题、同时也会影响SQL语句的预编译,所以从安全性和性能的角度出发,能使用#{}的情况下就不要使用 ${}。

2. #{ }解决了什么问题

  • sql注入

猜你喜欢

转载自blog.csdn.net/weixin_40485391/article/details/107424640
今日推荐