前言
在说ReentrantLock之前,我们先说说并发吧。
在JDK1.5之前,并发处理常用的关键字synchronized。使用synchronized关键字,锁的获取和释放是隐式的,synchronized主要通过系统的monitorenter指令实现的。
那时候synchronized可以称为重量级锁,执行效率不是很高。
而Doug Lea编写的util.concurrent 包被纳入JSR-166标准。这里面就包含了ReentrantLock。
ReentrantLock为编写并发提供了更多选择。
使用
ReentrantLock的通常用法如下:
1 | public class X { |
原理
ReentrantLock主要是通过AbstractQueuedSynchronizer实现的,是一个重入锁,即一个线程加锁后仍然可以获得锁,不会出现自己阻塞自己的情况。
UML图
我们看一下它们的UML图。
可以看到ReentrantLock实现了Lock接口。
锁类型
ReentrantLock的两种锁类型,公平锁和非公平锁。
源码分析
我们先来看下ReentrantLock的构造方法。
1 | public ReentrantLock() { |
可以看到默认无参构造方法为非公平锁实现。如果想定义公平锁实现,可以传入true来控制。
它的lock方法:
1 | public void lock() { |
公平锁和非公平锁各有自己的实现方式。我们来看下他们的tryAcquire方法。
非公平锁源码:
1 | static final class NonfairSync extends Sync { |
可以看到,非公平锁首先判断AQS(AbstractQueuedSynchronizer)中的state是否为0,0表示没有线程持有该锁,当前线程就尝试获取锁。
如果不是0,那在判断是不是当前线程持有该锁,如果是,就会增加state,改变state状态。(因此ReentranLock支持重入)。
公平锁源码:
1 | static final class FairSync extends Sync { |
1 | public final boolean hasQueuedPredecessors() { |
公平锁的tryAcquire方法,可以看到,相比非公平锁,多了hasQueuedPredecessors方法,这个方法是判断队列中是否有其他线程,如果没有,线程才会尝试获取锁,如果有,会先把锁分配给队列的线程,因此称为公平锁。
这儿可以看到,非公平锁的效率比公平锁要高。
这是tryAcquire方法,如果尝试获取锁失败了呢?
那就会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法啦。
我们先来看一下addWaiter方法。
1 | private Node addWaiter(Node mode) { |
可以看到,这个方法会把线程添加到队列尾,同时,for(;;)循环保证添加成功,直到return出去。
添加后,调用acquireQueued方法,这个方法为挂起等待线程。
看下该方法源码:
1 | final boolean acquireQueued(final Node node, int arg) { |
可以看到,如果节点为头节点,就尝试获取一次锁,如果成功,就返回。
否则判断该线程是否需要挂起,如果需要的化就调用parkAndCheckInterrupt挂起。
调用LockSupport.park方法挂起线程,直到被唤醒。
selfInterrupt方法:
1 | static void selfInterrupt() { |
调用interrupt方法,中断正在执行的线程(如果不是当前线程的话)。
释放锁unlock方法:
公平锁和非公平锁释放锁的方法是一样的。
1 | public void unlock() { |
可以看到首先会判断当前线程是否是获得锁的线程,如果是重入锁需要将state减完才算是完全释放锁。
释放后调用unparkSuccessor唤起挂起线程。
总结
- 非公平锁的效率是比公平锁要高的。
- ReentranLock支持重入,因为增加了对自身线程的处理,通过state可以控制。
- 解锁操作应放到finally块里,避免使用锁时出现资源无法释放的问题。