简介:这是一份专为Java后端及全栈开发者面试冲刺打造的「八股文」高频题库,聚焦Redis、MySQL、算法、多线程与数据结构等核心考点,直击互联网大厂技术面试关键环节。资源以1个25.8MB的PDF文件呈现,内容结构清晰、分类明确,涵盖B/S与C/S架构辨析、JDK/JRE区别、OOP四大特征、类与对象关系、Java八大基本类型及包装类、instanceof原理、自动拆装箱机制、隐式/显式类型转换、Char类型转换细节、常见数据结构实现(ArrayList/LinkedList/Stack/Queue/Map/Tree)等近20类高频问答,每题均附简明结论与底层逻辑说明。已有2498人学习下载,适合应届求职者夯实基础、转岗人员查漏补缺、以及在职工程师临阵磨枪——不堆砌概念,重在厘清易混淆点与面试常挖深坑,助你把理论知识转化为有说服力的现场表达。
1. 八股文不是背题手册,是面试官在30秒内判断你“代码直觉”的黑匣子
很多人把八股文当作文言文默写——死记硬背“重载和重写的区别”“String、StringBuilder、StringBuffer 三者差异”,结果一到手写代码题就卡壳,一问“为什么HashMap要重写hashCode和equals”就支吾,一聊“Redis缓存穿透怎么防”就开始编。这不是你没背熟,是你根本没理解:八股文的底层逻辑,从来不是考定义,而是考你对Java运行机制的肌肉记忆和边界感。
比如,看到“Integer i1 = 100; Integer i2 = 100; System.out.println(i1 == i2);输出什么?”,90%人能答“true”。但面试官真正想听的,是下一句:“因为IntegerCache默认缓存-128到127,这个范围内的自动装箱会复用对象,所以==比较地址也相等;但如果是200,就会新建对象,==就返回false。”——这背后牵出的是JVM常量池、类加载时机、包装类缓存策略三个考点。一道题,就是一条知识链的入口。
这套资源覆盖的不是零散知识点,而是面试高频攻击面:OOP底层契约(如继承/多态的JVM级约束)、并发安全临界点(synchronized锁升级路径、volatile可见性边界)、数据结构选型依据(ArrayList vs LinkedList真实增删查性能拐点)、MySQL索引失效黑盒(最左前缀到底“前缀”到哪一级)、Redis原子操作陷阱(INCR在集群模式下的分片不一致)。它不教你怎么“答对”,而是帮你建立“这个问题在系统里到底发生了什么”的条件反射。适合两类人:一是校招应届生,需要把课堂知识焊接到工程语境;二是3年以内社招,简历写了“熟悉JVM”,但被问到“G1如何避免全堆扫描”时大脑空白。别怕“理论无用论”——真正在项目里调API写逻辑的人,恰恰最需要八股文来补上那层“为什么这个API这么设计”的认知断层。现在,我们拆开这份资源,看它到底怎么帮你把“背过”变成“用过”。
1.1 面试官的30秒决策模型:从“能写”到“敢用”的信任链
你可能不知道,技术面试中,面试官对你的技术可信度评估,遵循一个隐性但极其严格的三级漏斗:
第一关:语法正确性(5秒)
看你写for (int i = 0; i < list.size(); i++)会不会下意识加list.get(i),还是直接写list.get(i)——前者暴露你对ArrayList随机访问O(1)的直觉,后者说明你连基础数据结构特性都没内化。这不是考记忆力,是考你日常编码是否形成肌肉反射。第二关:边界敏感度(15秒)
当你答“HashMap线程不安全”,面试官立刻追问:“那如果只读不写呢?”如果你脱口而出“安全”,恭喜,你掉进坑了——ConcurrentHashMap的size()方法在高并发下可能不准,而HashMap的迭代器在结构变更时会抛ConcurrentModificationException,“只读”不等于“绝对安全”。这种问题专打“概念模糊区”。第三关:设计权衡意识(10秒)
问“为什么Redis用单线程却快?”,标准答案是“避免上下文切换+纯内存操作”。但高手会补一句:“但它牺牲了CPU多核利用率,所以Redis 6.0引入了多线程IO,只负责网络读写,命令执行仍单线程——这是在吞吐和一致性间的精准切割。”面试官要的不是标准答案,而是你能否看见技术选择背后的trade-off。
这份八股文资源的价值,正在于它把每个问题都锚定在这三级漏斗上。比如“s1 = s1 + 1报错,s1 += 1不报错”,它不只告诉你“因为+=有隐式强转”,而是带你看字节码:iadd指令后跟i2s(int to short),而s1 = s1 + 1的字节码是iadd后直接istore_1,类型不匹配直接报错。你背的不是结论,是JVM指令级的证据链。
1.2 为什么“工作中用不到”的八股文,反而是大厂筛人的终极武器?
有人质疑:“我天天写Spring Boot,从来没手写过红黑树,为啥要考?”——这问题问到了本质。但真相是:大厂不考你“会不会用框架”,考你“框架崩了你能不能修”。举个真实场景:某次线上服务响应突增200ms,监控显示GC时间飙升。运维同学查日志发现大量java.lang.OutOfMemoryError: Metaspace。这时候,如果你只知道“加-XX:MaxMetaspaceSize”,却答不出“Metaspace内存泄漏通常由动态代理类(如CGLIB)未卸载导致,需结合jstat -gcmetacapacity定位类加载器”,你就失去了故障根因分析能力。
八股文里的每一个点,都是这种“故障时刻”的前置知识储备:
volatile的内存屏障作用 → 解释为什么双重检查锁(DCL)中instance字段必须加volatile;ThreadLocal的弱引用Key → 说明为什么不手动remove会导致内存泄漏;- MySQL B+树索引结构 → 推导出“like '%abc'无法走索引”的物理原因(B+树按字典序排序,%开头无法确定搜索起点);
- Redis AOF重写机制 → 理解为什么
bgrewriteaof期间新命令仍会追加到旧AOF文件。
这些不是“用不到”,而是“用到时,你有没有资格参与讨论”。这份资源把427道题按故障归因链组织:从现象(服务慢)→ 定位工具(jstat/jstack)→ 底层原理(JVM内存模型)→ 修复方案(参数调优/代码重构)。它不承诺让你写出完美代码,但确保你在生产环境报警时,第一个想到的不是重启,而是jmap -histo:live。
1.3 别再无效刷题:用“考点穿透法”把八股文变成你的技术雷达图
盲目刷题=用锤子砸所有钉子。高效准备的核心,是建立考点穿透法:对每道题,强制回答三个问题:
- 它在考哪个JVM/OS/数据库组件的哪条规则?(例:“
final修饰的变量不可变”实际在考JVM常量池的编译期优化规则) - 这个规则在什么边界会被打破?(例:
final修饰的引用类型,其指向对象的内容可变,这就是“不可变”的真实边界) - 如果违反这条规则,系统会以什么错误形态暴露?(例:不重写
hashCode导致HashSet去重失效,表现为“明明equals为true,却能存两个相同对象”)
你会发现,原本散落的题目突然连成网络。比如“String str = "abc"和String str = new String("abc")的区别”,表面考字符串常量池,深挖下去:
- 涉及类加载阶段(常量池在类加载时创建)
- 涉及对象创建路径(new走堆内存分配,字面量走常量池)
- 涉及GC策略(常量池对象在JDK7后移到堆中,受Full GC管理)
- 涉及性能陷阱(频繁new String会绕过常量池,造成堆内存浪费)
这份资源的全部题目,都已按此逻辑打上穿透标签:每道题旁标注【JVM-类加载】、【并发-锁升级】、【MySQL-索引失效】等。你不需要背427道题,只需掌握37个核心穿透点,就能覆盖90%的面试攻击路径。现在,我们进入第一个高频战场:OOP与Java基础——这里埋着最多“看似简单,实则致命”的失分点。
2. OOP与Java基础:那些被面试官反复验证的“契约感”
OOP不是编程范式,是Java世界的宪法。面试官所有关于“继承”“多态”“封装”的提问,本质是在测试你是否真正理解这套宪法的刚性条款——哪些行为被明令禁止(如子类重写final方法),哪些权利被严格保障(如父类构造器必然先于子类执行),哪些自由存在灰色地带(如static方法能否被“重写”)。这份资源把OOP考点拆解为契约三要素:继承契约、多态契约、封装契约,每道题都指向一个具体条款的履行情况。
2.1 继承契约:父类不是模板,是带法律效力的协议
继承不是“复制粘贴代码”,而是签署一份具有法律效力的协议。协议规定:子类必须遵守父类的行为契约(方法签名)、访问契约(修饰符权限)、生命周期契约(构造器调用顺序)。面试官最爱用“反常识案例”检验你是否签过这份协议。
真题1:以下代码输出什么?为什么?
class Parent { static { System.out.print("Parent-static "); } { System.out.print("Parent-instance "); } public Parent() { System.out.print("Parent-constructor "); } } class Child extends Parent { static { System.out.print("Child-static "); } { System.out.print("Child-instance "); } public Child() { System.out.print("Child-constructor "); } } public class Test { public static void main(String[] args) { new Child(); new Child(); } }答题思路:这不是考“谁先执行”,是考你是否理解JVM类加载的静态初始化块(static block)只执行一次,以及实例初始化块({})和构造器在每次new时都执行。
标准答案:Parent-static Child-static Parent-instance Parent-constructor Child-instance Child-constructor Parent-instance Parent-constructor Child-instance Child-constructor
关键解析:
- 第一次
new Child():先触发Parent类加载(执行Parent-static),再触发Child类加载(执行Child-static),然后按继承链执行实例块和构造器(Parent-instance→Parent-constructor→Child-instance→Child-constructor)- 第二次
new Child():Parent和Child类已加载,跳过静态块,只执行实例块和构造器
面试官想听的:你能否说出“静态块属于类,实例块属于对象”,并指出static修饰的代码块在类加载时执行,且仅执行一次——这是JVM规范的硬性要求,不是Java语法糖。
真题2:final方法能否被重写?如果子类定义同名方法会怎样?
class Base { final void method() { System.out.println("Base"); } } class Sub extends Base { // void method() { System.out.println("Sub"); } // 编译错误! }答题思路:
final是JVM层面的不可覆盖契约,编译器会在字节码校验阶段直接拒绝。
标准答案:编译失败,报错Cannot override the final method from Base。
关键解析:
final修饰的方法,其字节码中ACC_FINAL标志位被置位,JVM在类加载的验证阶段会检查该标志,若子类尝试覆盖则抛VerifyError- 这与
private方法不同:private方法因作用域限制无法被继承,所以子类“同名方法”是全新方法,不构成重写;而final方法可被继承,但禁止重写
面试官想听的:你能否区分“不能继承”(private)和“禁止重写”(final)的本质差异,并指出这是JVM字节码验证的强制规则。
2.2 多态契约:运行时绑定不是魔法,是JVM的精确寻址
多态的核心是运行时方法绑定(Dynamic Method Dispatch),它依赖JVM的虚方法表(vtable)。面试官通过“父子类同名方法调用”类题目,检验你是否理解vtable的构建规则和查找逻辑——这决定了你能否预判“到底执行哪个方法”。
真题3:以下代码输出什么?解释JVM的调用过程
class Animal { public void eat() { System.out.println("Animal eat"); } public static void sleep() { System.out.println("Animal sleep"); } } class Dog extends Animal { @Override public void eat() { System.out.println("Dog eat"); } public static void sleep() { System.out.println("Dog sleep"); } } public class Test { public static void main(String[] args) { Animal a = new Dog(); a.eat(); // 输出? a.sleep(); // 输出? } }答题思路:
eat()是实例方法,走vtable动态绑定;sleep()是静态方法,走编译期静态绑定。
标准答案:Dog eatAnimal sleep
关键解析:
a.eat():变量a声明类型为Animal,但实际对象是Dog。JVM在Dog类的vtable中查找eat方法入口,找到Dog.eat()的实现,故输出Dog eata.sleep():静态方法调用不依赖对象实例,只看引用变量的声明类型。a声明为Animal,所以调用Animal.sleep(),输出Animal sleep
面试官想听的:你能否说出“静态方法绑定在编译期完成,与对象实际类型无关”,并指出这是JVM规范明确规定的(JLS §15.12.4.1),而非Java语言的设计缺陷。
真题4:接口默认方法与抽象类方法冲突时,谁优先?
interface Flyable { default void fly() { System.out.println("Flyable fly"); } } abstract class Bird { public void fly() { System.out.println("Bird fly"); } } class Sparrow extends Bird implements Flyable { // 无需重写fly() }答题思路:JVM解决方法冲突的优先级是:类中的方法 > 接口默认方法。
标准答案:调用Sparrow.fly()输出Bird fly。
关键解析:
- Java 8引入默认方法时,明确规定了冲突解决规则:若类继承了某个方法(来自父类或自身定义),则忽略接口的默认方法
- 这保证了向后兼容性:已有类实现接口时,不会因接口新增默认方法而意外改变行为
面试官想听的:你能否引用JLS(Java Language Specification)第9.4.1.2节“Default Methods and Inheritance”,说明这是语言规范的强制约定,而非开发者可选策略。
2.3 封装契约:访问修饰符不是语法糖,是JVM的内存访问栅栏
public/protected/default/private不是代码风格选项,而是JVM在字节码层面设置的内存访问栅栏。面试官通过“跨包/跨类调用”题目,检验你是否理解这些栅栏如何影响类加载、链接、执行全过程。
真题5:以下代码能否编译通过?为什么?
// package com.example.a; class Utils { protected static void log() { System.out.println("log"); } } // package com.example.b; class Test { public static void main(String[] args) { Utils.log(); // 编译错误? } }答题思路:
protected修饰的成员,对不同包的子类可访问,但Test不是Utils的子类,且不在同一包,故不可访问。
标准答案:编译失败,报错log() has protected access in Utils。
关键解析:
protected的访问规则是:同一包内所有类 + 不同包的子类Test与Utils不同包,且Test未继承Utils,因此不满足任一条件- JVM在符号引用解析阶段(Resolution)会检查访问控制,不满足则抛
IllegalAccessError
面试官想听的:你能否画出protected的访问矩阵(包内/包外 × 子类/非子类),并指出这是JVM链接阶段的强制校验,不是编译器的善意提醒。
真题6:内部类访问外部类私有成员,JVM如何实现?
class Outer { private int x = 10; class Inner { void print() { System.out.println(x); } // 访问private字段 } }答题思路:编译器自动生成合成方法(synthetic accessor method),将
private字段暴露给内部类。
标准答案:编译后,Outer类会生成一个package-private的合成方法access$000(Outer),返回x的值;Inner.print()实际调用此方法。
关键解析:
javap -c Outer$Inner可看到字节码调用Outer.access$000(Lcom/example/Outer;)I- 合成方法被标记为
ACC_SYNTHETIC,对源码透明,但JVM可调用- 这是Java语言为支持内部类语义,在编译期对JVM规范的“合法越界”
面试官想听的:你能否指出“编译器生成合成方法”是Java实现内部类的关键机制,并说明ACC_SYNTHETIC标志的作用——它告诉JVM“此方法由编译器生成,不受访问控制限制”。
2.4 易错点:OOP中那些让90%人翻车的“伪常识”
OOP考点里藏着大量“听起来对,其实错”的伪常识。这些点往往是面试官的“压力测试题”,专门筛选出真正理解机制的人。
错在哪 → 为什么错 → 怎么答对
易错点1:String是基本数据类型?
- 错在哪:混淆“基本类型”(primitive type)和“常用类型”。
- 为什么错:
String是java.lang.String类的实例,属于引用类型;Java只有8种基本类型(byte/int/char/boolean等)。 - 怎么答对:明确说“
String是引用类型,其底层是final char[]数组;基本类型是JVM直接支持的原子类型,不占用堆内存”。
易错点2:float f = 3.4;正确?
- 错在哪:忽略Java字面量默认类型规则。
- 为什么错:
3.4是double类型字面量,float是32位,double是64位,小范围类型不能直接接收大范围字面量(需显式转换)。 - 怎么答对:答“错误,应写为
float f = 3.4f;或float f = (float)3.4;”,并补充“f后缀表示float字面量,避免精度损失”。
易错点3:short s1 = 1; s1 = s1 + 1;有错?
- 错在哪:忽视Java的算术运算类型提升规则。
- 为什么错:
short参与运算时自动提升为int,s1 + 1结果是int,赋值给short需强制转换。 - 怎么答对:答“有错,因
+运算使s1提升为int,结果int不能直接赋值给short;正确写法是s1 = (short)(s1 + 1);或使用复合赋值s1 += 1;(编译器自动插入强转)”。
易错点4:equals()和==的区别,只答“值 vs 地址”?
- 错在哪:停留在表面,未触及JVM对象模型。
- 为什么错:
==比较的是对象在堆中的内存地址(即引用是否指向同一块内存),equals()默认行为也是==,但可被重写为逻辑相等(如String.equals()比较字符序列)。 - 怎么答对:答“
==是引用比较,equals()是逻辑比较;重写equals()必须同时重写hashCode(),否则违反Object契约(相等对象必须有相同哈希码),导致HashSet等集合失效”。
易错点5:static方法能被重写?
- 错在哪:混淆“重写(override)”和“隐藏(hide)”。
- 为什么错:
static方法属于类,不参与多态,子类同名static方法只是“隐藏”父类方法,而非重写。 - 怎么答对:答“不能重写,只能隐藏;调用哪个
static方法取决于引用变量的声明类型,而非实际对象类型。例如Parent p = new Child(); p.staticMethod()调用Parent.staticMethod()”。
3. 并发与线程:面试官眼中“线程安全”的真实边界
并发不是“加个synchronized就万事大吉”,而是对内存模型、锁机制、可见性保障的系统性理解。面试官所有关于“线程安全”“死锁”“CAS”的提问,本质是在测试你能否在脑中构建出多线程执行的时空坐标系——时间上,指令重排序如何破坏逻辑;空间上,主内存与工作内存如何割裂数据。这份资源把并发考点锚定在JMM(Java Memory Model)三大特性:原子性、可见性、有序性,每道题都指向一个特性的失效场景与防护手段。
3.1 原子性:你以为的“一步操作”,其实是CPU的“多步微指令”
原子性不是“看起来像一步”,而是“执行中不可分割”。面试官最爱用“i++”“count++”类题目,戳破你对“简单操作”的幻觉——这些在Java源码里一行的代码,对应JVM字节码的多条指令,中间可能被线程切换打断。
真题1:以下代码在多线程下是否线程安全?为什么?
public class Counter { private int count = 0; public void increment() { count++; // 非原子操作! } public int getCount() { return count; } }答题思路:
count++在字节码中分解为iload(加载)、iinc(自增)、istore(存储)三条指令,线程切换可能发生在任意两条之间。
标准答案:不安全。线程A执行iload和iinc后被挂起,线程B执行完整count++,A恢复后用旧值istore,导致一次自增丢失。
关键解析:
javap -c Counter可看到increment()方法字节码:iload_0→iinc 0, 1→istore_0- 这三步不是原子的,
iinc指令本身是原子的,但load和store是独立的- 解决方案:用
AtomicInteger.incrementAndGet()(CAS保证原子性)或synchronized块(锁保证临界区互斥)
面试官想听的:你能否说出“i++的非原子性源于JVM指令分解”,并指出AtomicInteger的compareAndSet如何利用CPU的cmpxchg指令实现硬件级原子性。
真题2:synchronized锁的是什么?对象头还是整个对象?
public class SyncDemo { private final Object lock = new Object(); public void method1() { synchronized(lock) { /* ... */ } } public void method2() { synchronized(this) { /* ... */ } } }答题思路:
synchronized锁的是对象监视器(monitor),而monitor存在于对象头(Object Header)中,但锁的粒度是整个对象。
标准答案:锁的是对象关联的monitor,monitor是JVM为每个对象维护的同步状态,存储在对象头的Mark Word中。
关键解析:
- 对象头包含Mark Word(存储hashcode/锁状态/线程ID等)和Klass Pointer(指向类元数据)
synchronized(lock)获取lock对象的monitor;synchronized(this)获取当前实例的monitor- monitor有三种状态:无锁、偏向锁、轻量级锁、重量级锁,JVM根据竞争程度自动升级
面试官想听的:你能否说出“锁升级路径:无锁→偏向锁(单线程)→轻量级锁(少量竞争)→重量级锁(激烈竞争)”,并指出这是JVM优化,开发者无需干预。
3.2 可见性:为什么“我改了,他看不到”?——主内存与工作内存的鸿沟
可见性问题不是代码写错了,而是JVM的内存模型设计使然。面试官通过“线程间变量传递”题目,检验你是否理解volatile如何用内存屏障(Memory Barrier)填平主内存与工作内存的鸿沟。
真题3:以下代码能否保证线程B看到flag为true?为什么?
public class VisibilityDemo { private static boolean flag = false; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) {} flag = true; System.out.println("t1 set flag = true"); }); Thread t2 = new Thread(() -> { while (!flag) { /* busy wait */ } System.out.println("t2 see flag = true"); }); t1.start(); t2.start(); t1.join(); t2.join(); } }答题思路:不保证。
flag未加volatile,线程B可能永远读取自己工作内存中的旧值(false),因JVM允许线程缓存变量副本。
标准答案:不能保证,程序可能死循环。需将flag声明为volatile。
关键解析:
volatile写操作后插入StoreStore和StoreLoad屏障,强制刷新工作内存到主内存volatile读操作前插入LoadLoad和LoadStore屏障,强制从主内存读取最新值- 这保证了“写可见性”,但不保证“原子性”(如
volatile int i; i++仍不安全)
面试官想听的:你能否画出volatile的内存屏障图,并说明StoreLoad屏障如何防止后续读操作重排序到volatile写之前——这是JMM规范的核心保障。
真题4:ThreadLocal如何实现线程隔离?内存泄漏风险在哪?
public class ThreadLocalDemo { private static ThreadLocal<String> local = new ThreadLocal<>(); public static void main(String[] args) { local.set("main"); new Thread(() -> { local.set("thread1"); System.out.println(local.get()); // thread1 }).start(); System.out.println(local.get()); // main } }答题思路:
ThreadLocal为每个线程维护独立的ThreadLocalMap,Map的key是ThreadLocal实例(弱引用),value是线程局部变量。
标准答案:ThreadLocal通过Thread类的threadLocals字段(ThreadLocalMap类型)实现隔离;内存泄漏风险在于ThreadLocalMap的key是弱引用,若ThreadLocal被回收,key为null,但value仍强引用,若线程长期存活(如线程池),value无法被GC。
关键解析:
ThreadLocalMap的Entry继承WeakReference<ThreadLocal>,key为弱引用,value为强引用ThreadLocal的get()/set()方法会主动清理key为null的Entry(expungeStaleEntry)- 最佳实践:
ThreadLocal使用完必须调用remove(),尤其在线程池场景
面试官想听的:你能否指出“ThreadLocal的key弱引用是为了解决key泄漏,但value泄漏需开发者手动remove”,并说明InheritableThreadLocal如何实现父子线程值传递。
3.3 有序性:指令重排序不是Bug,是CPU的性能优化
有序性指程序执行顺序与代码顺序一致。面试官通过“双重检查锁(DCL)”题目,检验你是否理解volatile如何用内存屏障禁止重排序——这是JVM为平衡性能与正确性做的精密设计。
真题5:以下DCL单例是否线程安全?如何修复?
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 非原子:1.分配内存 2.初始化 3.赋值给instance } } } return instance; } }答题思路:不安全。
new Singleton()在JVM中分为三步:①分配内存;②初始化对象;③将内存地址赋值给instance。步骤②③可能被重排序,导致线程A看到未初始化的instance。
标准答案:不安全。修复:将instance声明为volatile,禁止重排序。
关键解析:
volatile写操作的StoreStore屏障禁止②③重排序,StoreLoad屏障禁止其他读写重排序到volatile写之后- 修复后,线程A执行
instance = new Singleton()时,②③顺序严格保证,线程B读到instance != null时,对象必已初始化
面试官想听的:你能否说出“DCL失效的根本原因是new对象的三步操作可能重排序”,并指出volatile的StoreLoad屏障如何确保“初始化完成后再赋值”。
真题6:ReentrantLock与synchronized的区别?何时用前者?
public class LockDemo { private final ReentrantLock lock = new ReentrantLock(); public void doSomething() { lock.lock(); try { // 临界区 } finally { lock.unlock(); } } }答题思路:
ReentrantLock是API层面的锁,提供synchronized不具备的高级功能:可中断等待、超时获取、公平锁、多条件队列。
标准答案:
synchronized是JVM内置锁,自动加锁解锁,不可中断;ReentrantLock需手动lock()/unlock(),支持lockInterruptibly()(可中断)和tryLock(long, TimeUnit)(超时)synchronized是非公平锁;ReentrantLock可选公平/非公平(构造函数传true)synchronized只能关联一个条件(wait/notify);ReentrantLock可创建多个Condition(newCondition())
关键解析:ReentrantLock底层基于AQS(AbstractQueuedSynchronizer),用state变量和FIFO队列管理线程等待synchronized在JDK6后优化为偏向锁/轻量级锁,性能接近ReentrantLock,但功能更少
面试官想听的:你能否说出“AQS是JUC锁的基石,state表示同步状态,Node构成等待队列”,并举例“用Condition实现生产者-消费者精确唤醒,避免synchronized的notifyAll()唤醒所有线程”。
3.4 易错点:并发中那些让资深工程师都踩坑的“直觉陷阱”
并发领域的错误,往往源于对硬件和JVM底层的“直觉误判”。这些点是面试官的“压轴题”,专门淘汰靠经验主义答题的人。
错在哪 → 为什么错 → 怎么答对
易错点1:synchronized修饰静态方法,锁的是Class对象?
- 错在哪:认为锁的是“类的Class对象”,忽略锁的实际载体。
- 为什么错:
synchronized static method等价于synchronized (ClassName.class),锁的是该类的java.lang.Class实例,而Class实例本身是JVM在类加载时创建的单例对象。 - 怎么答对:答“锁的是
ClassName.class这个Class对象,它是JVM为每个类唯一创建的实例,存储在方法区;所有线程对该类静态方法的同步访问,都竞争同一个Class对象的monitor”。
易错点2:volatile能保证原子性?
- 错在哪:混淆“可见性”和“原子性”。
- 为什么错:
volatile只保证变量修改对其他线程立即可见,但i++等复合操作仍需多条指令,volatile无法保证这些指令的原子执行。 - 怎么答对:答“
volatile仅保证可见性和有序性,不保证原子性;i++需用AtomicInteger或synchronized;volatile boolean flag是安全的,因boolean读写本身是原子的”。
易错点3:Thread.sleep()会释放锁?
- 错在哪:将
sleep()与wait()混为一谈。 - 为什么错:
sleep()是Thread类的静态方法,让当前线程暂停指定时间,但**
本文还有配套的精品资源,点击获取