队列同步器AbstractQueuedSynchronizer(AQS)
发布日期:2021-09-25 11:48:30 浏览次数:4 分类:技术文章

本文共 14335 字,大约阅读时间需要 47 分钟。

同步器的设计是基于模板方法模式的,即使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法会调用使用者重写的方法

先看下类的定义:

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

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态:

//获取当前同步状态    protected final int getState() {        return state;    }    //设置当前同步状态    protected final void setState(int newState) {        state = newState;    }    //使用CAS设置当前状态,该方法能够保证状态设置的原子性        protected final boolean compareAndSetState(int expect, int update) {        // See below for intrinsics setup to support this        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);    }

同步器可重写的方法:

//独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态    protected boolean tryAcquire(int arg) {        throw new UnsupportedOperationException();    }//独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态        protected boolean tryRelease(int arg) {        throw new UnsupportedOperationException();    }//共享式获取同步状态,返回大于等于0的值表示获取成功,反之获取失败        protected int tryAcquireShared(int arg) {        throw new UnsupportedOperationException();    }//共享式释放同步状态        protected boolean tryReleaseShared(int arg) {        throw new UnsupportedOperationException();//当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占        protected boolean isHeldExclusively() {        throw new UnsupportedOperationException();    }

实现自定义同步器组件时,将会调用同步器提供的模板方法,同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况,后面详细分析

eg:通过一个独占锁的实例深入了解下同步器的工作原理

package MultiThread.lockTest;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.AbstractQueuedSynchronizer;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;//独占锁Mutex是一个自定义同步组件,在同一时刻只允许一个线程占有锁public class Mutex implements Lock {
// 静态内部类,继承了同步器,并实现了独占式获取和释放同步状态 private static class Sync extends AbstractQueuedSynchronizer{
// 是否处于占用状态 protected boolean isHeldExclusively(){ return getState()==1; }// 当状态为0的时候获取锁,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态 public boolean tryAcquire(int acquires){ if(compareAndSetState(0,1)){ setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }// 释放锁,将状态设置为0 protected boolean tryRelease(int releases){ if(getState()==0){ throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; }// 返回一个Condition,每个Condition都包含了一个condition队列 Condition newCondition(){ return new ConditionObject(); } }// 仅需要将操作代理到Sync上即可 /* 当用户使用Mutex时并不会直接和内部同步器实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以lock为例,只需要在方法实现中 调用同步器的模板方法acquire即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待 */ private final Sync sync=new Sync(); public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }}

队列同步器的实现详细分析:

1.同步队列

AQS使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点Node并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态

static final class Node {//共享        static final Node SHARED = new Node();//独占        static final Node EXCLUSIVE = null;//因为超时或者中断,节点会被设置为取消状态,被取消的结点是不会参与到竞争中的,它会一直保持取消状态不会转变为其他状态        static final int CANCELLED =  1;//后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行        static final int SIGNAL    = -1;//节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal方法后//该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中        static final int CONDITION = -2;//表示下一次共享式同步状态获取将会无条件传播下去        static final int PROPAGATE = -3;        //等待状态,包含上述几个        volatile int waitStatus;        //前驱节点,当节点加入同步队列时被设置(尾部添加)        volatile Node prev;        //后继节点        volatile Node next;        //获取同步状态的线程        volatile Thread thread;        //等待队列中的后继节点,如果当前节点是共享的,那么这个字段将是一个SHARED常量        Node nextWaiter;        final boolean isShared() {            return nextWaiter == SHARED;        }        final Node predecessor() throws NullPointerException {            Node p = prev;            if (p == null)                throw new NullPointerException();            else                return p;        }        Node() {    // Used to establish initial head or SHARED marker        }        Node(Thread thread, Node mode) {     // Used by addWaiter            this.nextWaiter = mode;            this.thread = thread;        }        Node(Thread thread, int waitStatus) { // Used by Condition            this.waitStatus = waitStatus;            this.thread = thread;        }    }

同步器拥有首节点head和尾结点tail,没有成功获取同步状态的线程将会成为节点加入该队列尾部,基本结构如图:

这里写图片描述
同步器将节点加入到队列,必须保证线程安全,so提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update),它需要传递当前线程认为的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾结点建立关联:
这里写图片描述
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,由于只有一个线程能成功获取到同步状态,so设置头节点的方法并不需要使用CAS来保证,只需要将首节点设置为原首节点的后继节点并断开原首节点的next引用即可:
这里写图片描述

