锁的用法及常见面试题
2023-12-02 07:56:40
Lock的意义和使用
在多线程编程中,多个线程同时操作共享数据时,可能会引发数据不一致的情况。例如,两个线程同时对同一个变量进行写操作,可能会导致数据的丢失。为了解决这个问题,Java提供了锁(Lock)机制。
锁是一种同步机制,它允许一个线程在访问共享数据时,独占该数据的访问权。这样,其他线程只能等待锁被释放,才能访问共享数据。
使用锁可以保证共享数据的安全性,防止数据被破坏。然而,过多的使用锁也会导致性能下降,因为锁会阻塞其他线程的执行。因此,在使用锁时,需要仔细权衡锁的收益和成本。
同步器AbstractQueuedSynchronizer
AbstractQueuedSynchronizer (AQS)是Java中锁的基础类。AQS提供了锁的实现框架,允许开发人员自定义锁的行为。
AQS使用队列来管理等待获取锁的线程。当一个线程试图获取锁时,如果锁已经被其他线程持有,则该线程会被放入队列中等待。当锁被释放后,AQS会唤醒队列中的第一个线程,并允许该线程获取锁。
重入锁ReentrantLock
ReentrantLock是Java中的一种重入锁。所谓重入锁,就是允许一个线程多次获取同一个锁。这意味着,一个线程可以获取一个锁,然后在释放锁之前再次获取该锁。
ReentrantLock使用计数器来记录锁被获取的次数。当一个线程获取锁时,计数器加一;当一个线程释放锁时,计数器减一。当计数器为零时,锁被释放。
读写锁ReentrantReadWriteLock
ReentrantReadWriteLock是Java中的一种读写锁。读写锁允许多个线程同时读共享数据,但只允许一个线程写共享数据。
ReentrantReadWriteLock有两个锁:一个读锁和一个写锁。当一个线程获取读锁时,其他线程只能获取读锁,不能获取写锁。当一个线程获取写锁时,其他线程不能获取读锁或写锁。
Condition
Condition是一个等待队列,它允许线程等待某个条件满足。当条件满足时,线程会被唤醒并继续执行。
Condition与锁一起使用。当一个线程需要等待某个条件满足时,它可以获取锁,然后调用Condition的await()方法进入等待状态。当条件满足时,其他线程可以调用Condition的signal()或signalAll()方法唤醒等待的线程。
一、Lock的使用
Lock的用法非常简单。首先,创建一个Lock对象。然后,在需要获取锁的代码块之前,使用Lock对象的lock()方法获取锁。最后,在需要释放锁的代码块之后,使用Lock对象的unlock()方法释放锁。
例如,以下代码演示了如何使用Lock来保护一个共享变量:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SharedCounter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
二、常见面试题
-
锁的分类有哪些?
- 可重入锁(ReentrantLock)
- 不可重入锁(synchronized)
- 读写锁(ReentrantReadWriteLock)
- 独占锁(Exclusive Lock)
- 共享锁(Shared Lock)
- 公平锁(Fair Lock)
- 非公平锁(Unfair Lock)
-
什么是死锁?
- 死锁是指两个或多个线程互相等待,导致都无法继续执行的情况。
-
如何避免死锁?
- 避免环形等待
- 避免获取锁的顺序依赖
- 使用超时锁
-
什么是饥饿?
- 饥饿是指一个线程长时间无法获取锁,导致无法执行的情况。
-
如何避免饥饿?
- 使用公平锁
- 使用优先级队列
- 使用锁老化机制
-
什么是活锁?
- 活锁是指两个或多个线程互相抢夺锁,导致都无法继续执行的情况。
-
如何避免活锁?
- 使用锁老化机制
- 使用随机退避算法