MST

星途 面试题库

面试题:Java分布式多线程编程中的锁优化策略

在一个高并发的Java分布式系统中,多线程频繁竞争锁资源,导致系统性能瓶颈。请详细说明你会采取哪些锁优化策略来提升系统性能,例如锁粗化、锁消除、读写锁应用等,并结合具体场景解释如何应用这些策略。
16.0万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

锁粗化

  1. 原理:将多个连续的加锁、解锁操作合并为一次,扩大锁的作用范围。
  2. 应用场景:当一系列的操作都对同一个对象进行加锁操作,且这些操作之间不存在共享数据竞争时适用。例如在一个方法中,多次对同一对象进行简单的操作,如下代码:
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");
        }
    }
}

这样减少了锁的获取和释放次数,提升性能。

锁消除

  1. 原理:JVM检测到某些加锁操作不可能存在竞争,从而消除这些锁操作。
  2. 应用场景:在一些局部变量的操作中,如果这些变量不会被其他线程访问,即使加了锁,JVM也会优化掉。例如:
public class LockEliminationExample {
    public void doWork() {
        Object obj = new Object();
        synchronized (obj) {
            // 只在本方法内使用obj,没有其他线程访问
            System.out.println("在锁内的操作");
        }
    }
}

JVM可能会检测到obj只在该方法内使用,没有线程竞争,从而消除掉synchronized块。

读写锁应用

  1. 原理:读写锁(ReadWriteLock)分为读锁和写锁,允许多个线程同时读,但只允许一个线程写,且写时不能读。读锁是共享锁,写锁是排他锁。
  2. 应用场景:适用于读多写少的场景。比如一个缓存系统,大量线程需要读取缓存数据,偶尔有线程更新缓存。如下代码:
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();
        }
    }
}

多个线程可以同时获取读锁进行读取操作,而写操作时获取写锁,保证数据一致性,提高了系统并发性能。

锁的粒度控制

  1. 原理:尽量减小锁的粒度,只对共享数据部分加锁,而不是对整个方法或大的代码块加锁。
  2. 应用场景:在一个包含多个操作的方法中,只有部分操作涉及共享数据时。例如一个订单处理系统,处理订单的方法中有部分操作涉及更新订单状态(共享数据),部分操作是计算订单金额(不涉及共享数据)。
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;
    }
}

这样避免了对整个方法加锁,减少了线程等待时间。

偏向锁与轻量级锁

  1. 原理
    • 偏向锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
    • 轻量级锁:当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级时,会尝试使用轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  2. 应用场景
    • 偏向锁:适用于锁基本总是由同一个线程获取的场景,比如单线程环境下的一些同步操作,或者多线程但大部分时间是同一个线程在访问同步资源的场景。例如一个单线程定时任务中,对一些资源进行同步访问,开启偏向锁能减少不必要的锁竞争开销。
    • 轻量级锁:适用于竞争不是很激烈的多线程场景,线程自旋获取锁的时间较短,使用轻量级锁能避免重量级锁带来的线程上下文切换开销。例如在一个高并发但每次锁竞争时间较短的Web应用中,一些对资源的同步访问可以使用轻量级锁优化。可以通过JVM参数-XX:+UseBiasedLocking开启偏向锁,在JDK 6以后默认是开启的。对于轻量级锁,JVM会根据运行情况自动进行偏向锁、轻量级锁和重量级锁之间的转换。