线程学习(32)-AQS

AQS简介

AQS,全称是AbstractQueuedSynchronizer,是阻塞锁和相关的同步器的框架。

AQS内部核心。

1.state,用过state属性来表示资源的状态,分为独占锁与共享锁,子类会根据这个状态来维护获取锁和释放锁的方式,来决定锁的类型。

getState(),获取当前状态

setState(),给当前状态赋值

compareAndSetState,使用CAS的方式给State来进行赋值。

就像AQS的内部的acquire以及relaese等系列方法,都是针对于AQS的状态来进行更改。

2.Node阻塞队列,内部封装了Node内部类,在对获取不到资源的线程进行阻塞。类似于synchronized中的EntryList。

3.condition,即处于等待状态的线程,类似Synchronized中的waitSet,只不过可以存在多个。

底层的话,核心方法tryAcquire,tryRelease,tryAcquireShared,tryReleaseShared,isHeldExclusively等都是依赖于上述所说的这三点。

自定义不可重入锁

Lock,在java之中,只是一个接口。内部有六个类是需要实现的。lock,lockInterruptibly,newCondition,tryLock,tryLock(带时间参数),unLock方法,这些方法统一都是依赖于AQS来进行实现的。

package com.bo.threadstudy.eight;

import lombok.extern.slf4j.Slf4j;

import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 测试一下AQS,什么是AQS,AbstractQueuedSynchronizer,内部包含了state状态值,资源的获取以及释放方式,以及一个队列,对获取不到资源的线程阻塞
 * 这里是不可重入锁,是compareAnSet直接置为了0,1,其它的锁是在1的基础上无法进入的。
 */
@Slf4j
public class AQSTest {
    //定义一个锁,要实现Lock接口,Lock接口中共有6个方法,tryLock,Lock,lockInterruptibly,unlock,newCondition,tryLock加时间参数
    public static void main(String[] args) {
        MyLock myLock = new MyLock();
        new Thread(() -> {
            myLock.lock();
            try{
                log.debug("测试1");
                Thread.sleep(1000);
                log.debug("测试1结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                myLock.unlock();
            }


        }).start();

        new Thread(() -> {
            myLock.lock();
            try{
                log.debug("测试2");
                log.debug("测试2结束");
            } finally {
                myLock.unlock();
            }
        }).start();

    }

}


/**
 * Lock中的方法都依赖于AQS来实现的
 */
@Slf4j
class MyLock implements Lock {

    private MyAbstractQueuedSynchronizer myAbstractQueuedSynchronizer = new MyAbstractQueuedSynchronizer();

    @Override
    public void lock() {
        //先试图获取锁,获取失败后从队列中来进行获取,如果还没获取到,将当前线程中断,但中断后需要业务逻辑判断中断,然后阻塞,这个判断在哪
        myAbstractQueuedSynchronizer.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        myAbstractQueuedSynchronizer.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return myAbstractQueuedSynchronizer.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return myAbstractQueuedSynchronizer.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        myAbstractQueuedSynchronizer.release(1);
    }

    @Override
    public Condition newCondition() {
        return myAbstractQueuedSynchronizer.newCondition();
    }
}

/**
 * 这个抽象方法,把能实现的功能都实现了,虽然只是带了一个抛出异常吧
 * 不可重入锁
 */
@Slf4j
class MyAbstractQueuedSynchronizer extends AbstractQueuedSynchronizer{

    //默认情况下都是抛出的异常,所以这里需要重写
    @Override
    protected boolean tryAcquire(int arg) {
        //代表当前是要加锁操作
        if(arg == 1){
            int state = getState();
            //如果在中间被其它线程加上锁之后,加锁失败
            //TODO 果然,这里应该写0,1,因为在多线程访问呢,如果我用state,state+1,很可能第一个线程读到0,第二个读到1,然后累加全部返回true
            //TODO 我上面说的场景有点像共享锁啊,虽然现在要做的是排他锁
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
        }
        return false;
    }

