面试题答案
一键面试锁粗化
- 原理:将多个连续的加锁、解锁操作合并为一次,扩大锁的作用范围。
- 应用场景:当一系列的操作都对同一个对象进行加锁操作,且这些操作之间不存在共享数据竞争时适用。例如在一个方法中,多次对同一对象进行简单的操作,如下代码:
public class LockCoarseningExample {
private static final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// 操作1
System.out.println("操作1");
}
synchronized (lock) {
// 操作2
System.out.println("操作2");
}
}
}
可优化为:
public class LockCoarseningOptimized {
private static final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// 操作1
System.out.println("操作1");
// 操作2
System.out.println("操作2");
}
}
}
这样减少了锁的获取和释放次数,提升性能。
锁消除
- 原理:JVM检测到某些加锁操作不可能存在竞争,从而消除这些锁操作。
- 应用场景:在一些局部变量的操作中,如果这些变量不会被其他线程访问,即使加了锁,JVM也会优化掉。例如:
public class LockEliminationExample {
public void doWork() {
Object obj = new Object();
synchronized (obj) {
// 只在本方法内使用obj,没有其他线程访问
System.out.println("在锁内的操作");
}
}
}
JVM可能会检测到obj
只在该方法内使用,没有线程竞争,从而消除掉synchronized
块。
读写锁应用
- 原理:读写锁(ReadWriteLock)分为读锁和写锁,允许多个线程同时读,但只允许一个线程写,且写时不能读。读锁是共享锁,写锁是排他锁。
- 应用场景:适用于读多写少的场景。比如一个缓存系统,大量线程需要读取缓存数据,偶尔有线程更新缓存。如下代码:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private final Map<String, Object> cache = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public Object get(String key) {
lock.readLock().lock();
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
public void put(String key, Object value) {
lock.writeLock().lock();
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}
多个线程可以同时获取读锁进行读取操作,而写操作时获取写锁,保证数据一致性,提高了系统并发性能。
锁的粒度控制
- 原理:尽量减小锁的粒度,只对共享数据部分加锁,而不是对整个方法或大的代码块加锁。
- 应用场景:在一个包含多个操作的方法中,只有部分操作涉及共享数据时。例如一个订单处理系统,处理订单的方法中有部分操作涉及更新订单状态(共享数据),部分操作是计算订单金额(不涉及共享数据)。
public class OrderProcessor {
private static final Object statusLock = new Object();
private Order order;
public void processOrder() {
// 计算订单金额,无需加锁
double amount = calculateAmount();
synchronized (statusLock) {
// 更新订单状态,加锁
order.setStatus(OrderStatus.PROCESSED);
}
}
private double calculateAmount() {
// 计算逻辑
return 0;
}
}
这样避免了对整个方法加锁,减少了线程等待时间。
偏向锁与轻量级锁
- 原理:
- 偏向锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
- 轻量级锁:当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级时,会尝试使用轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 应用场景:
- 偏向锁:适用于锁基本总是由同一个线程获取的场景,比如单线程环境下的一些同步操作,或者多线程但大部分时间是同一个线程在访问同步资源的场景。例如一个单线程定时任务中,对一些资源进行同步访问,开启偏向锁能减少不必要的锁竞争开销。
- 轻量级锁:适用于竞争不是很激烈的多线程场景,线程自旋获取锁的时间较短,使用轻量级锁能避免重量级锁带来的线程上下文切换开销。例如在一个高并发但每次锁竞争时间较短的Web应用中,一些对资源的同步访问可以使用轻量级锁优化。可以通过JVM参数
-XX:+UseBiasedLocking
开启偏向锁,在JDK 6以后默认是开启的。对于轻量级锁,JVM会根据运行情况自动进行偏向锁、轻量级锁和重量级锁之间的转换。