Java 2022(Java基础篇)最新面试题和答案

Java基础

Java8种基本数据类型

类型 数据类型和占用字节
整数型 byte(1字节) short(2字节) int(4字节) long(8字节)
浮点型 float (4字节) double(8字节)
字符类型 char(3字节)
布尔型 boolean(1字节)

ArrayList和LinkedList的区别

  • ArrayList 是用数组来实现的,LinkedList 是基于链表实现的。
  • 数组和链表的最大区别就是数组是可以随机访问,而链表只能从头开始遍历。
  • 改查 ArrayList 的效率高
  • 增删在尾部时并且数据量大时 ArrayList 效率高,在其他地方LinkedList效率高。

数组因为在物理内存上的连续性,当要增删元素时,会导致后续元素都要移动一位,所以效率比较低,而链表则可以轻松的断开和下一个元素的连接,直接插入新元素或者移除旧元素效率较高,但增删在尾部的时候,不需要移动后续元素,所以 ArrayList 比较快。

单向链表和双向链表的区别

单向链表
组成:链接方向是单向的,链表的遍历要从头部开始顺序读取;结点构成,head指针指向第一个成为表头结点,终止于最后一个指向NULL的指针。
优点:单向链表增加删除节点简单,遍历时候不会死循环。
缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。

双向链表
组成:双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
优点:可进可退,能找到前驱和后继。
缺点:占用内存较大,需要多分配一个前驱指针,遍历复杂。

双向循环链表
组成:双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个循环,LinkedList就是双向循环列表组成的。
优点:从循环链表中的任何一个结点出发都能找到任何其他结点。

HashMap

底层实现

JDK 1.7
存储结构:数组+链表
插入方式:头插法
扩容方式:先扩容,再添加
扰动函数:要进行4次位运算 + 5次异或运算

JDK 1.8
存储结构:数组+链表+红黑树
插入方法:尾插法
扩容方式:先添加,再扩容
扰动函数:进行1次位运算 + 1次异或运算

引入红黑树原因

为了解决hash碰撞后,链表过长引起的查询效率问题。

红黑树转化条件

数组的长度大于64,链表长度大于8才会从链表转换为红黑树。

当数组长度小于64时,数组+链表比红黑树占用空间较小,而且红黑树自平衡比较耗时,hash碰撞严重时不如先扩容来解决冲突。

线程安全问题
  • JDK1.7:因为采用头插法,所以当并发执行扩容操作时环形链表的情况。
  • JDK1.8:采用尾插法解决了1.7的环形链表问题,但是当并发执行put操作时会发生数据丢失等情况。
负载因子
  • 负载因子:0.75
  • 扩容机制:0.75 * 16=12,当容量达到了12的时就会执行扩容操作。
  • 原因:根据泊松分布,当负载因子为0.75时,空间利用率比较高,hash碰撞的频率较少,属于时间和空间平衡的最佳大小。
put操作顺序

1.先判断数组是否为空,为空则调用resize()进行扩容,直接插入到对应的数组中。
2.不为空则根据当前key来计算数据的索引值判断当前key是否存在,如果存在,则覆盖value值。
3.不存在则判断是否是红黑树,是红黑树结构则直接插入。
4.不是红黑树的话,遍历链表准备插入,若链表长度小于8且数组长度小于64时,直接插入链表,数组的长度大于64,链表长度大于8的时候,则将链表转为红黑树,转成功之后,再插入。
5.判断++size是否大于threshold,大于则进行一次resize();

ConcurrentHashMap

底层实现

1.7
存储结构: Segment数组 + HashEntry数组 + 链表

线程安全实现:Segment继承了ReentrantLock,用来做分段锁,理论上如果有 n 个 Segment,那么最多可以同时支持 n 个线程的并发访问,从而大大提高了并发访问的效率。

分段锁:ConcurrentHashMap由多个Segment组成(Segment下包含很多Node),每个Segment都有一把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

1.8
存储结构: Node数组 + 链表/红黑树

线程安全实现:取消了分段锁的概念,采用synchronized关键字和CAS来保证其线程安全,CAS失败自旋保证成功,再失败就synchronized保证。
synchronized:只锁定当前链表或者红黑树的首节点。

CAS(compare and swap):CAS 操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么会自动将该位置值更新为新值。否则,不做任何操作。


并发多线程相关

synchronized关键字

作用

synchronized关键字用来来防止线程干扰和内存一致性错误,解决了多线程之间资源的一致性。

用法
  • 修饰实例方法 锁是当前实例对象
  • 修饰静态方法 锁是当前类的Class对象
  • 修饰代码块 锁是synchonized括号里配置的对象
底层原理

对象中内置一个Monitor监视器
Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

对象在内存中由三个部分组成

分别为:对象头,实例数据和对其填充(非必须)构成。

Mark Word:存在于对象头中,存储对象的hashcode、锁信息、分代年龄和GC标志等信息,锁升级的过程就是基于修改Mark Word来实现的。

如何实现同步
同步方法:是隐式的,无须通过字节码指令来控制,通过ACC_SYNCHRONIZED访问标志判断一个方法是否声明为同步方法。
同步代码块:通过monitorentermonitorexit两条指令来实现同步,关联到一个monitor对象,进入时设置Owner为当前线程,进入计数器+1、退出计数器-1。

锁升级过程

过程:无锁状态->偏向锁状态(1次CAS)->轻量级锁状(多次CAS)->重量级锁状态
锁升级的过程是不可逆的,锁升级是为了将众多细粒度锁转换为较少的粗粒度的锁的过程,以削减系统开销,降低锁带来的性能消耗。

