AQS学习(一)


前言

`
AQS 的全称为 AbstractQueuedSynchronizer,即抽象队列同步器。这个类在 java.util.concurrent.locks 包下面
AQS 就是一个抽象类,主要用来构建锁和同步器

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable

AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。

一、自定义锁模拟?

@Slf4j(topic = "e")
public class TestLock {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(()->{
            lock.lock();
            log.debug("-------------1");
            try {
                TimeUnit.SECONDS.sleep(3);//让cpu放弃这个线程的调度
                log.debug("-------------3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
        },"t1");

        Thread t2 = new Thread(()->{
            lock.lock();
            log.debug("-------------2");
            lock.unlock();
        },"t2");

        t1.start();
        t2.start();
    }
}

如上代码,如果我们不使用ReentrantLock锁,由于t1线程中有睡眠,会导致cpu放弃这个调度,继而结果为

22:49:15.437 [t1] DEBUG e - -------------1
22:49:18.447 [t1] DEBUG e - -------------2
22:49:18.447 [t2] DEBUG e - -------------3

如果我们使用锁,则t2需要等待锁释放才能执行,最终结果为

22:49:15.437 [t1] DEBUG e - -------------1
22:49:18.447 [t1] DEBUG e - -------------3
22:49:18.447 [t2] DEBUG e - -------------2

而我们可以先模拟ReentrantLock,先进行简单认识,再深入研究

什么是锁
目标: 同步 多线程间一前一后的执行
它的本质就是一个标识: 如果这个标识改变成了某个状态我们就理解为获取锁
拿不到锁其实就是陷入阻塞(死循环) 让这个方法不返回

自旋实现一个同步

替换上述的ReentrantLock,自定义CustomLock实现clock与cunlock

public class CustomLock {

    volatile int status=0;//标识---是否有线程在同步块-----是否有线程上锁成功

    private static Unsafe unsafe = null;

    private static long statusOffset;

    //获取unsafe对象
    static {
        Field singLeoneInstanceField = null;
        try {
            singLeoneInstanceField = Unsafe.class.getDeclaredField("theUnsafe");
            singLeoneInstanceField.setAccessible(true);
            unsafe = (Unsafe)singLeoneInstanceField.get(null);
            statusOffset = unsafe.objectFieldOffset(com.juc.CustomLock.class.getDeclaredField("status"));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public void clock() {
        //cas 原子操作
        while (!compareAndSet(0, 1)) {

        }

    }

     boolean compareAndSet(int oldVal, int newVal) {
        return unsafe.compareAndSwapInt(this, statusOffset, oldVal, newVal);
    }

    public void cunlock() {
        status = 0;
    }
}

缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费Ns处理业务逻辑,那另外一个线程就会白白的花费Ns的cpu资源
解决思路:让得不到锁的线程让出CPU

park+自旋方式实现同步

volatile int status=0;
Queue parkQueue;

void lock(){
	while(!compareAndSet(0,1)){
		park();
	}
   unlock()
}

void unlock(){
	lock_notify();
}

void park(){
	//将当期线程加入到等待队列
	parkQueue.add(currentThread);
	//将当前线程释放cpu  阻塞
	releaseCpu(); //LockSupport.park();
}
void lock_notify(){
	//得到要唤醒的线程头部线程
	Thread t=parkQueue.header();
	//唤醒等待线程
	unpark(t); // LockSupport.unpark(t);
}

JDK的JUC包下面ReentrantLock类的原理就是利用了这种机制

ReentrantLock分析

先看加锁流程,再去分析

lock方法的过程(分为公平和非公平锁)
1、获取当前线程
final Thread current = Thread.currentThread();
2、获取锁的状态getState()
int c = getState()
3、判断锁的状态
if(c == 0)
4、如果锁是自由状态则第5步,否则第7步
5、判断自己是否需要排队
什么情况下当前线程不需要排队(排队=入队+阻塞)
1、队列没有初始 对头和队尾等于null的时候不需要排队
2、队列当中只有一个人的时候是不需要排队
6、如果不需要排队–则cas加锁 成功则直接返回;加锁流程结束,执行临界区代码
7、判断是否重入(一般情况下不重入),如果第6步执行,则没有这一步,这是第4步的分支
8、直接返回false(加锁失败,不考虑重入)
9、加锁失败之后会调用addWaiter,主要是入队(入队不等于排队)
入队完成之后第一个节点是虚拟出来的thread等于null的节点,而不是我们入队的节点
为什么这里需要虚拟出来一个点,而不是拿当前节点作为头部?
10、判断是否需要自旋

相关组件定义
FairSync(AQS公平锁)

private transient volatile Node head; //队首
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
private transient Thread exclusiveOwnerThread; //当前持有锁线程

Node类的设计

public class Node{
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
}

acquire方法方法源码分析

 public final void acquire(int arg) {
 //tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队
 //加入队列之后线程会立马park,等到解锁之后会被unpark,醒来之后判断自己是否被打断了;被打断下次分析
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire方法源码分析

protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
            int c = getState();
            if (c == 0) {
            //hasQueuedPredecessors,判断自己是否需要排队
            //如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    //设置当前线程为拥有锁的线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果是当前线程,判断是否需要重入
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

hasQueuedPredecessors判断是否需要排队
这个地方会涉及多种情况,放到后面详细说明(不得不佩服大师的高超水平)

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

acquireQueued(addWaiter(Node.exclusive),arg))
代码执行到这里一定是tc需要排队,而出现这种场景有两种情况
1、tf持有了锁,并没有释放,所以tc来加锁的时候需要排队,但这个时候—队列并没有初始化
2、tn持有了锁,那么由于加锁tn!=tf,所以队列是一定被初始化了的,tc来加锁,那么队列当中有人在排队,故而他也去排队

addWaiter(Node mode)

  private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //判断pred是否为空,其实就是判断对尾是否有节点
        if (pred != null) {
            node.prev = pred;
            //这里需要cas,因为防止多个线程加锁,确保nc入队的时候是原子操作
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //入队(会有死循环)
        enq(node);
        return node;
    }

acquireQueued(final Node node, int arg)

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

加锁过程总结
如果是第一个线程tf,那么和队列无关,线程直接持有锁。并且也不会初始化队列,如果接下来的线程都是交替执行,那么永远和AQS队列无关,都是直接线程持有锁,如果发生了竞争,比如tf持有锁的过程中T2来lock,那么这个时候就会初始化AQS,初始化AQS的时候会在队列的头部虚拟一个Thread为NULL的Node,因为队列当中的head永远是持有锁的那个node(除了第一次会虚拟一个,其他时候都是持有锁的那个线程锁封装的node),现在第一次的时候持有锁的是tf而tf不在队列当中所以虚拟了一个node节点,队列当中的除了head之外的所有的node都在park,当tf释放锁之后unpark某个(基本是队列当中的第二个,为什么是第二个呢?前面说过head永远是持有锁的那个node,当有时候也不会是第二个,比如第二个被cancel之后,至于为什么会被cancel,不在我们讨论范围之内,cancel的条件很苛刻,基本不会发生)node之后,node被唤醒,假设node是t2,那么这个时候会首先把t2变成head(sethead),在sethead方法里面会把t2代表的node设置为head,并且把node的Thread设置为null,为什么需要设置null?其实原因很简单,现在t2已经拿到锁了,node就不要排队了,那么node对Thread的引用就没有意义了。所以队列的head里面的Thread永远为null

参考博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值