java锁概述-AQS

?

在java代码中,代码为了保证逻辑的原子性,往往会给代码加锁,防止多线程并发下对非原子性操作的执行,造成逻辑紊乱。

aqs是由Doug Lee写的对于synchronized的优化,aql是clh锁,即Craig, Landin, and Hagersten (CLH),CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,线程只需要在本地自旋,查询前驱节点的状态,如果前驱节点释放了锁,就结束自旋。

在aqs中,维护了一个volatile int state(共享资源)和一个FIFO的线程等待队列,线程在获取锁失败之后,会放置进入等待队列的对尾,知道被前驱节点唤醒。

能够访问state的方式有三种:

getState、setState、compareAndSetState

在aqs中有两种模式:独占模式(Exclusive):只有一个线程能执行

                              共享模式(Share):可以多个线程同时执行

 

aqs使用了模版方法的设计模式,在类中定义好了队列的维护算法。将state该共享资源的获取与释放留给实现者去实现,主要是以下几个方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

锁的释放与获取没有定义成abstract,所以可以继承的时候,只实现一种模式

自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可

AQS是双向链表的形式保存每个等待的任务,在Node中存在属性waitStatus,有5种值:

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

 

独占模式锁的入口:acquire(int)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
java锁概述-AQS

 

1、tryAcquire尝试获取共享资源,如果获取到了,返回true否则false(非公平,每个线程进来会直接抢夺共享资源,失败才去CLH队尾)

2、addWaiter失败后,将任务添加到等待队列的队尾,并标记为独占模式

3、acquireQueued 线程阻塞在队列中获取资源,一直到获取到资源之后才返回,如果在等待过程中被中断过,则返回true,否则返回false

4、如果线程在等待的过程当中被中断过,它是不响应该中断的。只是获取资源之后再进行自我中断selfInterrupt ,补上中断

 

独占模式:

acquire(int)获取锁之后,如果获取失败,则会放进等待队列,直到前驱节点唤醒。

 

共享模式:前驱节点被唤醒后,state如果资源还有剩余,会继续唤醒后续节点

 

 

waitStatus之PROPAGATE(-3) 状态

在jdk1.6早期一些版本中,是没有PROPAGATE这个状态的,这个状态是为了解决bug,即线程阻塞,永远不会被唤醒的状态;

PROPAGATE只在 doReleaseShared 方法中被使用:

出现的bug(共享模式):

举例,如果有四条线程,t1,t2获取锁,在CLH中阻塞,t3、t4的线程只执行release方法

即此时aqs中 head->t1->t2 state状态尾-1(SIGNAL)

1、线程t3,调用releaseShare方法,在tryReleaseShare中state+1 设置为1了,head非空,并且waitStatus=-1,于是调用unparkSuccessor唤醒后续线程,并将head的waitStatus设置为0

2、线程t1,被t3使用unparkSuccessor方法唤醒了,调用tryAcquireShared将state-1又变成了0,此时,调用链还没走到setHeadAndPropagate

3、线程t4,调用了releaseShare方法,调用tryAcquireShared将state+1,变成了1,head非空,并且waitStatus为0,所以不会执行unparkSuccessor方法

4、线程t3,完成 int r = tryAcquireShared(arg);方法后,进入setHeadAndPropagate方法,将自己设置为head,但在此时propagate也就是剩余的state已经为0了(propagate是在步骤2时通过传参的方式传进来的,那个时候-1后剩余的state是0),所以也不会执行unparkSuccessor方法。

 

至此可以发现一轮循环走完后,CLH队列中的t2线程永远不会被唤醒,主线程也就永远处在阻塞中,这里也就出现了bug。那么来看一下现在的AQS代码在引入了PROPAGATE状态后,在面对同样的场景下是如何解决这个bug的:

1、线程t3,调用releaseShare方法,在tryReleaseShare中state+1 设置为1了,head非空,并且waitStatus=-1,于是调用unparkSuccessor唤醒后续线程,并将head的waitStatus设置为0

2、线程t1,被t3使用unparkSuccessor方法唤醒了,调用tryAcquireShared将state-1又变成了0,此时,调用链还没走到setHeadAndPropagate

3、线程t4,调用了releaseShare方法,调用tryAcquireShared将state+1,变成了1,head非空,并且waitStatus为0,所以不会执行unparkSuccessor方法,但是!!!会进入另一个if,将waitStatus从0改为PROPAGATE状态。

4、线程t3,完成 int r = tryAcquireShared(arg);方法后,进入setHeadAndPropagate方法,将自己设置为新head,但在此时propagate也就是剩余的state已经为0了(propagate是在步骤2时通过传参的方式传进来的,那个时候-1后剩余的state是0),但是,在setHeadAndPropagate方法中,获取判断原本head的waitStatus<0,此时waitStatus为-3,满足条件,且t1后一个节点t2非空,为共享节点,所以调用doReleaseShared方法,唤醒新的头节点后面的节点,即线程t2

 

 

条件队列:独占模式下,在调用await方法的时候,会将节点放置到condition队列当中,并释放锁,唤醒下一个等待线程,并将节点退出同步队列。挂起线程。

在调用signal方法的时候,会恢复条件队列的第一个节点,并在条件队列中删除,之后再将该节点重新放置待同步队列当中。

?

java锁概述-AQS

上一篇:【C语言】第8章 善于利用指针


下一篇:JAVA自学第四章:Hellow Word