源码分析-ReentratLock,AQS原理

先画个大致的假类图

主要的类都在这里,核心就是ReentrantLock的内部类 Sync,

FairSync NonfairSync 是Sync的公平锁 非公平锁的实现

Sync继承于AbstractQueueSynchronizer(AQS)  核心功能也都在这 先来分析AQS

AQS的核心思想就是:  一个队列(实质是一个链表结构)  + 一个状态  /  a Queue and a state 

大致思路 : 

state代表当前锁的状态 默认为0,当该锁被某个线程持有则state 为1,如果是重入锁 每重入一次state+1

当线程尝试获取锁失败就将线程自己封装成Node节点加入到队列,然后挂起等待被唤醒,再去竞争锁

完结 !

源码分析:

竞争锁的队列该有的样子

AQS的几个核心属性  : head, tail, state, exclusiveOwnerThread

head 表示队列的头节点   (头节点不属于阻塞队列!头节点不代表实际的线程)

state 表示当前锁的状态  0 代表无人占有  1 代表被线程占用,每+1 代表被重入一次

tail 表示队列的尾节点

 AQS继承于AbstractOwnableSynchronizer

继承了重要属性 exclusiveOwnerThread   表示当前占有锁的线程

AQS内部类 Node

 几个重要的属性:

next 后继节点,

perv 先驱节点,

thread 该节点代表的线程,

waitStatus 表示后继节点是否需要唤醒 (由后继节点控制,默认是0 不需要唤醒,当后继节点需要被前置节点唤醒则将0设置为-1,如果前置节点主动放弃竞争锁 则将waitStatus > 0)

EXCLUSIVE 标识节点会独占锁    (目前看来用在ReentratLock上抢锁)

SHARED 标识节点会共享锁  (目前看用在CountDownLautch抢锁)

从客户端调用入手分析

加锁过程

public static void main(String[] args) {

Lock lock = new ReentrantLock();
lock.lock();
try {
//do something
}finally {
lock.unlock();
}

}

1.初始化  无参默认是非公平锁

2.lock.lock();  非公平锁的实现

 先试探CAS去改锁的状态,如果成功则加锁成功! 将持有锁的线程设置为自己 over!

如果CAS失败则 执行acquire(1), 参数传1

3. acquire  尝试获取锁失败,并且加入到阻塞队列还被中断了,自己重新标记一下中断位

4.  重点:tryAcquire(1)  尝试去获取锁  传的参数是1

 非公平锁的tryAcquire()实现,

先判断state是否为0 (当前锁是否没被持有)

if (state == 0){

  if (尝试CAS直接去改变状态 因为此时可能会有多个线程同时发现了state是0 都想去改变状态){

    如果CAS成功了就将持有锁的线程设置为自己 over!

  }

}else if (当前线程就是当前持有锁的线程,说明这是锁重入 ){

  直接state+1

}

  尝试获取锁失败了 return false;

5.  boolean acquireQueued()   中断当前线程是否成功 , 传入封装好的节点  和参数1

 try {

  for(;;){

    获取当前节点的前直接点 p

    if (如果p是head节点 并且 再次尝试获取锁成功了){

      就把当前节点设置为head节点

       下个节点置空

       return  中断当前线程失败

    }

    if ( 再次抢占锁失败后是否需要挂起 && 检查并挂起线程){

      中断成功

    }

  }

} finally {

  //failed == true的情况  再次尝试获取锁抛异常导致

  if (如果中断失败了){
    解散节点 不玩了 

  }

6. addWaiter()  尝试加入到阻塞队列   传入的mode标识当前节点是想 独占锁 还是想共享锁

将当前线程封装成一个Node

获取到尾节点 pred

if (如果尾节点不为null){

  将perd作为当前节点的前置节点

  if (CAS将当前节点设置为队列的尾节点){

    return 当前节点;

  }

}

说明尾节点是null,就说明没有head节点,

for循环{

判断尾节点是否为空,如果为空则CAS 将自己设置为head节点 兼 尾节点,

如果中途判断发现尾节点不为空了(可能是被别人抢先占了头节点了),就默默的加入到尾节点后面

}  

解锁过程 unlock()

1. 

 unlock() 是 Sync的方法

2. release()  传入参数1   

 if (尝试去释放锁成功) {

  获取头节点

  if (头节点不为null &&  头节点的下一个节点需要被唤醒) {

    唤醒

  }

}

tryRelease()  传入1,很简单  尝试释放锁  

修改state - 1

如果state减为0  自由了 将持有锁的线程置空

如果是state降为0 return释放锁成功,否则重入锁-1 return false

3.  唤醒节点   传入头节点

if (头节点的下一个节点需要被唤醒) {

 CAS修改waitstatus 为0  表示不需要再换醒了

}

获取头节点的下一个节点 s

if (下一个节点是空,或者下一个节点已经退出竞争锁了) {

  将节点s 置空

  for () {

    for循环再去找 直到找到下一个节点符合的

  }

}

最终 s 不为空 就将s的线程唤醒

 最终通过unpark 唤醒线程

公平锁与非公平锁区别

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

原文地址:https://www.cnblogs.com/ttaall/p/13828134.html