MST
星途 面试题库

面试题:Java多线程编程下,针对高并发场景中锁的性能调优策略有哪些?

在高并发的Java多线程应用中,锁的使用不当会严重影响性能。请详细说明乐观锁与悲观锁的原理及应用场景,并且分析在不同业务场景下,如何选择合适的锁机制,以及如何对锁进行优化,例如锁粗化、锁消除等技术的应用场景和实现方式。
22.3万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

乐观锁与悲观锁原理

  1. 乐观锁原理: 乐观锁认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测。如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。实现方式通常是使用版本号(version)或者时间戳(timestamp)。在读取数据时,将版本号一同读出,数据更新时,将版本号加一。然后和数据库表中的版本号进行比较,如果一致则更新成功,否则重试。
  2. 悲观锁原理: 悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。在Java中,synchronized关键字和ReentrantLock等都是悲观锁的实现。

应用场景

  1. 乐观锁应用场景: 适用于读多写少的场景。比如电商系统中的商品库存读操作远多于写操作,在库存扣减时可以使用乐观锁。因为读操作不会加锁,所以不会影响读性能,而写操作即使发生冲突,重试的概率也相对较低,不会对系统性能造成太大影响。
  2. 悲观锁应用场景: 适用于写多读少的场景。例如银行转账操作,涉及到资金的变更,数据一致性要求非常高,此时使用悲观锁可以保证在任何时刻只有一个线程能进行转账操作,避免数据不一致问题。

锁机制的选择

  1. 读多写少场景:优先选择乐观锁。因为读操作不加锁,能提高系统的并发读性能。同时,由于写操作冲突概率低,即使发生冲突重试也不会对性能造成严重影响。
  2. 写多读少场景:优先选择悲观锁。悲观锁能保证数据的强一致性,在写操作频繁的情况下,使用乐观锁可能导致大量的写操作重试,从而降低系统性能。

锁优化技术

  1. 锁粗化
    • 应用场景:当一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会频繁地进行加锁和解锁操作,增加系统开销。锁粗化就是将多次的锁操作扩展成一次范围更大的锁操作,减少加锁解锁的次数。
    • 实现方式:在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++) {
                // 执行一些操作
            }
        }
    }
}
  1. 锁消除
    • 应用场景:当编译器检测到不可能存在竞争的锁操作时,会将这些锁操作消除。例如在一些只在单线程环境下执行的代码块中,虽然有加锁操作,但实际上不会发生竞争,这种情况下就可以进行锁消除。
    • 实现方式:同样是在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编译器会检测到这里的锁操作不会有竞争,从而将锁消除,优化性能。