在并发编程中,锁是用于控制对共享资源访问的机制。理解不同类型的锁及其特点是实现高效和可靠多线程程序的基础。以下是公平锁、非公平锁、乐观锁、悲观锁的介绍以及如何处理死锁的讨论。
公平锁与非公平锁
公平锁
定义: 公平锁是一种确保线程按照请求的顺序获得锁的机制。它保证了线程在请求锁时的顺序性,通常通过维护一个队列来实现。
特点:
- 公平性: 保证了线程获得锁的顺序与请求的顺序一致,避免了“饥饿”现象。
- 性能开销: 由于需要维护线程队列,可能导致更多的性能开销和线程上下文切换。
- 应用场景: 适用于需要严格保证线程执行顺序的场景,如金融系统、任务调度等。
示例(Java):
ReentrantLock lock = new ReentrantLock(true); // 公平锁
非公平锁
定义: 非公平锁不保证线程按照请求的顺序获得锁,它允许线程在锁释放时“插队”,尝试先获得锁。
特点:
- 性能: 通常具有较好的性能,能够减少锁竞争带来的开销。
- 公平性差: 可能导致某些线程长期无法获得锁,即线程“饥饿”。
- 应用场景: 适用于对性能要求较高且不需要严格公平性的场景,如高性能计算任务。
示例(Java):
ReentrantLock lock = new ReentrantLock(false); // 非公平锁
乐观锁与悲观锁
乐观锁
定义: 乐观锁是一种基于版本机制的锁策略,它假设并发冲突不会频繁发生,因此在操作前不会加锁,而是在提交时检查是否有其他操作修改了数据。
特点:
- 无锁操作: 减少了锁竞争带来的开销,适合读多写少的场景。
- 冲突重试: 如果检测到冲突,操作将会重试,可能会导致性能下降。
- 应用场景: 适用于读操作远多于写操作的场景,如缓存系统、某些数据库操作。
示例:
在数据库中,乐观锁通常通过版本号实现。例如,更新时检查记录的版本号是否一致,如果一致则更新成功,否则重试。
悲观锁
定义: 悲观锁是一种假设并发冲突频繁的锁策略,因此在操作之前会加锁,确保对共享资源的独占访问。
特点:
- 数据一致性: 通过加锁来避免并发冲突,适用于写操作频繁的场景。
- 锁竞争: 可能导致较高的锁竞争,影响性能。
- 应用场景: 适用于写操作多且冲突较多的场景,如银行转账、事务处理等。
示例:
在数据库中,悲观锁通过“锁表”或“锁行”实现。例如,使用 SELECT ... FOR UPDATE
语句来锁定记录,直到事务结束。
死锁
定义: 死锁是指多个进程(或线程)在相互等待对方释放资源时形成的僵局状态,导致所有参与的进程(或线程)都无法继续执行。
死锁的四个条件:
- 互斥条件: 至少有一个资源必须处于排他模式,不能被多个进程(或线程)共享。
- 占有并等待: 已持有资源的进程在等待其他资源时,保持已持有的资源不释放。
- 不剥夺条件: 已获得的资源不能被强行剥夺,只能由持有者自愿释放。
- 循环等待: 存在一种进程(或线程)循环等待的关系,即进程 (P1) 等待进程 (P2) 持有的资源,进程 (P2) 等待进程 (P3) 持有的资源,以此类推,最终进程 (Pn) 等待进程 (P1) 持有的资源。
死锁的预防与避免:
-
死锁预防: 通过破坏死锁的四个条件之一来防止死锁的发生。
- 破坏互斥条件(共享资源)。
- 破坏占有并等待条件(一次性请求所有资源)。
- 破坏不剥夺条件(强行剥夺资源)。
- 破坏循环等待条件(资源排序)。
-
死锁避免: 使用算法(如银行家算法)动态检查资源分配是否会导致死锁。避免进入不安全状态。
-
死锁检测与恢复: 允许系统进入死锁状态,但实时检测并采取措施恢复。
- 资源分配图:用于检测死锁状态。
- 进程回滚:将进程回滚到某个安全状态,释放资源。
- 进程终止:终止一个或多个进程以解除死锁。
示例(Java):
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Holding lock1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Holding lock1 and lock2...");
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("Holding lock2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Holding lock2 and lock1...");
}
}
}
}
在这个例子中,如果两个线程分别执行 method1()
和 method2()
,就会导致死锁,因为每个线程都在等待另一个线程释放其持有的锁。