显式锁的分类
显式锁 有很多种,从不同的角度来看,显式锁大概有以下几种分类
- 可重入锁 和 不可重入锁
- 悲观锁 和 乐观锁
- 共享锁 和 独占锁
- 可中断锁 和 不可中断锁
- 公平锁 和 非公平锁
1.可重入锁(Reentrant Lock)与不可重入锁
可重入锁: 可重入锁允许同一个线程多次获取同一把锁而不被阻塞,即在已经持有锁的情况下再次尝试获取该锁时仍能成功。Java中的ReentrantLock
就是一个典型的可重入锁实现,它内部维护一个计数器来记录锁被重入的次数,每次成功获取锁时计数器加1,释放锁时计数器减1,直到计数器为0时锁才真正释放。这种设计允许递归调用和嵌套锁的场景,避免了自我死锁。
不可重入锁: 不可重入锁在设计上不允许一个线程重复获取同一把锁,一旦锁被某线程获取,其他线程包括持有锁的线程再次尝试获取时都会被阻塞。在Java标准库中并没有直接提供不可重入锁的实现,因为可重入锁是更为实用且安全的选择。
2.悲观锁(Pessimistic Lock)与乐观锁(Optimistic Lock)
悲观锁: 悲观锁假设最坏的情况,认为每次访问共享资源都可能发生冲突,因此在访问数据前先锁定资源,阻止其他线程访问。这通常通过独占锁实现,例如在数据库中通过行锁或表锁。悲观锁会减少并发性能,但能确保数据的一致性。
乐观锁: 乐观锁则持乐观态度,假定读多写少,大多数情况下不会有并发冲突,仅在提交更新时检查数据是否被其他线程修改过。乐观锁通常通过版本号或时间戳来实现,如果数据在更新前已被其他线程修改,则更新失败并可能重新尝试。乐观锁降低了锁的竞争开销,适合读多写少的场景。
3.共享锁(Shared Lock)与独占锁(Exclusive Lock)
共享锁: 共享锁允许多个线程同时持有锁,即多个读取者可以同时访问共享资源。它适用于读操作较多的场景,能够提升并发度。Java中的ReentrantReadWriteLock
中的读锁(ReadLock
)就是共享锁的典型例子,它允许多个读锁共存,但排斥写锁。
独占锁: 独占锁(排他锁)在同一时间只允许一个线程持有,无论是读操作还是写操作。synchronized
关键字、ReentrantLock
和ReentrantReadWriteLock
中的写锁(WriteLock
)都是独占锁的实现。独占锁确保了在任一时刻只有一个线程可以修改共享资源,避免了并发写冲突。
4.可中断锁与不可中断锁
可中断锁: 可中断锁允许等待锁的线程在等待期间响应中断信号,通过抛出InterruptedException
中断线程的等待状态,从而可以更灵活地控制线程的生命周期。ReentrantLock
的lockInterruptibly()
方法就提供了可中断的锁获取行为。
不可中断锁: 不可中断锁在等待获取锁的过程中不会响应中断请求,一旦开始等待就会一直等到获取到锁或者其它原因导致等待结束。Java中的synchronized
块或方法以及ReentrantLock
的lock()
方法默认表现为不可中断锁,这可能导致线程长时间阻塞而无法响应外部中断。
5.公平锁(Fair Lock) 和 非公平锁(Nonfair Lock)
公平锁:公平锁遵循严格的FIFO(先进先出)原则,即线程按照请求锁的顺序来获取锁。如果一个线程已经在等待队列中,那么它会优先于新来的线程获得锁,即使新线程先尝试获取锁。
非公平锁:非公平锁不保证线程获取锁的顺序,新来的线程有可能直接抢占正在等待的线程而获得锁。线程在尝试获取锁时,会直接尝试获取,如果此时锁未被占用,则直接获得锁,否则才会进入等待队列。