    /**
     * 释放锁的步骤
     * @param arg
     * @return
     */
    @Override
    protected boolean tryRelease(int arg) {
        if(arg == 1){
            if(getState() == 0){
                throw new IllegalMonitorStateException("state已经为0");
            }
            //释放锁资源的过程肯定是单线程操作,所以不需要CAS排进行判定
            //当然,如果是重入锁,释放组员肯定不能判断是否等于0来处理了
            setState(0);
            setExclusiveOwnerThread(null);
            return true;
        }
        return false;
    }

    /**
     * 这个实现的主要目的是为了判断当前线程是否加锁了
     * @return
     */
    @Override
    protected boolean isHeldExclusively() {
        //英文意思是是否独占,如果state=1就是独占锁,如果不为1,则可能没被调用,也可能是共享锁
        return getState() == 1;
    }


    protected Condition newCondition() {
        return new ConditionObject();
    }
}

我自己写了一个MyAbstractQueuedSynchronizer,继承了AQS的底层实现,并依赖它的方法完成了锁的封装。

AQS简单源码

上面的代码,在MyLock类中,实现的几个方法,都是用的AQS自带的一些方法,就像acquire方法,release方法等。我先简单看了一下acquire方法,剩下的在重入锁,读写锁里接着看一下。

在内部首先会试图获取锁,tryAcquire方式,在没有获取到的时候,会将其加入至Node阻塞队列中,调用的是addWaiter方法。

 

 

 首先,就是把当前线程封装成一个Node节点。

将Node阻塞队列中的尾节点tail,给获取到,并且再创建一个局部变量pred。

判断tail是否为空,如果不为空的话,也就是说,当前内部存在其它正在阻塞的线程。那么,将pred的尾引用指向新存放进来的Node节点。然后采用CAS的方式将tail变量的引用指向了新存放的Node节点(代表了当前传入的Node节点作为最后节点),最终将新传入Node节点的前置节点置为pred对象。

当tail为空时,即阻塞队列内部不存在线程时,进入enq方法。

内部是一个死循环结构,初始状态下,获取到尾节点(因为有可能在这个过程中,其它线程插入了新节点)。然后再次进行CAS判断,查看head预期值是否为空,如果为空的话,那么就先创建一个空节点,并将当前节点也赋值给尾节点。再次循环,执行else里面的代码,将传入节点的prev引用赋值为tail的指针,将需要插入的节点经过CAS判定后,将tail的引用指向新传入的节点,并且原先的tail(现在的倒数第二个节点)的next指向为新插入Node对象。

最终返回这个新插入的Node对象。

 

acquireQueued方法,默认一个初始的failed方法状态,进入循环后,判断当前节点的头节点是否是队列中第二个元素,如果是的话,因为第一个元素是空的,所以下一个需要被唤醒的线程就是node节点,此时将head引用指向当前节点,并且当前节点之前的node从链表中移除。并返回打断状态为false。如果不是第二个元素的话,则返回一个true的打断状态。最终将当前线程打断。

现在的这种方式,是在程序中调用了Lock方法后会走这个方法,然后应该是在释放锁资源时也会走这个方法。

AQS 的基本思想其实很简单

获取锁的逻辑
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队

释放锁的逻辑
if(state 状态允许了) {
恢复阻塞的线程(s)
}
要点
原子维护 state 状态
阻塞及恢复线程
维护队列
1) state 设计
state 使用 volatile 配合 cas 保证其修改时的原子性
state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
2) 阻塞恢复设计
早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume
那么 suspend 将感知不到
解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没
问题
park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
park 线程还可以通过 interrupt 打断
3) 队列设计
使用了 FIFO 先入先出队列,并不支持优先级队列
设计时借鉴了 CLH 队列,它是一种单向无锁队列
 


 

还是有点模糊,接下来还得好好的看一下,头疼啊。面试假设问到这程度,没法玩了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值