面试题答案
一键面试1. 锁的获取与释放方式
- synchronized:
- 特点:synchronized 是基于 JVM 层面的隐式锁。当进入 synchronized 修饰的代码块或方法时,自动获取锁;当代码块或方法执行完毕(正常结束或抛出异常),自动释放锁。
- 预防死锁优势:简单直观,由 JVM 自动管理锁的释放,减少了手动释放锁不当导致死锁的风险。
- 预防死锁劣势:如果获取锁的逻辑复杂,在嵌套锁的情况下,可能因获取锁顺序不当导致死锁,且一旦获取锁,不能在代码中间主动释放锁以打破死锁。
- ReentrantLock:
- 特点:是基于 API 层面的显式锁。通过调用 lock() 方法获取锁,调用 unlock() 方法释放锁,且必须在 finally 块中释放锁以确保锁一定会被释放。
- 预防死锁优势:手动控制锁的获取和释放,在复杂逻辑中可更灵活地调整锁的获取顺序,有机会打破死锁。
- 预防死锁劣势:需要手动释放锁,如果在 unlock() 之前发生异常,且没有在 finally 块中释放锁,可能导致死锁。
2. 锁的可中断性
- synchronized:
- 特点:synchronized 锁一旦获取,除非正常执行完毕或抛出异常,否则无法被中断。
- 预防死锁优势:无,在死锁发生时,无法中断线程来打破死锁。
- 预防死锁劣势:当多个线程相互等待对方释放锁形成死锁时,无法通过中断线程来解决死锁问题。
- ReentrantLock:
- 特点:ReentrantLock 提供了 lockInterruptibly() 方法,线程在获取锁的过程中可以被中断。
- 预防死锁优势:当检测到死锁倾向时,可通过中断线程来打破死锁。例如,在一个线程等待获取锁的时间过长时,可以中断该线程,让其放弃已获取的部分锁,从而打破死锁。
- 预防死锁劣势:需要额外编写处理中断的逻辑,如果处理不当,可能导致程序逻辑混乱。
3. 公平性
- synchronized:
- 特点:synchronized 是非公平锁,即锁的获取顺序不是按照请求顺序来分配,新请求的线程有可能插队获取锁。
- 预防死锁优势:在高并发场景下,非公平锁能减少线程切换开销,提高系统吞吐量。由于新线程可能插队获取锁,在一定程度上减少了死锁的可能性(因为线程获取锁的随机性增加)。
- 预防死锁劣势:非公平性可能导致某些线程长时间得不到锁,在极端情况下,也可能间接导致死锁(例如某些线程一直无法获取锁而陷入等待)。
- ReentrantLock:
- 特点:ReentrantLock 可以通过构造函数选择公平锁或非公平锁。公平锁按照请求锁的顺序来分配锁,非公平锁则允许新请求的线程插队获取锁。
- 预防死锁优势:使用公平锁时,所有线程按照顺序获取锁,减少了因线程插队导致的死锁风险。例如,在多个线程依赖相同资源且获取锁顺序敏感的场景下,公平锁能确保每个线程都有机会获取锁,避免死锁。
- 预防死锁劣势:公平锁会增加线程切换开销,降低系统吞吐量。因为每次锁的获取都要检查等待队列,这可能导致线程频繁地进入和退出等待状态,在高并发场景下性能不如非公平锁。
实际代码案例
使用 synchronized 预防死锁
public class SynchronizedDeadlockPrevention {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
}
}
});
thread1.start();
thread2.start();
}
}
上述代码中,两个线程按照相同顺序获取锁,避免了死锁。但如果线程 1 先获取 lock1,线程 2 先获取 lock2,然后各自尝试获取对方已持有的锁,就会发生死锁。由于 synchronized 无法中断,死锁一旦发生难以解决。
使用 ReentrantLock 预防死锁
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDeadlockPrevention {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
boolean gotLock1 = false;
boolean gotLock2 = false;
try {
lock1.lockInterruptibly();
gotLock1 = true;
System.out.println("Thread 1 acquired lock1");
Thread.sleep(100);
lock2.lockInterruptibly();
gotLock2 = true;
System.out.println("Thread 1 acquired lock2");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (gotLock2) {
lock2.unlock();
}
if (gotLock1) {
lock1.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
boolean gotLock1 = false;
boolean gotLock2 = false;
try {
lock1.lockInterruptibly();
gotLock1 = true;
System.out.println("Thread 2 acquired lock1");
Thread.sleep(100);
lock2.lockInterruptibly();
gotLock2 = true;
System.out.println("Thread 2 acquired lock2");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (gotLock2) {
lock2.unlock();
}
if (gotLock1) {
lock1.unlock();
}
}
});
thread1.start();
thread2.start();
}
}
上述代码中,使用 ReentrantLock 的 lockInterruptibly() 方法,线程在获取锁的过程中可以被中断。如果发生死锁倾向(例如等待锁时间过长),可以通过中断线程来打破死锁。同时,在 finally 块中确保锁的正确释放,避免死锁。如果使用公平锁,可通过 ReentrantLock lock = new ReentrantLock(true);
来构造公平锁,减少因锁获取顺序不当导致的死锁风险。