Java 基础面试题

David 2023-06-01 00:22:04
Categories: Tags:

ArrayList 和 LinkedList 的区别有哪些?

  1. ArrayList
    1. 优点 :ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询 操作效率会比较高(在内存里是连着放的)
    2. 缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低
  2. LinkedList
    1. 优点 :LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等 一个连续的地址。对于新增和删除操作,LinkedList 比较占优势。 LinkedList 适用于要头尾操 作或插入指定位置的场景
    2. 缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低

深复制和浅复制

  1. 浅拷贝只是增加了一个指针指向已存在的内存地址
  2. 深拷贝是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存
  3. 使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误

HashMap的长度为什么是2的N次

  1. 为了存取效率高,减少hash值的碰撞
  2. 尽量将数据均匀分配,每个链表或者红黑树的长度相等

HashMap和ConcurrentHashMap的区别

HashMap ConcurrentHashMap
key-value存储形式 key-value存储形式
线程不安全 线程安全
底层数组 + 链表 + 红黑 底层Node + CAS + Synchronized
默认数组大小16 默认数组大小16

红黑树的特点

  1. 根节点是黑色
  2. 如果叶子是红,其子节点为黑
  3. 路径平衡:从任意节点到其所有叶子节点的简单路径上的黑色节点数量必须相同。
  4. 不会出现任何路径比其他路径长出两倍。

为什么推荐使用 ThreadPoolExecutor 创建线程池

  1. 创建线程池的方法有两种:
  1. FixedThreadPool 和 SingleThreadPool:允许任务队列 workQueue 的长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  2. CachedThreadPool 和 ScheduledThreadPool:允许线程池最大数量 maximumPoolSize 为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。

说白了就是:使用有界队列,控制线程创建数量。除了避免 OOM 的原因之外,不推荐使用Executors提供的两种快捷的线程池的原因还有:

  1. 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
  2. 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。

Netty的FastThreadLocal是什么?

既然 Java 中有了 ThreadLocal 类了,为什么 Netty 还自己创建了一个叫做 FastThreadLocal 的结构?
问题就出在 ThreadLocalMap 类上,它虽然叫 Map,但却没有实现 Map 的接口。ThreadLocalMap 在 rehash的时候,并没有采用类似 HashMap 的数组+链表+红黑树的做法,它只使用了一个数组,使用开放寻址(遇到冲突,依次查找,直到空闲位置)的方法,这种方式是非常低效的。
由于 Netty 对 ThreadLocal 的使用非常频繁,Netty 对它进行了专项的优化。它之所以快,是因为在底层数据结构上做了文章,使用常量下标对元素进行定位,而不是使用 JDK 默认的探测性算法。

ThreadLocalMap为什么要定义在ThreadLocal中,而不直接定义在Thread中?

将ThreadLocalMap定义在Thread类内部看起来更符合逻辑,但是ThreadLocalMap并不需要Thread对象来操作,所以定义在Thread类内只会增加一些不必要的开销。定义在ThreadLocal类中的原因是ThreadLocal类负责ThreadLocalMap的创建,仅当线程中设置第一个ThreadLocal时,才为当前线程创建ThreadLocalMap,之后所有其他ThreadLocal变量将使用一个ThreadLocalMap。
总结,ThreadLocalMap不是必需品,定义在Thread中增加了成本,定义在ThreadLocal中按需创建。

说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
· 普通同步方法,锁是当前实例对象
· 静态同步方法,锁是当前类的class对象
· 同步方法块,锁是括号里面的对象

synchronized 和 volatile 的区别是什么?

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

synchronized 和 Lock 有什么区别?

首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

JVM的如何优化

目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。

JVM配置方面

一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。

  1. Xms(程序初始化的时候内存栈的大小)和-Xmx(应用程序使用的最大内存数)的值设置成相等,堆大小默认为-Xms指定的大小,默认空闲堆内存小于40%时,JVM会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,JVM会减小堆到-Xms指定的大小。如果在Full GC后满足不了内存需求会动态调整,这个阶段比较耗费资源,不会频繁使用gc。
  2. 新生代尽量设置大一些,让对象在新生代存活时间长一点,每次Minor GC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生Full GC的频率。

    代码实现方面

  3. 避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC
  4. 避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。
  5. 当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空
  6. 尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。
  7. 避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。