1. hashCode和equals
hashCode介绍
hashCode()的作用是获取哈希码,也称为散列码,它实际上是返回一个int整数,这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,Java中的任何类都包含了hashCode()函数。
散列表存储的是键值对(key-value),特点是:能根据key快速检索出对应的value,这其中就利用到了散列码(可以快速找到需要的对象)。
为什么要有hashCode,以“HashSet 如何检查重复”为例:
对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,看该位置是否有值,如果没有,HashSet 会假设对象没有重复出现。但是如果发现该索引位置已有元素,这时会调用 equals() 方法来检查两个对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会发生哈希冲突,将新对象挂载到链表或红黑树上。这样就大大减少了 equals 的调用次数,相应就大大提高了执行速度。
- 如果两个对象相等,则hashCode一定相同
- 两个对象相等,对这两个对象分别调用equals方法都返回true
- 两个对象有相同的hashCode值,他们也不一定是相等的
- 因此,equals方法被重写过,则hashCode方法也必须被重写
- hashCode()的默认行为是对堆上的对象生成哈希值。如果没有重写hashCode(),即便两个对象成员变量完全一致,该类的两个对象哈希码大概率不同,equals默认比较地址也不会判定相等
2. ConcurrentHashMap原理,jdk7和jdk8版本的区别
JDK7
底层数据结构
ReentrantLock + Segment + HashEntry
外层是Segment数组,单个Segment内部封装HashEntry数组,数组每个下标位置挂载单向HashEntry链表。
寻址查询
采用两次哈希定位:
加锁机制
Segment直接继承ReentrantLock实现分段锁;
仅锁定当前操作的Segment分段,其余Segment完全不受锁影响;
并发上限等于Segment分段数量,可在构造方法手动指定;
单个Segment内部数组扩容,不会干扰其他分段。
get读操作
查询全程不需要加锁,依靠volatile修饰变量保证数据可见性。
JDK8
底层数据结构
synchronized + CAS + Node + 链表+红黑树
废弃Segment分层结构,底层直接是Node数组;
链表长度超过阈值会转为红黑树优化查询效率;
Node节点里val值与next指针均用volatile修饰,保障多线程可见。
基础修改操作
查找、替换、新增赋值优先使用CAS无锁操作。
锁策略
不再锁整个分段,仅对当前哈希桶链表头head节点加synchronized锁;
锁粒度大幅缩小,不会阻塞其他桶位元素读写,并发性能更高;
扩容支持多线程并发协助迁移数据,不会阻塞全局读写。
读操作无锁设计
Node的val、next被volatile修饰,读写之间数据互相可见;
哈希数组table本身由volatile修饰,扩容时读线程可以及时感知数组变化。
对比总结
- JDK7依靠Segment分段锁+ReentrantLock,锁定整个分段,两次hash寻址,各分段独立扩容;
- JDK8移除Segment,改用CAS+对桶头加synchronized锁,链表过长转为红黑树,支持多线程并发扩容,锁粒度更细、并发能力更强;
- 两个版本get操作均不加锁,依靠volatile关键字保证读取数据可见性。
3. 如何实现一个IOC容器
简要回答
详细回答
4. 什么是字节码?采用字节码的好处是什么?
Java中引入了虚拟机的概念,这台虚拟机在任意平台都为编译程序提供统一接口。
编译程序只需要面向虚拟机,生成虚拟机能够识别的代码,再由JVM解释器将虚拟机代码转换为当前操作系统的机器码执行。在Java中,这种专供JVM解析执行的中间代码叫做字节码(即扩展名为.class的文件),它不面向任何特定CPU处理器,只面向Java虚拟机。
执行流程:.java源代码 → javac编译 → .class字节码文件 → JVM解析翻译为机器码 → CPU执行
好处
5. Java类加载器
Java源码编译后生成class字节码文件存储在磁盘,类加载器ClassLoader是JVM中专负责查找、读取class字节码文件,将类加载至JVM方法区,并在堆中生成对应Class对象,程序后续依靠该Class对象创建实例、访问类成员。
JVM内置三种类加载器,自上而下双亲委派层级关系如下:
启动类加载器(Bootstrap ClassLoader)
最顶层加载器,由C++编写,本身并非Java类;负责加载JDK核心基础类库,例如java.lang包下Object、String等核心类,加载路径为jre/lib下核心rt.jar等包。
扩展类加载器(Extension ClassLoader)
父加载器为启动类加载器,用于加载jre/lib/ext扩展目录下的jar包,用来拓展JDK原生基础功能。
应用程序/系统类加载器(Application ClassLoader)
开发默认使用的加载器,父加载器是扩展类加载器;加载项目classpath下自定义编写代码,以及Maven/Gradle引入的第三方依赖Jar包。
6. 双亲委派模型
加载流程
作用
7. Java中的异常体系
Java中所有异常与错误的顶层父类为Throwable。
Throwable下分为两大子类:Exception与Error。
- Error:属于程序无法手动处理的严重系统级错误,一旦触发通常直接终止程序进程。
- Exception:不会直接导致程序整体崩溃,又划分为运行时异常RuntimeException与受检异常CheckedException。
- RuntimeException运行时异常:程序运行期间抛出,编译阶段不会强制校验捕获,会终止当前线程执行。
- CheckedException受检异常:编译期强制校验,必须使用try-catch捕获或者throws向上抛出,否则代码编译报错。
8. GC如何判断对象可以被回收
1. 引用计数法
给每个对象维护一个引用计数器,新增引用计数+1,引用失效释放计数-1,计数器为0时标记可回收。
缺陷:若A对象持有B引用、B同时持有A引用,二者外部无其他引用,计数器仍为1,对象永远无法被GC回收,出现内存泄漏,因此Java未采用该算法。
2. 可达性分析法(Java正式采用)
以GC Roots作为起始节点向下遍历搜索,遍历经过的链路称为引用链。若一个对象没有任何引用链与GC Roots相连,则判定该对象不可达,标记为可回收对象。
可作为GC Roots的对象:
对象回收补充说明
被标记为不可达不等于立即销毁:对象拥有一次自救机会,依靠finalize()方法。
9. 线程的生命周期/线程有哪些状态
线程共五种核心状态:新建、就绪、运行、阻塞、终止。
阻塞状态细分三类:
阻塞分类
各状态详解
10. sleep()、wait()、join()、yield()的区别
锁池与等待池概念
- 锁池:所有竞争同一把synchronized同步锁的线程存入锁池;锁被占用时其余线程在锁池等待;锁释放后锁池内线程争抢锁,抢到锁后进入就绪队列等待CPU调度。
- 等待池:调用wait()后线程进入等待池,等待池内线程不会参与锁竞争;必须被notify/notifyAll唤醒后,线程才会转入锁池参与锁争夺。notify随机选取一条等待池线程移入锁池;notifyAll将等待池全部线程移入锁池。
方法区别
yield()
执行yield()后线程直接回到就绪状态,主动让出本次CPU执行权,但依旧保留CPU调度资格,操作系统下一次调度仍有可能再次选中该线程执行。
join()
在主线程B中调用线程A.join(),线程B进入阻塞状态,直到线程A执行完毕结束,或是调用interrupt中断等待,B才会解除阻塞继续执行。





