开始之前
最近一直没有写东西,两个原因 一:前段时间工作太忙,再加上办了一张游泳卡,近期都在去游泳,二:所有的输出都输出到纸上去了
深入理解Java虚拟机这本书买来一直吃灰状态中,最近拜读完,并且做了笔记。发现一个问题,很多博客上面的内容都是来自此书,而且都是断断续续的,建议如果需要系统的了解JVM,还是买来看一看
有时间将笔记上的内容copy到网上,最近无事,准备看看JUC (java.util.concurrent)里面的内容,然后结合Java并发编程的艺术,来看看Java并发是怎么一回事
开始
是什么?
ReentrantLock是一个可重入的互斥锁,又被称为独占锁。ReentrantLock锁在同一时间点只能被一个线程锁持有,而可重入的意思是,ReentrantLock锁可以被单个线程多次获取
ReentrantLock可以分为公平锁和非公平锁 ,公平锁:通过一个FIFO等待队列来管理获取该锁的所有线程,保证线程先进先出。非公平锁:在锁是可获取状态时,任何线程都有可能获取到该锁。
方法
1 | // 创建一个 ReentrantLock ,默认是“非公平锁”。 |
源码结构
ReentrantLock实现了Lock接口
ReentrantLock的锁实现机制是通过Sync实现的。Sync下有两个子类 FairSync (公平锁)和 NonfairSync (非公平锁)
Sync又是AQS的子类
节点数据结构
Node是CLH队列的节点,代表等待锁的线程队列
每个Node都会对应一个线程,每个Node的prev和next都会对应等待队列的上一个线程和下一个线程
Node通过waitStatus来判断线程的等待状态,具体的状态下文表格中有
1 | static final class Node { |
源码
结构图
ReentrantLock的锁实现机制是有Sync类具体实现的
1 | /** Synchronizer providing all implementation mechanics */ |
从这里可以看出来,在实例化一个ReentrantLock的时候默认是实例化一个非公平锁的,可以选择当前锁是使用公平锁还是非公平锁
1 | /** |
加锁
1 | /** |
这里是父类实现的抽象方法,具体实现在两个子类里面,就是NonfairSync和FairSync里面
非公平锁加锁
非公平锁加锁是每次都会尝试获取锁,如果获取成功了,就直接返回,如果没有成功,就会添加到等待队列
所以非公平锁在第一次没有获取成功时,加入等待队列之后也和公平锁没什么区别
1 | /** |
acquire是AQS里面实现的方法,具体的方法又会在Sync的子类里面去重写,这里挺绕的
1 | /** |
tryAcquire方法就是在Sync子类里面重写的
1 |
|
非公平锁加锁的过程描述
1、通过compareAndSetState()去尝试更改锁的状态,如果锁状态更改成功,则表示获取锁成功,然后将当前线程保存到AbstractOwnableSynchronizer 当前的同步器
2、如果1 不成功过,就去走锁竞争的逻辑,首先会去尝试获取锁,如果获取成功 (两种可能 上一条线程执行完了,是重入了,state+1)就直接返回了
3、如果2不成功,就会先去构建一个包含当前线程信息的节点,然后插入到队列尾部,然后当前线程的上一个节点是头节点,则去尝试获取锁,如果不是,则挂起当前线程,等待解锁操作的唤醒
做了一个流程图可以参考:
这里面涉及到几个状态的值:
状态 | 值 | 说明 |
---|---|---|
CANCELLED | 1 | 等待超时或者中断,需要从同步队列中取消 |
SIGNAL | -1 | 后继节点出于等待状态,当前节点释放锁后将会唤醒后继节点 |
CONDITION | -2 | 节点在等待队列中,节点线程等待在Condition上,其它线程对Condition调用signal()方法后,该节点将会从等待同步队列中移到同步队列中,然后等待获取锁。 |
PROPAGATE | -3 | 表示下一次共享式同步状态获取将会无条件地传播下去 |
INITIAL | 0 | 初始状态 |
加锁的过程就这些了,可以一句话总结一下,就是加锁,如果加锁失败,则放到AQS队列尾部,这个是重入锁,可以被同一个线程加锁多次,只需要在状态上+1 解锁也需将状态减少到0
公平锁的加锁
公平锁和非公平锁在上文说过了,在代码上的区别不是很多,只是在尝试获取锁之前,需要判断当前线程是CLH等待队列的表头时,才获取锁,非公平锁就是只要锁处于空闲状态,不管CLH里面的队列,就直接获取,如果获取失败,就还是相当于一个公平锁放到CLH队列里面等待
公平锁在尝试获取锁的的时候多了一个hasQueuedPredecessors()方法,如果当线程状态为0的时候,表示当前线程是空闲的,就判断当前线程是不是在等待队列的头部,如果是,则获取锁,如果不是,则和非公平锁一样,放到等待队列去
1 | /** |
解锁
1 | /** |
非公平锁解锁的过程描述
解锁,将state的值-1 如果为0,则去解锁,并且将当前保存的线程信息设置为null,解锁分为两个步骤
1、将state的值 -1 如果为0,就表示能直接解锁
2、开始解锁,解锁分为两步,第一步是将头节点的状态改成0,然后将头结点的next后置节点激活
流程图:
总结
ReentrantLock是一个可重入锁,他的可重入是标识当前线程能在状态上+1来保证当前线程能进入已经被当前线程加锁的线程,这个源码的重点还是AbstractQueuedSynchronizer里面的这个AQS队列