volatile关键字

特性

volatile是Java提供的轻量级的同步机制

  • 保证可见性(JMM模型和MESI协议)
  • 不保证原子性
  • 禁止指令重排(基于内存屏障实现)
MESI协议

一种缓存一致性协议,主要来解决多核CPU访问内存比较慢和缓存不一致等问题。

状态 描述
M(Modified) 数据被修改了,和内存中的数据不一致,数据只存在于本Cache中
E(Exclusive) 数据和内存中的数据一致,数据只存在于本Cache中。
S(Shared) 数据和内存中的数据一致,数据存在于很多Cache中。
I(Invalid) 缓存行中的内容时无效。
JMM内存模型

1.所有的变量都存储在主内存中,每个线程都有一个私有的工作内存来存储共享变量的副本,并且每个线程只能访问自己的工作内存,无法访问其他线程的工作内存,也不能直接读写主内存中的变量。
2.不同线程之间无法直接访问对方工作内存中的变量,线程间变量值传递均需要在主内存来完成。

多线程并发编程中三个特性
  • 可见性
    是指当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。
  • 原子性
    原子性指的一个操作或一组操作要么全部执行,要么全部不执行。
  • 有序性
    有序性是指程序执行的顺序按照代码的先后顺序执行。
总线风暴

由于volatile基于MESI缓存一致性协议需要不断的从主内存嗅探和CAS不断循环无效交互导致总线带宽达到峰值。

AtomicInteger

基于volatile + CAS + Native 来实现的线程安全的int包装类

Lock

介绍

Lock锁也叫做同步锁,实现类为ReentrantLock。

Lock和synchronized的区别
  • synchronized是关键字,作用于JVM层面的,Lock是API层面的锁。

  • synchronized不需要手动释放锁,在线程结束或者异常时会自动释放锁。ReentrantLock必须手动去释放锁,遇到异常,释放不正确会导致死锁,一般在finally{}中释放。

  • synchronized为非公平锁,ReentrantLock可以通过参数来设置是否为公平锁,是否公平取决于线程是否按照先后顺序获取锁。

  • synchronized不可中断,ReentrantLock可以中断。

  • ReentrantLock提供的Condition(条件)可以指定唤醒哪些线程,而synchronized只能随机唤醒一个或者全部唤醒。

  • ReentrantLock比synchronized更灵活。

ThreadLocal

介绍

ThreadLocal类存储每个线程的私有数据,即线程的局部变量副本,每个线程维护自己的一份数据,从而达到线程隔离的效果。

内存泄露问题

ThreadLocalMap的生命周期与 Thread 一致,同时ThreadLocalMap 中的 Entry的 key 对 ThreadLocal 是弱引用,但是Entry中的value是被Entry强引用的,如果不手动的清除掉 ThreadLocalMap中Entry对象,在垃圾回收时,key会被回收掉,但是value会被回收,这样不管是否使用弱引用都有内存泄露的可能。所以一般在使用完后需要手动调用remove()方法清除掉

应用场景
  • 全局存储用户信息,方便获取。
  • 代替参数的显示传递。
  • 解决线程安全问题,达到线程隔离的效果。

线程

创建线程有哪几种方式
  • 继承Thread类。
  • 实现Runnable接口。
  • 使用Callable和Future创建。
  • 创建ThreadPool,从线程池中获取。
线程有哪几种状态
  • New(新建),新创建的线程进过初始化,进入Runnable(就绪)状态;
  • Runnable(就绪),等待线程调度,调度后进入Running(运行)状态;
  • Running(运行),线程正常运行,期间可能会因为某些情况进入Blocked(堵塞)
    状态(同步锁;调用了sleep()和join()方法进入Sleeping状态;执行wait()
    方法进入Waiting状态,等待其他线程notify通知唤醒);
  • Blocked(堵塞),线程暂停运行,解除堵塞后进入Runnable(就绪)状态
    重新等待调度;
  • Dead(死亡):线程完成了它的任务正常结束或因异常导致终止;

线程池

线程池的优点
  • 降低资源消耗。
  • 提高响应速度。
  • 提高线程的可管理性。
  • 避免大量的线程间因互相抢占系统资源导致的阻塞现象。
线程池的核心参数
  • corePoolSize 核心线程数
  • maximumPoolSize 线程池最大线程数
  • keepAliveTime 空闲线程存活时间
  • unit 空闲线程存活时间单位
  • workQueue 工作队列
  • threadFactory 线程工厂
  • handler 拒绝策略
    • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(默认
    • DiscardPolicy:丢弃任务,但是不抛出异常。
    • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
    • CallerRunsPolicy:由调用线程处理该任务。
创建线程池有哪几种方式
Executors
  • newCachedThreadPool:创建一个可缓存线程池。
  • newFixedThreadPool : 创建一个定长线程池。
  • newScheduledThreadPool :创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor :创建一个单线程的线程池。
  • newWorkStealingPool:创建了一个新的具有抢占式操作的线程池,每个线程都有一个任务队列存放任务。
ThreadPoolExecutor
  • 根据核心参数定制你需要的线程池
如何设置核心线程数
  • CPU密集型:核心线程数 = CPU核数 + 1
  • IO密集型:核心线程数 = CPU核数 * 2
    具体可以参考:Java线程池实现原理及其在美团业务中的实践

JVM、Spring、Mysql、Redis等相关面试题持续更新中。

你可能感兴趣的:(Java之路,八股文之路,java,面试)