Java并发编程 - ReentrantLock
AQS内部属性
state(volatile修饰):为0是标记没有被占用,大于0时表示被其它线程或当前线程占用,对于ReentranLock,state可以大于1,表示多次重入,每次所释放时state减1,直至state等于0时释放锁。
head:等待队列的头节点,head后节点为null节点,链表为是双向链表,便于查找前驱和后继节点。
tail:等待队列的尾节点。
exclusiveOwnerThread:持有锁的线程。
AQS使用双向链表的原因
- 当某个线程出现异常不需要竞争锁,比如线程被中断了,可以快速移除链表。
- 通过lock()竞争锁时,需要不断的判断当前线程的前驱节点是否是head,如果是才能成功获得锁。
ReentrantLock实现原理
ReentrantLock默认使用NonfairSync实现类,NonfairSync和FairSync继承Sync,Sync是一个抽象类。
加锁流程(lock)
加锁成功(tryAcquire)
- 如果state=0,通过CAS尝试将state赋值为1,同时设置exclusiveOwnerThread为当前线程。
- 如果state!=0,判断exclusiveOwnerThread是否为当前线程,如果是当前线程state+1。
加锁失败(acquireQueued)
- (addWaiter)先创建Node节点,放到等待队列尾部。如果是tail节点为null,会先创建一个Dummy节点(空节点),head和tail的引用指向Dummy节点,在创建Node节点,放到等待队列尾部。
node节点如何插入到队列尾部过程
-
- 把当前线程的node的prev指向tail
- 通过cas把node节点设置为新tail
- 把原tail节点的next指向当前node
- (acquireQueued)如果创建的Node节点的前驱节点是head节点,会在尝试一次锁竞争(tryAcquire),竞争失败走3。
- (shouldParkAfterFailedAcquire)将前驱节点的waitStatus改为-1,并返回false,再次执行acquireQueued(改成-1的作用是:告知前驱节点有责任去唤醒下一个节点)。
- (parkAndCheckInterrupt)阻塞当前线程。
释放锁流程(unlock)
- (tryRelease)state-1,当state=0时,设置exclusiveOwnerThread为null,并返回true表示锁释放成功,否则返回false。
- (release)判断head!=null&&head.waitStatus!=0,则表明需要唤醒head的后继节点(unparkSuccessor)。
- (acquireQueued),被parkAndCheckInterrupt阻塞的线程会继续执行for循环,(tryAcquire)如果竞争成功会设置当前节点为头节点,头节点的下一个节点为null(方便GC)。
- (acquireQueued)如果后继节点竞争失败,继续阻塞。
ReentryLock 特性
- 支持可重入
- 支持阻塞竞争锁和非阻塞竞争锁,lock()和tryLock()
- 支持公平和非公平锁