MST

星途 面试题库

面试题:在高并发Java系统中,如何预防死锁并优化锁的使用

在一个高并发的Java系统中,既要保证数据的一致性和线程安全,又要尽可能避免死锁并提高系统性能。请详细阐述你在设计锁机制方面的策略,包括如何选择合适的锁类型(如乐观锁、悲观锁、读写锁等),如何确定锁的粒度,以及如何使用Java提供的并发包(如java.util.concurrent)中的工具来优化锁的使用,给出具体的代码示例说明。
42.9万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

锁类型选择

  1. 乐观锁
    • 适用场景:适用于读多写少的场景,因为乐观锁假设在大部分情况下数据不会发生冲突,只有在更新数据时才会检查是否有冲突。
    • 实现方式:通常使用版本号(version)或者时间戳(timestamp)来实现。在读取数据时,同时获取数据的版本号,在更新数据时,将当前版本号与数据库中存储的版本号进行比较,如果相同则更新数据并将版本号加1,否则重试操作。
    • 代码示例
// 假设这是数据库中的数据对象
class DataObject {
    private int value;
    private int version;

    public DataObject(int value) {
        this.value = value;
        this.version = 0;
    }

    public int getValue() {
        return value;
    }

    public int getVersion() {
        return version;
    }

    public void setValue(int value, int expectedVersion) {
        if (this.version == expectedVersion) {
            this.value = value;
            this.version++;
        } else {
            throw new RuntimeException("Version conflict");
        }
    }
}

// 模拟更新操作
public class OptimisticLockExample {
    public static void main(String[] args) {
        DataObject data = new DataObject(10);
        int expectedVersion = data.getVersion();
        int newValue = 20;
        try {
            data.setValue(newValue, expectedVersion);
            System.out.println("Update successful, new value: " + data.getValue());
        } catch (RuntimeException e) {
            System.out.println("Update failed: " + e.getMessage());
        }
    }
}
  1. 悲观锁
    • 适用场景:适用于写多读少的场景,悲观锁认为数据在被访问时很可能会发生冲突,所以在访问数据前就加锁。
    • 实现方式:在Java中,synchronized关键字就是一种悲观锁的实现。当一个线程进入synchronized块时,会获取对象的锁,其他线程无法进入,直到该线程释放锁。
    • 代码示例
class PessimisticLockExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) {
        PessimisticLockExample example = new PessimisticLockExample();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final count: " + example.getCount());
    }
}
  1. 读写锁
    • 适用场景:适用于读多写少的场景,读写锁允许多个线程同时进行读操作,但只允许一个线程进行写操作,并且写操作时不允许读操作。
    • 实现方式:在Java中,可以使用ReentrantReadWriteLock来实现读写锁。
    • 代码示例
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadWriteLockExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private int data = 0;

    public void read() {
        lock.readLock().lock();
        try {
            System.out.println("Reading data: " + data);
        } finally {
            lock.readLock().unlock();
        }
    }

    public void write(int newData) {
        lock.writeLock().lock();
        try {
            data = newData;
            System.out.println("Writing data: " + data);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();
        Thread readThread1 = new Thread(() -> example.read());
        Thread readThread2 = new Thread(() -> example.read());
        Thread writeThread = new Thread(() -> example.write(10));
        readThread1.start();
        readThread2.start();
        writeThread.start();
    }
}

锁粒度确定

  1. 粗粒度锁
    • 特点:对整个资源或者较大范围的资源加锁。优点是实现简单,缺点是并发性能差,因为同一时间只有一个线程能访问整个资源,可能导致其他线程长时间等待。
    • 适用场景:适用于资源较少且访问频率不高的场景,例如在系统启动时初始化一些全局配置信息,对全局配置对象加一个粗粒度锁即可。
  2. 细粒度锁
    • 特点:对资源进行细分,每个小部分都可以独立加锁。优点是并发性能好,因为不同线程可以同时访问不同部分的资源。缺点是实现复杂,可能会因为锁的数量过多而增加管理成本,并且可能会增加死锁的风险。
    • 适用场景:适用于资源较多且访问频率高的场景,例如在一个大型的缓存系统中,可以对每个缓存分区使用细粒度锁,不同线程可以同时操作不同的缓存分区。

使用Java并发包优化锁的使用

  1. 使用ConcurrentHashMap替代HashMap
    • 原因HashMap不是线程安全的,在高并发环境下使用需要手动加锁,而ConcurrentHashMap是线程安全的,并且采用了分段锁的机制,允许多个线程同时访问不同的段,提高了并发性能。
    • 代码示例
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);
        map.put("key2", 2);
        System.out.println(map.get("key1"));
    }
}
  1. 使用CountDownLatch进行线程同步
    • 作用CountDownLatch可以让一个或多个线程等待其他线程完成操作后再继续执行。
    • 代码示例
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟一些操作
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " has finished");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        try {
            latch.await();
            System.out.println("All threads have finished, main thread can continue");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  1. 使用Semaphore控制并发访问数量
    • 作用Semaphore可以控制同时访问某个资源的线程数量。
    • 代码示例
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permits = 2;
        Semaphore semaphore = new Semaphore(permits);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " has acquired the semaphore");
                    // 模拟一些操作
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " has released the semaphore");
                }
            }).start();
        }
    }
}