您的位置:

Java锁面试题详解

一、锁的概念与种类

锁通常用于保证多线程访问共享资源的安全性,它能有效地避免资源竞争,确保多线程之间的数据同步。Java中的锁可以分为以下几种:

1. synchronized锁

public synchronized void method() {
    // critical section
}

使用synchronized关键字修饰的方法或代码块,即可将其变成同步方法(或同步代码块)。它是Java中最基本的锁实现方式。

2. ReentrantLock锁

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();
}

ReentrantLock是一个可重入锁,支持公平性和非公平性操作,并提供了比synchronized更加灵活的锁控制。

3. ReadWriteLock锁

ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
    // read
} finally {
    lock.readLock().unlock();
}
lock.writeLock().lock();
try {
    // write
} finally {
    lock.writeLock().unlock();
}

ReadWriteLock允许多个线程同时读取共享资源,但只能由一个线程修改资源。因此,它的读写锁分别有readLock和writeLock两种。

二、死锁与活锁

1. 死锁

死锁是指多个线程在获取锁的过程中,由于互相等待对方释放锁而陷入阻塞的状态,无法继续执行下去。如下代码就存在死锁的风险:

public class Deadlock {
    private static final Object obj1 = new Object();
    private static final Object obj2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (obj1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj2) {
                    System.out.println("Thread1 finished");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (obj2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1) {
                    System.out.println("Thread2 finished");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

以上代码中,t1和t2先后获取到obj1和obj2,但又同时等待对方释放锁,从而导致死锁的产生。

2. 活锁

活锁是指多个线程在获取锁的过程中,由于过度地释放锁或者过度地交换执行权,导致线程一直在重复执行同一段代码,无法继续向前执行。

三、锁的优化

1. 锁的粗化

锁的粗化是指将一些连续的同步操作拼接在一起,减少线程竞争的次数。比如以下代码:

public synchronized void method() {
    // step1
    // step2
    // step3
    // step4
}

以上代码中,step1 ~ step4是一系列的同步操作,如果不进行粗化处理,则在每个步骤之间都需要获取和释放锁,造成较大的时间和性能消耗。可以将代码粗化,将所有同步操作都放在一个同步块中:

public void method() {
    synchronized (this) {
        // step1
        // step2
        // step3
        // step4
    }
}

2. 锁的细化

锁的细化是指将原本的锁拆解成多个锁,从而减少竞争的粒度。比如以下代码:

public synchronized void method() {
    // step1
    // step2
    // step3
    // step4
}

以上代码中,如果step1 ~ step4之间存在比较重的计算操作,会导致该方法在执行时无法同时处理其它的请求。可以将锁进行细化,将步骤拆解出来,从而减少竞争的粒度:

private final Object lock1 = new Object();
private final Object lock2 = new Object();
private final Object lock3 = new Object();
private final Object lock4 = new Object();

public void method() {
    synchronized (lock1) {
        // step1
    }
    synchronized (lock2) {
        // step2
    }
    synchronized (lock3) {
        // step3
    }
    synchronized (lock4) {
        // step4
    }
}

3. 乐观锁

乐观锁是一种无锁的实现方式,它假设并发的冲突非常少,并通过CAS操作来检查和更新数据。对于不频繁的冲突,乐观锁可以在多线程并发中提供良好的性能和吞吐量。Java中的Atomic类就是乐观锁的一种实现方式。

四、锁的使用场景

1. 高并发访问的共享资源

在高并发访问下,共享资源容易被多个线程同时访问,导致数据不一致等问题。这时可以使用锁来保证数据的一致性。例如收银台模拟程序中,多个售票员同时向一个账户扣款,需要使用锁保证操作的原子性。

2. 多线程间的通信

在多线程间的通信中,常常需要一个线程等待另一个线程执行完毕,在该线程继续执行。这时可以使用锁来实现线程之间的互斥和同步。例如生产者和消费者模型中,需要用锁机制来实现对共享资源(缓冲区)的同步访问。

3. 避免数据竞争

数据竞争是指当两个或多个线程同时访问某个变量,并且至少其中一个线程对变量进行了写操作,此时可能会导致变量的值不确定性,出现严重错误。这时可以使用锁来避免数据竞争的产生。

五、案例演示

以下是一个使用ReentrantLock锁的示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public int incrementAndGet() {
        lock.lock();
        try {
            Thread.sleep(100); // 模拟长时间计算
            return ++count;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Runnable runnable = () -> {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + counter.incrementAndGet());
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count: " + counter.count);
    }
}

运行以上代码,可以看到输出结果:

Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-1: 4
Thread-1: 5
Thread-1: 6
count: 6

以上代码中,counter是一个计数器,在incrementAndGet方法中使用了ReentrantLock锁,来保证count变量的线程安全。在main方法中,开启两个线程同时访问incrementAndGet方法,从而验证锁的效果。