2.独占式同步状态获取与释放

//独占式获取同步状态    public final void acquire(int arg) {        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }

上述代码主要完成了同步状态获取、节点构造、加入同步队列及在同步队列中自旋等待,逻辑:首先调用自定义同步器实现的tryAcquire方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点,并通过addWaiter方法将该节点加入到同步队列的尾部,最后调用acquireQueued方法使得节点以“死循环”的方式获取同步状态

节点的构造以及加入同步队列:

private Node addWaiter(Node mode) {        Node node = new Node(Thread.currentThread(), mode);        // 快速尝试在尾部添加        Node pred = tail;        if (pred != null) {            node.prev = pred;            if (compareAndSetTail(pred, node)) {
//确保节点能够被线程安全添加 pred.next = node; return node; } } enq(node); return node; }
//同步器通过死循环来保证节点的正确添加,只有通过CAS将节点设置为尾结点之后,当前线程才从该方法返回,否则当前线程不断尝试设置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;                }            }        }    }

节点进入到同步队列后,就进入了一个自旋的过程,每个节点(线程)都在自省观察,当条件满足,获得了同步状态(只有前驱节点是头节点才能尝试获取同步状态),就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中:

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);        }    }

独占式同步状态获取流程,即acquire方法调用流程:

这里写图片描述
当线程获得同步状态并执行了相关逻辑,就需要释放同步状态,使得后续节点能够继续获取同步状态:

//该方法执行时会唤醒后继节点的线程    public final boolean release(int arg) {        if (tryRelease(arg)) {            Node h = head;            if (h != null && h.waitStatus != 0)            //使用LockSupport来唤醒处于等待状态的线程                unparkSuccessor(h);            return true;        }        return false;    }

总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态,在释放同步状态时,同步器调用tryRelease方法释放,然后唤醒头节点的后继节点

3.共享式同步状态获取与释放

共享式获取与独占式获取区别:同一时刻能否有多个线程同时获取到同步状态。通过调用同步器的acquireShared方法可以共享式获取同步状态:

public final void acquireShared(int arg) {    //当返回值大于等于0时表示能获取到同步状态        if (tryAcquireShared(arg) < 0)            doAcquireShared(arg);    }
//如果当前节点的前驱为头节点,尝试获取同步状态   //如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出    private void doAcquireShared(int arg) {        final Node node = addWaiter(Node.SHARED);        boolean failed = true;        try {            boolean interrupted = false;            for (;;) {                final Node p = node.predecessor();                if (p == head) {                    int r = tryAcquireShared(arg);                    if (r >= 0) {                        setHeadAndPropagate(node, r);                        p.next = null; // help GC                        if (interrupted)                            selfInterrupt();                        failed = false;                        return;                    }                }                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    interrupted = true;            }        } finally {            if (failed)                cancelAcquire(node);        }    }
//该方法在释放同步状态后,将会唤醒后续处于等待状态的节点    public final boolean releaseShared(int arg) {        if (tryReleaseShared(arg)) {            doReleaseShared();            return true;        }        return false;    }

4.独占式超时获取同步状态

通过调用同步器的doAcquireNanos方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到则返回true,否则返回false:

private boolean doAcquireNanos(int arg, long nanosTimeout)            throws InterruptedException {        if (nanosTimeout <= 0L)            return false;        final long deadline = System.nanoTime() + nanosTimeout;        final Node node = addWaiter(Node.EXCLUSIVE);        boolean failed = true;        try {            for (;;) {                final Node p = node.predecessor(); //当节点的前驱节点为头节点时获取同步状态,获取成功则从方法返回                if (p == head && tryAcquire(arg)) {                    setHead(node);                    p.next = null; // help GC                    failed = false;                    return true;                } //如果当前线程获取同步状态失败,判断是否超时,如果没有超时,重新计算超时时间间隔nanosTimeout,然后使当前线程等待nanosTimeout //需要睡眠的时间间隔nanosTimeout,若大于0表示超时时间未到需要继续睡眠;反之表示已经超时                nanosTimeout = deadline - System.nanoTime();                if (nanosTimeout <= 0L)                    return false; //如果nanosTimeout <= spinForTimeoutThreshold(1000纳秒),将不会使该线程进入超时等待,而是进入快速的自旋 //原因:非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现的反而不精确                if (shouldParkAfterFailedAcquire(p, node) &&                    nanosTimeout > spinForTimeoutThreshold)                    LockSupport.parkNanos(this, nanosTimeout);                if (Thread.interrupted())                    throw new InterruptedException();            }        } finally {            if (failed)                cancelAcquire(node);        }    }

独占式超时获取同步状态流程图:

这里写图片描述

例子

设计一个同步工具:该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞,将此工具命名为TwinsLock

分析:首先,确定访问模式,TwinsLock能在同一时刻支持多个线程的访问,共享式访问,使用同步器提供的acquireShared方法等和Shared相关的方法,TwinsLock必须重写tryAcquireShared和tryReleaseShared方法,这样才能保证同步器的共享式同步状态的获取与释放方法得以执行

其次,定义资源数,同步资源数为2,可设置初始状态status为2,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0/1/2,其中0表示当前已经有两个线程获取了同步资源,此时若再有其他线程对同步状态进行获取,只能被阻塞

最后,组合自定义同步器

package MultiThread.lockTest;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.*;public class TwinsLock implements Lock {
// 同一时刻只能有两个线程同时获取到锁 private final Sync sync=new Sync(2);// 自定义同步器Sync,该同步器面向线程访问和同步状态控制 private static final class Sync extends AbstractQueuedSynchronizer{
Sync(int count){ if(count<=0){ throw new IllegalArgumentException("count must large than zero"); } setState(count); } public int tryAcquireShared(int reduceCount) { for (; ; ) { int current = getState(); int newCount=current-reduceCount; if(newCount<0 || compareAndSetState(current,newCount)){ return newCount; } } } public boolean tryReleaseShared(int returnCount){ for(;;){ int current=getState(); int newCount=current+returnCount; if(compareAndSetState(current,newCount)){ return true; } } } } public TwinsLock() { super(); } @Override public void lock() { sync.acquireShared(1); } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public void unlock() { sync.releaseShared(1); } @Override public Condition newCondition() { return null; }}
package MultiThread.lockTest;import org.junit.Test;import java.util.concurrent.locks.Lock;//测试验证TwinsLock是否能够按照预期工作public class TwinsLockTest {
@Test public void test() throws Exception{ final Lock lock=new TwinsLock();// 该线程在执行过程中获取锁,获取锁后使当前线程睡眠1s(并不释放锁),随后打印当前线程名称,最后再次睡眠1s并释放锁 class Worker extends Thread{
public void run(){ while(true){ lock.lock(); try{ SleepUtils.second(1); System.out.println(Thread.currentThread().getName()); SleepUtils.second(1); }catch(Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } }// 启动10个线程 for(int i=0;i<10;i++){ Worker w=new Worker(); w.setDaemon(true); w.start(); }// 每隔一秒进行换行 for(int i=0;i<10;i++){ SleepUtils.second(1); System.out.println(); } }}
//使用到的SleepUtilspublic class SleepUtils {    public static final void second(long seconds){        try{            TimeUnit.SECONDS.sleep(seconds);        }catch (InterruptedException e){}    }}

运行结果,线程名称成对输出,即在同一时刻只有两个线程能够获得锁

摘自《Java并发编程的艺术》

转载地址:https://blog.csdn.net/Autumn03/article/details/81701335 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:再看Lock
下一篇:源码分析-Java Set

发表评论

最新留言

表示我来过!
[***.240.166.169]2024年04月15日 16时48分53秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章