锁相关
1. 什么是可重入锁?Java 中如何实现?
答:
可重入锁允许一个线程多次获取同一把锁(即递归调用时无需重新竞争锁)。
- 关键点:防止死锁,避免线程因重复请求已持有的锁而阻塞。
- Java 实现:
- synchronized:隐式支持可重入。
public synchronized void methodA() {
methodB(); // 可重入
}
public synchronized void methodB() {} - ReentrantLock:显式锁,通过计数器实现可重入。
ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 递归调用或其他同步代码
} finally {
lock.unlock();
}
}
2. ReentrantLock 与 synchronized 的区别?
3. 什么是读写锁(ReadWriteLock)?
答:
读写锁分离读操作(共享)和写操作(独占),提升并发性能。
- 规则:
- 读锁:允许多线程同时读,但排斥写锁。
- 写锁:独占锁,排斥其他所有读写锁。
- 适用场景:读多写少(如缓存)。
- Java 实现:ReentrantReadWriteLock
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void readData() {
rwLock.readLock().lock();
try {
// 读操作
} finally {
rwLock.readLock().unlock();
}
}
public void writeData() {
rwLock.writeLock().lock();
try {
// 写操作
} finally {
rwLock.writeLock().unlock();
}
}
4. 死锁产生的条件?如何避免?
死锁条件:
1.互斥:资源独占。
2.持有且等待:线程持有资源并等待其他资源。
3.不可抢占:资源只能由持有线程释放。
4.循环等待:多个线程形成资源请求闭环。
避免方法: - 破坏循环等待:按固定顺序获取锁(如按锁对象的哈希值排序)。
public void transfer(Account a, Account b, int amount) {
Object firstLock = a.hashCode() < b.hashCode() ? a : b;
Object secondLock = firstLock == a ? b : a;
synchronized (firstLock) {
synchronized (secondLock) {
// 转账操作
}
}
} - 超时机制:使用 tryLock() 设置超时时间。
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
try { /* … */ } finally { lock2.unlock(); }
}
} finally { lock1.unlock(); }
} - 银行家算法:动态检查资源分配是否安全(实际开发较少用)。
5. 乐观锁与悲观锁的区别?
CAS 示例:
AtomicInteger count = new AtomicInteger(0);
public void increment() {
int oldVal, newVal;
do {
oldVal = count.get();
newVal = oldVal + 1;
} while (!count.compareAndSet(oldVal, newVal)); // CAS 更新
}
6. synchronized 锁升级过程?
1.无锁:初始状态。
2.偏向锁:锁被同一线程多次访问时,记录线程 ID(避免 CAS 操作)。
3.轻量级锁:当多线程竞争时,通过 CAS 自旋尝试获取锁(减少阻塞)。
4.重量级锁:自旋超过阈值后,转为操作系统级互斥锁(线程阻塞)。
-
目的:平衡性能与开销,减少直接使用重量级锁的成本。
7. volatile 能否替代锁?
答:不能完全替代。 -
volatile 特性:
-
保证可见性:修改后立即刷新到主内存。
-
禁止指令重排序(内存屏障)。
-
不足:
-
不保证原子性(如 i++ 需配合锁或原子类)。
-
无法实现互斥访问(如临界区保护)。
适用场景: -
状态标记(如 boolean flag = true)。
-
单次读写操作(如 long、double 的 64 位写入)。
8. 什么是自旋锁?
答:线程在获取锁失败时,不立即阻塞而是循环(自旋)重试,避免上下文切换开销。 -
实现:
public class SpinLock {
private AtomicReference owner = new AtomicReference<>();public void lock() {
Thread t = Thread.currentThread();
while (!owner.compareAndSet(null, t)) {
// 自旋等待
}
}
public void unlock() {
owner.set(null);
}
} -
适用场景:锁持有时间短、线程数少。
-
缺点:长时间自旋浪费 CPU(JDK 的自旋锁有自适应策略)。
9. StampedLock 是什么?
答:Java 8 引入的高性能读写锁,支持三种模式:
1.写锁:独占锁(类似 ReentrantReadWriteLock 的写锁)。
2.悲观读锁:共享锁(类似读锁)。
3.乐观读:无锁操作,需验证是否有写发生。
StampedLock sl = new StampedLock();
// 乐观读
long stamp = sl.tryOptimisticRead();
if (!sl.validate(stamp)) { // 检查期间是否有写操作
stamp = sl.readLock(); // 转为悲观读
try { /* … */ } finally { sl.unlockRead(stamp); }
}
- 优势:乐观读避免锁开销,提升读性能。
10. Condition 接口的作用?
答:配合 ReentrantLock 实现线程等待/唤醒机制,可替代 Object.wait()/notify()。 - 优势:支持多个等待队列(如生产者-消费者模型)。
java运行复制ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列未满条件
Condition notEmpty = lock.newCondition(); // 队列非空条件
// 生产者
public void put(Object item) {
lock.lock();
try {
while (queue.isFull()) notFull.await(); // 等待未满
queue.add(item);
notEmpty.signal(); // 唤醒消费者
} finally { lock.unlock(); }
}
// 消费者
public Object take() {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await(); // 等待非空
notFull.signal(); // 唤醒生产者
return queue.remove();
} finally { lock.unlock(); }
}
11. 锁消除(Lock Elision)和锁粗化(Lock Coarsening)是什么?
- 锁消除:JIT 编译器移除不必要的锁(如局部变量锁)。
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // 局部变量,线程安全
sb.append(s1).append(s2);
return sb.toString();
}
// JIT 可能移除 StringBuffer 的同步操作 - 锁粗化:合并多次加锁/解锁操作为一次(减少开销)。
synchronized (obj) { /* 操作1 / }
synchronized (obj) { / 操作2 / }
// 优化后 =>
synchronized (obj) { / 操作1+操作2 */ }
12. 如何检测死锁?
1.命令行工具: - jstack :查看线程堆栈,标记死锁信息。
- jconsole:图形化检测死锁。
2.代码检测:
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
if (threadIds != null) {
// 处理死锁
}
总结 - 基础锁:synchronized(隐式)、ReentrantLock(显式)。
- 高级锁:ReadWriteLock、StampedLock、Condition。
- 锁优化:锁升级、消除、粗化。
- 避免死锁:顺序加锁、超时机制。
- 工具:乐观锁(CAS)、原子类、并发工具包(java.util.concurrent)。
synchronized和lock
synchronized vs Lock(以ReentrantLock为例)详解
1. 实现机制
2. 使用方式
// ========== synchronized ==========
// 1. 方法级锁
public synchronized void syncMethod() { /…/ }
// 2. 代码块锁
public void method() {
synchronized(this) { // 锁对象可以是任意对象
// 临界区代码
}
}
// ========== Lock ==========
private final Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock(); // 必须手动加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须放在finally中保证释放
}
}
3. 核心功能对比
4. 性能差异
- Java 5 之前:synchronized 性能远低于 Lock(线程阻塞涉及内核态切换)
- Java 6 及以后:
- synchronized 引入锁升级机制(偏向锁→轻量级锁→重量级锁)
- 两者性能差距大幅缩小
- 建议:
- 简单场景 → 优先用 synchronized(简洁安全)
- 复杂场景 → 用 Lock(利用高级功能)
5. 锁升级过程(synchronized优化)
图片代码首次获取锁其他线程竞争自旋超过阈值无锁状态偏向锁轻量级锁:自旋适应重量级锁:线程阻塞
6. 条件变量对比
// ===== synchronized 单条件 =====
synchronized(lockObj) {
while(!condition) lockObj.wait(); // 只能使用一个条件
// 业务代码
lockObj.notifyAll();
}
// ===== Lock 多条件 =====
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
// 生产者
lock.lock();
try {
while(queue.isFull()) notFull.await(); // 特定条件等待
// 生产操作
notEmpty.signal(); // 唤醒特定条件
} finally { lock.unlock(); }
// 消费者
lock.lock();
try {
while(queue.isEmpty()) notEmpty.await();
// 消费操作
notFull.signal();
} finally { lock.unlock(); }
7. 适用场景总结
8. 关键差异总结
💡 最终建议:
- 优先用 synchronized(简洁安全,JVM持续优化)
- 遇到以下需求时改用 Lock:
✦ 需要等待超时/中断响应
✦ 分组唤醒线程
✦ 公平锁需求
✦ 需要获取锁的详细信息
锁升级过程中什么是偏向锁→轻量级锁→重量级锁?什么是自旋?
锁升级过程详解(从偏向锁→轻量级锁→重量级锁)
1. 偏向锁(Biased Locking)
- 目的:消除无竞争情况下的同步开销
- 适用场景:单线程多次访问同步块
- 实现原理:
// 对象头存储结构
|-------------------------------------------------------|
| Mark Word (64 bits) |
|-------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
|-------------------------------------------------------| - 首次获取锁时,JVM将线程ID写入对象头(biased_lock=1, lock=01)
- 后续进入同步块时直接比对线程ID,无需CAS操作
- 特点:
- 只需一次CAS开销(首次)
- 单线程重复访问无同步成本
- 示例:
synchronized(lock) { /* 第1次进入:CAS设置偏向锁 / }
synchronized(lock) { / 第2次进入:直接检查线程ID */ }
2. 轻量级锁(Lightweight Locking) - 触发条件:第二个线程尝试获取锁(偏向锁撤销)
- 实现原理:
a.JVM在线程栈帧中创建Lock Record
b.复制对象头到Lock Record(Displaced Mark Word)
c.CAS尝试将对象头替换为指向Lock Record的指针
d.成功获取锁(lock状态置为00) - 锁竞争处理:
while (true) {
if (尝试CAS成功) { break; }
else if (自旋超过阈值) { 升级为重量级锁 }
else { continue; } // 继续自旋
} - 特点:
- 通过自旋避免线程阻塞
- 适合短时间持有锁的场景
- 自旋消耗CPU但减少线程切换
3. 重量级锁(Heavyweight Locking) - 触发条件:自旋失败或等待线程数过多
- 实现原理:
图片代码Object Header指向Monitor对象Owner:持有锁的线程EntryList:竞争队列WaitSet:等待队列 - 依赖操作系统的互斥量(mutex)
- 未获锁线程直接阻塞(用户态→内核态切换)
- 特点:
- 开销大(涉及上下文切换)
- 防止CPU空转
- 支持更复杂的同步机制
自旋(Spinning)详解
1. 核心思想
-
不阻塞线程,而是在循环中重试获取锁
-
示例自旋锁实现:
public class SpinLock {
private AtomicReference owner = new AtomicReference<>();public void lock() {
Thread t = Thread.currentThread();
while (!owner.compareAndSet(null, t)) {
// 空循环重试(自旋)
}
}public void unlock() {
owner.set(null);
}
}
2. 自旋策略
3. 开销分析
4. 最佳实践
1.短任务优先自旋
if (!lock.tryLock()) {
for (int i=0; i<MAX_SPIN; i++) { // 限制自旋次数
if (lock.tryLock()) return;
}
lock.lock(); // 自旋失败后阻塞
}
2.结合条件检查
while (true) {
if (tryAcquireLock()) break;
if (needToBlock()) {
parkThread(); // 放弃自旋
break;
}
}
3.JVM自适应优化
- 历史数据:统计锁持有时间
- 动态调整:自旋时间 ≈ 上次持有时间 * 系数
- 平台相关:单核CPU直接禁用自旋
关键点总结
1.锁升级
- 目标:最小化同步开销
- 路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 不可逆:重量级锁无法降级(部分JVM支持降级但成本高)
2.自旋优化 - 目的:避免用户态→内核态切换
- 权衡:CPU时间 vs 上下文切换开销
- 临界点:锁持有时间 ≈ 线程切换时间 * 2
💡 实战建议:
undefined.减少竞争:缩小同步块范围
undefined.分离热点:读写锁分离(如ConcurrentHashMap)
undefined.避免升级:控制锁持有时间短于自旋阈值
undefined.监控工具:jstack -l 检查锁状态