Lock
接口下的锁是基于AQS实现的显式锁。具体有ReentrantLock
、ReentrantReadWriteLock.ReadLock
、ReentrantReadWriteLock.WriteLock
。相对于synchronized
隐式锁,这些锁更灵活。
ReentrantLock
1 | public class ReentrantLock implements Lock, java.io.Serializable |
介绍
实现了Lock接口,是一个显示可重入锁,并且提供了公平锁和非公平锁的实现。
非公平锁 new ReentrantLock();
lock()
非公平锁获取锁的过程:
- 首先会尝试将锁状态
state
通过CAScompareAndSetState(0, 1)
设置为独占模式(修改为1)- 如果修改成功,将会调用
setExclusiveOwnerThread(Thread.currentThread())
将独占线程变量exclusiveOwnerThread
指向当前线程 - 如果修改失败,将会调用
acquire(1)
尝试重新获取锁。原因是可能当前线程之前通过lock()
已经获取到锁了,现在又调用lock()
,所以会失败,所以会判断独占锁的线程是否是当前线程。如果不是的话或者重试以后还是没有获取到锁就会将当前线程封装成一个队列节点放到阻塞队列中,挂起当前线程。
- 如果修改成功,将会调用
1 | final void lock() { |
首先会再调用tryAcquire(arg)
尝试通过CAS获取锁
- 如果获取成功,直接返回
- 如果获取失败,就会调用
addWaiter(Node.EXCLUSIVE), arg)
创建队列节点
1 | public final void acquire(int arg) { |
将当前线程封装成队列节点,插入到队列尾部
1 | private Node addWaiter(Node mode) { |
插入到队列以后,根据当前线程节点的前驱节点状态waitstatus
决定当前线程是否应该被阻塞
1 |
|
封装当前线程的节点会根据它前驱节点的状态判断当前线程是否应该被阻塞
- 如果当前线程节点的前驱节点状态是SIGNAL(-1),说明已经可以保证它的前驱节点在释放锁时唤醒它,所以当前线程可以安全的被阻塞了…
- 如果当前线程节点的前驱节点状态大于0,说明前驱节点封装的线程被取消了,应该从队列中移除,然后判断前驱节点的前驱节点… 直到找到状态为小于0的节点
- 如果当前线程节点的前驱节点状态等于0,说明前驱节点里的线程已经被阻塞了,此时会将前驱节点的状态改为SIGNAL,然后返回true,代表应该阻塞当前线程
1 | private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { |
挂起当前线程。当此线程被唤醒时判断线程的中断状态,如果线程被中断过,就通过
1 | if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; |
重新设置线程的中断状态
1 | private final boolean parkAndCheckInterrupt() { |
unlock()
首先将锁计数state
减1
- 如果锁计数
state
为0了,则释放当前锁,唤醒头节点的下一节点中封装的线程 - 如果锁计数
state
不为0,则不释放当前锁
1 | public void unlock() { |
tryLock()
如果获取不到锁,直接返回false
,不会阻塞当前线程。
在tryLock()
中尝试获取一次锁,如果没有成功,就不会再去获取或者阻塞当前线程,而是返回false
如果获取不到
1 | public boolean tryLock() { |
tryLock(long timeout, TimeUnit unit)
超时获取锁:在指定时间段内timeout
会一直尝试获取锁,直到获取锁返回true
,如果一直没有获取锁,超时以后会返回false
1 | public boolean tryLock(long timeout, TimeUnit unit) |
lockInterruptibly()
支持中断锁:如果一个线程在获取锁的过程中被中断了,会抛出InterruptedException。
Java Doc:
If the current thread:
has its interrupted status set on entry to this method; or
is interrupted while acquiring the lock,
then InterruptedException is thrown and the current thread’s interrupted status is cleared.
1 | public void lockInterruptibly() throws InterruptedException { |
公平锁 new ReentrantLock(true);
lock()
公平锁获取锁的过程:
- 首先会根据锁状态
state
来判断是否应该获取锁,而不是直接像非公平锁那样,直接尝试通过CAScompareAndSetState(0, 1)
设置为独占模式(修改为1)来获取锁。- 如果此时可以获取锁(
state
为0),会进一步判断等待队列中是否存在等待获取锁的线程(和非公平锁的区别)- 如果此时队列中有等待线程,就会直接挂起当前线程。
- 如果此时队列中没有等待线程,就会尝试通过CAS
compareAndSetState(0, acquires)
去尝试获取锁。- 如果修改成功,将会调用
setExclusiveOwnerThread(Thread.currentThread())
将独占线程变量exclusiveOwnerThread
指向当前线程 - 如果修改失败,将会调用
acquire(1)
- 如果修改成功,将会调用
- 如果此时不可以获取锁(
state
不为0),直接挂起当前线程。
- 如果此时可以获取锁(
注意:公平锁和非公平锁都重写了
tryAcquire(int acquires)
1 | final void lock() { |
Q&A
公平锁和非公平锁区别?
- 处于非公平锁的线程直接尝试获取锁,不会去判断等待队列中是否已经有线程正在等待锁的释放。非公平锁会导致线程饥饿现象
- 处于公平锁模式的线程按照请求锁的顺序决定获取锁的顺序(加锁时会判断等待队列中是否已经有线程正在等待锁,如果有的话就不会再去获取锁而是插到队列尾部)。
ReentrantLock和synchronized的区别?
- 在用法上
- ReentrantLock需要显示的获取锁和释放锁。(注意:为了安全,需要在finally中释放锁)。而synchronized会自动地获取锁和释放锁
- ReentrantLock通过Condition实现了唤醒线程和使当前线程睡眠的API,没有使用Object的API。
- ReentrantLock只能修饰代码块,并不能修饰方法。而synchronized既可以修饰方法也可以修饰代码块
- 在特性上
- ReentrantLock可以以非阻塞的形式来获取锁,如果没有获取锁,会返回false,而不会阻塞当前线程。 synchronized如果没有获取到锁,会阻塞当前线程
- ReentrantLock可以以超时的形式来获取锁,如果在指定时间段内没有获取到锁,直接返回false,不会阻塞当前线程。
- ReentrantLock实现了公平锁,防止了非公平锁可能产生的线程饥饿现象。
- ReentrantLock实现了以中断形式来获取锁,如果线程在获取锁的过程中,被中断了,会直接抛出异常,不会再去尝试获取锁。