Java AQS的机制

如果是你,你怎么设计AQS框架?

1、既然要做一个框架,首先要保证框架的通用性,可复用性,下游实现透明的同步机制,并且与上游解耦。

2、利用CAS进行原子修改共享资源,在多个线程想要修改共享资源的时候,先读我们定好的标记位,如果标记位显示共享资源空闲,就给予该线程操作权限,并阻碍其他线程的调用。

3、那么阻碍其他线程调用怎么设计?那么就运用等待队列,这样能保证CPU利用率达到最好

其实这就是AQS的基本框架原理。下面来看源码

AQS的成员属性

state:用于判断共享资源是否被占用的标记位

volatile : 保证了线程之间的可见性

state 为什么是int值而不是布尔值?布尔值所占内存更小(1bit)。

这里线程获取锁有两种模式:独占和共享。独占:一旦被占有,其他线程不能占用;共享:一旦被占用,其他共享模式下也可以占用。

所以state用int值。

head、tail:这就是上述说到的等待队列。

AQS等待队列

独占模式下:

1、尝试获取锁(tryAcquire)

该方法只有抛出一个异常,并且使用该方法意味着必须override该方法,否则抛出不支持该操作的异常。体现了框架的灵活度。

2、获取锁(acquire)

if条件判断就是如果尝试获取到锁,那么!tryAcquire就是false,直接跳出该判断

那么如果tryAcquire为false,那么就进入aquireQueued方法和addWaiter方法

addWaiter:将当前线程封装成一个Node,然后加入等待队列,返回值为当前节点(源码注释:这里先尝试快速插入尾节点,失败则尝试完全插入)

1、首先先创建了一个node对象

2、获取尾节点指针,将其作为当前节点的前置节点,如果尾节点不为空,将当前节点置为尾节点,然后再将前置节点置的next指针指向已经成为尾节点的当前指针。

在pred.next=node会出现安全问题么?不会,这里只会改变指针。

既然先进先出,有生产插入尾节点的操作,那肯定有消费取出头节点的操作。但是源码不是这样操作的,来看acquireQueued怎么实现的

acquireQueued

这个方法是配合release方法,是对线程进行挂起和响应来实现队列的先进先出。

1、先看最后finally,能执行到finally说明failed一直为true,只有抛出异常才会将node的waitStatus置为CANCEL(也就是cancelAcquire方法)

2、再看try,当前节点是头节点且自旋获得锁,直接返回

备注:在AQS中,头节点是虚节点,而是通过第二个节点拿到锁之后,它就会变成头节点,且在独占模式下,头节点只有一个。

 

shouldParkAfterFailedAcquire:

当前节点的前置节点很有可能不是头节点,或者是头节点尝试获取失败,那么就要进入下一个判断 当前线程是否需要挂起(挂起比直接自旋性能要高)

哪些情况需要挂起(如下图三个判断)

1、如果当前节点的前置节点的waitstatus为SIGNAL,说明前置节点也在等待拿锁,所以当前节点是可以挂起的

2、如果waitStatus状态是CANCEL,直接删除

3、如果waitStatus是其他状态,既然当前节点已经加入,那么前置节点就应该做好等待锁的准备,所以将前置节点waitStatus置为SIGNAL

那么以上三种情况返回true,那么说明当前节点需要被挂起,其他两种false则不需要。

parkAndCheckInterrupt

release

在该方法中,假如尝试释放锁成功,那么就要唤醒等待队列的其他节点

主要看下unparkSuccessor方法

这个head节点就是acquireQueued方法中的node节点,该方法是为了唤醒后面的Node节点(下图中unpark唤醒操作),是其自旋获得锁,并且被置为head,将waitStatus置为0;

那么为什么唤醒不直接从头节点开始搜索?而是从后往前搜索?

在enq方法里,在执行“if(compareAndSetTail(t, node)){//...}”时,cas是原子操作,但是当cas成功(tail指向当前node),执行if代码块里的内容时,此时不是同步操作。

这个时刻,node与前一个节点t之间,node的prev指针在cas操作之前已经建立,而t的next指针还未建立。

此时若其他线程调用了unpark操作,从头开始找就无法遍历完整的队列,而从后往前找就可以。

猜你喜欢

转载自blog.csdn.net/weixin_39082432/article/details/114272058
今日推荐