面试题答案
一键面试乐观锁与悲观锁原理
- 乐观锁原理: 乐观锁认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测。如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。实现方式通常是使用版本号(version)或者时间戳(timestamp)。在读取数据时,将版本号一同读出,数据更新时,将版本号加一。然后和数据库表中的版本号进行比较,如果一致则更新成功,否则重试。
- 悲观锁原理: 悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。在Java中,synchronized关键字和ReentrantLock等都是悲观锁的实现。
应用场景
- 乐观锁应用场景: 适用于读多写少的场景。比如电商系统中的商品库存读操作远多于写操作,在库存扣减时可以使用乐观锁。因为读操作不会加锁,所以不会影响读性能,而写操作即使发生冲突,重试的概率也相对较低,不会对系统性能造成太大影响。
- 悲观锁应用场景: 适用于写多读少的场景。例如银行转账操作,涉及到资金的变更,数据一致性要求非常高,此时使用悲观锁可以保证在任何时刻只有一个线程能进行转账操作,避免数据不一致问题。
锁机制的选择
- 读多写少场景:优先选择乐观锁。因为读操作不加锁,能提高系统的并发读性能。同时,由于写操作冲突概率低,即使发生冲突重试也不会对性能造成严重影响。
- 写多读少场景:优先选择悲观锁。悲观锁能保证数据的强一致性,在写操作频繁的情况下,使用乐观锁可能导致大量的写操作重试,从而降低系统性能。
锁优化技术
- 锁粗化:
- 应用场景:当一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会频繁地进行加锁和解锁操作,增加系统开销。锁粗化就是将多次的锁操作扩展成一次范围更大的锁操作,减少加锁解锁的次数。
- 实现方式:在JVM层面,JIT编译器会在运行时进行锁粗化优化。例如,对于如下代码:
public class LockCoarsening {
private final Object lock = new Object();
public void method() {
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
// 执行一些操作
}
}
}
}
JIT编译器可能会将其优化为:
public class LockCoarsening {
private final Object lock = new Object();
public void method() {
synchronized (lock) {
for (int i = 0; i < 1000; i++) {
// 执行一些操作
}
}
}
}
- 锁消除:
- 应用场景:当编译器检测到不可能存在竞争的锁操作时,会将这些锁操作消除。例如在一些只在单线程环境下执行的代码块中,虽然有加锁操作,但实际上不会发生竞争,这种情况下就可以进行锁消除。
- 实现方式:同样是在JVM层面由JIT编译器完成。例如,如下代码:
public class LockElimination {
public void method() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
}
}
在JDK 1.5之后,StringBuffer的append方法是同步的,但在单线程环境下,JIT编译器会检测到这里的锁操作不会有竞争,从而将锁消除,优化性能。