AQS 自定义同步锁,挺难的! (2)

注意: 在acquire方法中,如果tryAcquire(arg)返回true, 就直接执行完了,线程被放行了。所以的后面的方法调用acquireQueued、addWaiter都是tryAcquire(arg)返回false时才会被调用。

tryAcquire 的作用

tryAcquire在AQS类中是一个直接抛出异常的实现:

protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }

而在我们自定义的 CustomLock 中,重写了此方法:

@Override protected boolean tryAcquire(int arg) { int state = getState(); if(state == 0){ if( compareAndSetState(state, arg)){ setExclusiveOwnerThread(Thread.currentThread()); System.out.println("Thread: " + Thread.currentThread().getName() + "拿到了锁"); return true; } }else if(getExclusiveOwnerThread() == Thread.currentThread()){ int nextState = state + arg; setState(nextState); System.out.println("Thread: " + Thread.currentThread().getName() + "重入"); return true; } return false; }

tryAcquire方法返回一个布而值,true表示当前线程能够访问资源,false当前线程不能访问资源,所以tryAcquire的作用:决定线程是否能够访问受保护的资源。tryAcquire里面的逻辑在子类可以自由发挥,AQS不关心这些,只需要知道能不能访问受保护的资源,然后来决定线程是放行还是进行等待队列(阻塞)。

因为是在多线程环境下执行,所以不同的线程执行tryAcquire时会返回不同的值,假设线程A比线程B要快一步,先到达compareAndSetState设置state的值成员并成功,那线程A就会返回true,而 B 由于state的值不为0或者compareAndSetState执行失败,而返回false。

线程B 抢占锁流程

上面访问到线程A成功获得了锁,那线程B就会抢占失败,接着执行后面的方法。

线程的入队

线程的入队是逻辑是在addWaiter方法中,addWaiter方法的具体逻辑也不需要说太多,如果你知道链表的话,就非常容易理解了,最终的结果就是将新线程添加到队尾。AQS的中有两个属性head、tail分别指定等待队列的队首和队尾。

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; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }

需要注意的是在enq方法中,初始化队列的时候,会新建一个Node做为head和tail,然后在之后的循环中将参数node添加到队尾,队列初始化完后,里面会有两个节点,一个是空的结点new Node()另外一个就是对应当前线程的结点。

由于线程A在tryAcquire时返回了true,所以它会被直接放行,那么只有B线程会进入addWaiter方法,此时的等待队列如下:

AQS 自定义同步锁,挺难的!

注意: 等待队列内的节点都是正在等待资源的线程,如果一个线程直接能够访问资源,那它压根就不需要进入等待队列,会被放行。

线程B 的阻塞

线程B被添加到等待队列的尾部后,会继续执行acquireQueued方法,这个方法就是AQS阻塞线程的地方,acquireQueued方法代码的一些解释:

外面是一个for (;;)无限循环,这个很重要

会重新调用一次tryAcquire(arg)判断线程是否能够访问资源了

node.predecessor()获取参数node的前一个节点

shouldParkAfterFailedAcquire判断当前线程获取锁失败后,需不需要阻塞

parkAndCheckInterrupt()使用LockSupport阻塞当前线程,

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); } } shouldParkAfterFailedAcquire 判断是否要阻塞

shouldParkAfterFailedAcquire接收两个参数:前一个节点、当前节点,它会判断前一个节点的waitStatus属性,如果前一个节点的waitStatus=Node.SIGNAL就会返回true:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygwzg.html