一、为什么需要锁优化技术
在Java世界里,高并发场景就像节假日的高速公路收费站,当大量车辆(线程)同时到达时,如果没有合理的调度机制,很容易出现拥堵(线程阻塞)。JVM的锁机制就像收费站的ETC通道,设计得好能大幅提升通行效率,设计不好反而会成为性能瓶颈。
举个实际例子,我们有个电商秒杀系统,在最初的实现中直接使用了synchronized关键字:
public class SeckillService {
private int stock = 100;
// 原始同步方法
public synchronized boolean seckill() {
if (stock > 0) {
stock--;
return true;
}
return false;
}
}
这个实现虽然线程安全,但当并发量达到5000QPS时,系统吞吐量直线下降,响应时间飙升到2秒以上。这就是典型的"一把大锁保平安"带来的性能问题。
二、JVM内置的锁优化技术
2.1 偏向锁:给单线程开绿灯
偏向锁就像公司门禁的人脸识别系统,当发现总是同一个人(线程)进出时,就直接放行不再检查。JVM也是类似的思路:
public class BiasLockExample {
private static final Object lock = new Object();
public static void main(String[] args) {
// 第一次加锁会启用偏向锁
synchronized (lock) {
System.out.println("第一次获取锁");
}
// 同一个线程再次获取锁时直接通过
synchronized (lock) {
System.out.println("第二次获取锁");
}
}
}
偏向锁在无竞争时能省去CAS操作的开销,但一旦有第二个线程尝试获取锁,就会立即升级为轻量级锁。
2.2 轻量级锁:线程间的友好竞争
当出现轻度竞争时,JVM会使用轻量级锁机制。这就像几个同事轮流使用会议室,通过简单的登记就能协调:
public class LightweightLockExample {
private int count = 0;
public void increment() {
// 这里会尝试使用轻量级锁
synchronized (this) {
count++;
}
}
}
轻量级锁基于CAS操作和线程栈中的Lock Record实现,当CAS失败时说明竞争激烈,此时会升级为重量级锁。
2.3 锁消除:JVM的智能判断
有时候JVM能通过逃逸分析发现某些锁根本不需要:
public class LockElimination {
public String concat(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
// StringBuffer是线程安全的,但这里JVM会发现sb不会逃逸出方法
// 实际会消除锁开销
sb.append(s1).append(s2).append(s3);
return sb.toString();
}
}
2.4 锁粗化:合并相邻的同步块
当JVM发现一连串的加锁解锁操作时,会尝试合并:
public class LockCoarsening {
public void process() {
// 原本多个同步块
synchronized (this) { doSomething(); }
synchronized (this) { doSomethingElse(); }
synchronized (this) { finish(); }
// 优化后可能合并为一个同步块
synchronized (this) {
doSomething();
doSomethingElse();
finish();
}
}
}
三、应用层锁优化技巧
3.1 减小锁粒度:化整为零
把一个大锁拆分成多个小锁,就像把一个大仓库分成多个小储物柜:
public class FineGrainedLock {
// 使用多个锁而不是一个全局锁
private final Object[] locks = new Object[16];
private final int[] counts = new int[16];
public FineGrainedLock() {
for (int i = 0; i < locks.length; i++) {
locks[i] = new Object();
}
}
public void increment(int bucket) {
// 只锁定需要的部分
synchronized (locks[bucket % locks.length]) {
counts[bucket % locks.length]++;
}
}
}
3.2 读写分离:读多写少的场景
使用ReadWriteLock可以大幅提升读多写少场景的性能:
public class ReadWriteCache {
private final Map<String, Object> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
3.3 乐观锁:CAS的妙用
对于冲突较少的情况,乐观锁往往更高效:
public class OptimisticLockExample {
private AtomicInteger version = new AtomicInteger(0);
private String data;
public void update(String newData) {
int currentVersion = version.get();
// 模拟其他操作
try { Thread.sleep(10); } catch (InterruptedException e) {}
// 乐观更新
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
data = newData;
} else {
// 处理冲突
System.out.println("更新冲突,请重试");
}
}
}
四、实战分析与选型建议
4.1 应用场景分析
- 偏向锁:适合明确知道只会单线程访问的场景
- 轻量级锁:适合短时间、低竞争的同步块
- 读写锁:适合读多写少的缓存场景
- 分段锁:适合可以分片处理的数据结构
- 乐观锁:适合冲突率低的并发更新场景
4.2 技术优缺点对比
| 技术 | 优点 | 缺点 |
|---|---|---|
| 偏向锁 | 单线程零开销 | 撤销时有性能损耗 |
| 轻量级锁 | 竞争小时开销低 | 自旋消耗CPU |
| 重量级锁 | 竞争大时稳定 | 上下文切换开销大 |
| 读写锁 | 读并发高 | 写操作可能饿死 |
| 乐观锁 | 无阻塞 | 冲突时需要重试 |
4.3 注意事项
- 不要过度优化,先证明锁确实是瓶颈
- 注意锁的公平性问题,避免线程饿死
- 小心死锁,尽量按固定顺序获取多个锁
- 考虑锁的可重入性需求
- 监控锁竞争情况,使用JVisualVM等工具
4.4 总结
JVM锁优化就像交通管理,需要根据不同的车流量(并发量)采取不同的调度策略。理解各种锁的特性和适用场景,才能在高并发环境下写出既安全又高效的代码。记住,没有最好的锁,只有最合适的锁。
评论