[TOC]
类结构
首先ReentrantLock
继承自父类Lock
,然后有3
个内部类,其中中有一个抽象内部类Sync继承AQS,两个内部类NonfairSync和FairSync继承了Sync(Sync重写tryRelease),重写了lock()方法和tryAcquire()方法,分别实现了非公平锁和公平锁。
ReentrantLock默认为非公平锁,如果想创建公平锁,可给构造方法传入参数true
1 | public ReentrantLock() { |
ReentrantLock
有两个构造方法,无参构造方法默认是创建非公平锁,而传入true
为参数的构造方法创建的是公平锁。
非公平锁的实现原理
当我们使用无参构造方法构造的时候即ReentrantLock lock = new ReentrantLock()
,创建的就是非公平锁。
1 | public ReentrantLock() { |
lock方法获取锁
lock
方法调用CAS
方法设置state
的值,如果state
等于期望值0
(代表锁没有被占用),那么就将state
更新为1
(代表该线程获取锁成功),然后执行setExclusiveOwnerThread
方法直接将该线程设置成锁的所有者。如果CAS
设置state
的值失败,即state
不等于0
,代表锁正在被占领着,则执行acquire(1)
,即下面的步骤。nonfairTryAcquire
方法首先调用getState
方法获取state
的值,如果state
的值为0
(之前占领锁的线程刚好释放了锁),那么用CAS
设置state
的值,设置成功则将该线程设置成锁的所有者,并且返回true
。如果state
的值不为0
,那就调用getExclusiveOwnerThread
方法查看占用锁的线程是不是自己,如果是的话那就直接将state + 1
,然后返回true
。如果state
不为0
且锁的所有者又不是自己,那就返回false
,然后线程会进入到同步队列中。
1 | 作者:冠状病毒biss |
tryRelease锁的释放
- 判断当前线程是不是锁的所有者,如果是则进行步骤
2
,如果不是则抛出异常。 - 判断此次释放锁后
state
的值是否为0,如果是则代表锁没有重入,然后将锁的所有者设置成null
且返回true
,然后执行步骤3
,如果不是则代表锁发生了重入执行步骤4
。 - 现在锁已经释放完,即
state=0
,唤醒同步队列中的后继节点进行锁的获取。 - 锁还没有释放完,即
state!=0
,不唤醒同步队列。
1 | public void unlock() { |
公平锁的实现原理
lock方法获取锁
- 获取状态的
state
的值,如果state=0
即代表锁没有被其它线程占用(但是并不代表同步队列没有线程在等待),执行步骤2
。如果state!=0
则代表锁正在被其它线程占用,执行步骤3
。 - 判断同步队列是否存在线程(节点),如果不存在则直接将锁的所有者设置成当前线程,且更新状态state,然后返回true。
- 判断锁的所有者是不是当前线程,如果是则更新状态state的值,然后返回true,如果不是,那么返回false,即线程会被加入到同步队列中
通过步骤2
实现了锁获取的公平性,即锁的获取按照先来先得的顺序,后来的不能抢先获取锁,非公平锁和公平锁也正是通过这个区别来实现了锁的公平性。
1 | final void lock() { |
tryRelease锁的释放
公平锁的释放和非公平锁的释放一样,这里就不重复。
公平锁和非公平锁的公平性是在获取锁的时候体现出来的,释放的时候都是一样释放的。
ReentrantLock的等待/通知机制
我们知道关键字Synchronized
+ Object
的wait
和notify
、notifyAll
方法能实现等待/通知机制,那么ReentrantLock
是否也能实现这样的等待/通知机制,答案是:可以。
ReentrantLock
通过Condition
对象,也就是条件队列实现了和wait
、notify
、notifyAll
相同的语义。 线程执行condition.await()
方法,将节点1从同步队列转移到条件队列中。
线程执行condition.signal()
方法,将节点1从条件队列中转移到同步队列。
因为只有在同步队列中的线程才能去获取锁,所以通过Condition
对象的wait
和signal
方法能实现等待/通知机制。
代码示例:
1 | ReentrantLock lock = new ReentrantLock(); |
运行输出:
1 | 线程获取锁----Thread-0 |
执行的流程大概是这样,线程t1
先获取到锁,输出了”线程获取锁—-Thread-0”,然后线程t1
调用await
方法,调用这个方法的结果就是线程t1
释放了锁进入等待状态,等待唤醒,接下来线程t2
获取到锁,然输出了”另外一个线程获取到锁—-Thread-1”,同时线程t2
调用signal
方法,调用这个方法的结果就是唤醒一个在条件队列(Condition)的线程,然后线程t1
被唤醒,而这个时候线程t2
并没有释放锁,线程t1
也就没法获得锁,等线程t2
继续执行输出”唤醒线程—-Thread-1”之后线程t2
释放锁且输出”另外一个线程释放锁—-Thread-1”,这时候线程t1
获得锁,继续往下执行输出了线程被唤醒----Thread-0
,然后释放锁输出”线程释放锁—-Thread-0”。
如果想单独唤醒部分线程应该怎么做呢?这时就有必要使用多个Condition
对象了,因为ReentrantLock
支持创建多个Condition
对象,例如:
1 | //为了减少篇幅 仅给出伪代码 |
这样就实现了部分唤醒的功能。
Condition
和synchronized的对比
- 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 - 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。 - 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。 - 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。 - 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一
种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没
有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
可中断
1 | public class hhh { |
作者:薛8
链接:https://juejin.im/post/5c95df97e51d4551d06d8e8e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。