Java多线程之AbstractQueuedSynchronizer--抽象队列同步器

本文深入剖析了AbstractQueuedSynchronizer(AQS)的工作原理,包括其内部数据结构、核心方法及其实现机制。并通过一个独占锁的示例展示了如何利用AQS来构建自定义的同步组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文参考自 

       http://www.cnblogs.com/waterystone/p/4920797.html 

       http://www.tianshouzhi.com/api/tutorials/mutithread/110

AbstractQueuedSynchronizer简述

AbstractQueuedSynchronizer也被称为AQS ,AQS提供了一个FIFO阻塞队列, AQS定义了一套多线程访问共同资源的同步器框架,许多实现同步化的操作都是继承AQS来实现同步的操作,如CountDownLatch、ReentrantLock、Semaphore等等。

AbrtractQueuedSynchronizer维护了一个 volatile int state 属性来表示状态,期望可以通过此状态来实现大多数的同步需求,一般子类通过继承AQS,实现一些管理 state的方法来管理状态,方法一般是类似 acquire()和 release(),由于多线程坏境,所以对state操作时,需要确保原子性,所以AQS提供了三个方法

    1). AQS.getState() 返回同步状态的当前值

    2). AQS.setState(int newState) 设置同步状态值

    3).compareAndSetState(int oldState,int update) 如果当前值等于预期值,则原子性的把当前值更新为预期值

AQS提供了两种机制 ,独占模式(如ReentrantLock)和共享模式(如CountDownLatch),在共享模式下,多个线程获取锁时,可能会获取成功,在独占模式下,当前线程获取锁了,其他线程想要获取锁,会失败,当然两种模式是可以共存的,如ReadWriteLock,不同模式的同步器获取资源的方法在AQS中定义了一些方法

    1).isHeldExclusively()如果当前线程同步模式是独占模式则返回True

    2).tryAcquire()试图在独占模式下获取对象状态

    3).tryRelease()试图在独占模式下释放对象状态

    4).tryAcquireShared()试图在共享模式下获取对象状态

    5).tryReleaseShared()试图在共享模式下释放对象状态

举个例子,ReenTrantLock初始化时 state状态为0,当A线程Lock()时,会调用tryAcquire()使得state==1,其他线程tryAcquire()时,会失败,当A线程unLock()时,会调用tryRelease()使得state==0,其他线程tryAcquire()成功

AQS中阻塞队列是通过链表实现的,我们来看下队列中的元素

在Node中存储线程的信息,有一个waitState的变量表示当前被封装成Node节点的等待状态,有5中取值

  • CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

  • SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

  • CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

  • 0状态:值为0,代表初始化状态

AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态。

 

来看一些AQS的源码

static final class Node {}//内部类
//把当前节点插入到队列尾部
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;//得到当前队列的尾部Node
            if (t == null) { // Must initialize 初始化
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;//把参数Node的头部指向队列尾部
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}
  //把当前节点插入同步队列尾部,如果队列没有初始化则初始化队列,最后返回当前节点
  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;
  }

 

    //唤醒一个节点的Thread,此节点不是当前节点 
    private void unparkSuccessor(Node node) {

        //得到当前node的等待状态
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)//循环队列
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//如果给定线程的许可不可用,则使其可用
    }
//唤醒后继节点--共享模式
private void doReleaseShared() {
        
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {//如果队列头不为空且队列头不等于队列尾,如果队列头等于队列尾,则队列中就一个节点,循环就没意思了
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);//唤醒后继节点
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

如何保证state变量的维护与等待队列的一致性?

因为获取许可与释放许可的过程中,会同时涉及state变量的维护和等待队列的维护。为了减少操作的复杂性, AQS是基于模板设计模式实现的,

通过对模板方法的调用,可以保证对state变量和等待队列维护的一致性。

AQS模板方法列表:

方法名称描述
void acquire(int arg)独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法
void acquireInterruptibly(int arg)与tryAcquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException
boolean tryAcquireNanos(int arg, long nanosTimeout)在acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时实现内没有获取到同步状态,那么将会返回false,如果获取到了就返回true
boolean release(int arg)独占式释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒
void acquireShared(int arg)共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态。
void acquireSharedInterruptibly(int arg)与void acquireInterruptibly(int arg)相同,该方法响应中断
boolean tryAcquireSharedNanos(int arg, long nanosTimeout)在acquireSharedInterruptibly(int arg)的基础上增加了超时限制。
boolean releaseShared(int arg)共享式释放同步状态
Collection<Thread> getQueuedThreads()获取等待在同步队列上的线程集合。

同步器提供的模板方法基本上分为3类,独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的线程等待情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。

以AQS模板方法acquire(int arg)为例,这是一个获取执行许可的方法,arg表示获取许可的数量。其源码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) && //功能1:state变量的维护,开发者需要覆写此方法
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//功能2:线程等待队列的维护,AQS覆写
        selfInterrupt();//不响应中断,也就是不抛出异常,目前不在讨论范围,后面会介绍
}

可以看到在模板方法,分别调用了state变量维护相关方法tryAcquire(arg)以及等待队列维护相关方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg))。

对于线程等待队列的维护,AQS已完成,我们不要考虑。

对于state变量的维护,调用的是需要开发者覆写的tryAcquire(int arg)方法实现。在覆写过的tryAcquire(int arg)方法中,通过调用getState()、setState()、compareAndSetState(int expect, int update)方法,从而实现对state变量的维护与访问。

模板方法与需要开发者覆写的方法的对应关系

AQS中定义了8个模板方法,对应4个需要开发者覆写的方法:

组件类型模板方法需要覆写的方法
独占式同步组件

void acquire(int arg)

boolean tryAcquire(int arg)

void acquireInterruptibly(int arg)

boolean tryAcquireNanos(int arg, long nanosTimeout)

boolean tryAcquireNanos(int arg, long nanosTimeout)

boolean tryRelease(int arg)

共享式同步组件

void acquireShared(int arg)

int tryAcquireShared(int arg)

void acquireSharedInterruptibly(int arg)

boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

boolean releaseShared(int arg)

boolean tryReleaseShared(int arg)

此外,还需要一个isHeldExclusively()方法需要覆写,不过其没有对应的模板方法。

通常情况下,我们实现的同步组件要不就是独占式,要不就是共享式,但是也有例外,例如ReadWriteLock同时实现了独占式与共享式。

因此当我们需要实现一个独占式同步组件时,只需要覆写AQS的tryAcquire和tryRelease即可;当实现一个共享式同步组件的时候,只需要实现tryAcquireShared和tryReleaseShared即可。isHeldExclusively()是可选的。默认情况下,需要覆写的方法的实现都是抛出一个UnsupportedOperationException。

编写同步组件需要注意的地方

1)使用新的接口和实现包装同步组件:在我们编写一个同步组件的时候,例如我们想实现一个独占锁,假设为Sync,其继承了AQS。只需要在Sync类中覆写tryRelease和tryAcquire即可,但是由于继承AQS的时候,会把tryAcquireShared、tryReleaseShared等共享锁方法也继承下来。而Sync并不会实现这些共享式同步组件的方法,因为Sync只是一个独占锁而已,从业务含义上,因此应该将这些方法屏蔽,从而防止用户误操作。按照最佳实现,屏蔽的方式是定义一个新的接口,假设用Mutex表示,这个接口只定义了独占锁相关方法,再编写一个类MutexImpl实现Mutex接口,而对于同步组件Sync类的操作,都封装在MutexImpl中。

2)同步组件推荐定义为静态内部类:因为某个同步组件通常是为实现特定的目的而实现,可能只适用于特定的场合。如果某个同步组件不具备通用性,我们应该将其定义为一个私有的静态内部类。结合第一点,我们编写的同步组件Sync应该是MutexImpl的一个私有的静态内部类。

使用AQS

只有掌握了同步器的工作原理才能更加深入的理解并发包中的其他并发组件。所以下面通过一个独占锁的示例来深入了解一下同步器的工作原理。顾名思义,独占锁就是同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

案例代码:

Mutex接口:

//不可重入的独占锁接口
public interface Mutex {
    //获取锁
    public void lock();
    //释放锁
    public void release();
}

MutexImpl

//实现
public class MutexImpl implements Mutex{
    // 仅需要将操作代理到Sync上即可
    private Sync sync=new Sync();
    @Override
    public void lock() {
        sync.acquire(1);
    }
 
    @Override
    public void release() {
        sync.release(1);
    }
 
    //独占式同步组件实现
    private static class Sync extends AbstractQueuedSynchronizer{
 
        @Override
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0,1);
        }
 
        @Override
        protected boolean tryRelease(int arg) {
            return compareAndSetState(1,0);
        }
    }
}

 

测试类MutexMain:

public class MutexMain {
    public static void main(String[] args) throws InterruptedException {
        Mutex mutex=new MutexImpl();
        for (int i = 0; i <5 ; i++) {
            new MutexThread("线程"+i,mutex).start();
        }
    }
    static class MutexThread extends Thread{
        private Mutex mutex;
 
        public MutexThread(String name,Mutex mutex) {
            this.mutex = mutex;
            this.setName(name);
        }
 
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"启动..");
            mutex.lock();
            System.out.println(Thread.currentThread().getName()+"获取锁成功..");
            try {
                System.out.println(Thread.currentThread().getName()+"开始执行,当前时间:"+new Date().toLocaleString());
                Thread.currentThread().sleep(1000);//假设线程执行需要1秒钟
                System.out.println(Thread.currentThread().getName()+"结束执行,当前时间:"+new Date().toLocaleString());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println(Thread.currentThread().getName()+"释放锁..");
                mutex.release();
            }
 
        }
    }
}

 

案例代码某次输出:

线程1启动..

线程1获取锁成功..

线程0启动..

线程2启动..

线程3启动..

线程4启动..

线程1开始执行,当前时间:2016-6-25 15:08:06

线程1结束执行,当前时间:2016-6-25 15:08:07

线程1释放锁..

线程0获取锁成功..

线程0开始执行,当前时间:2016-6-25 15:08:07

线程0结束执行,当前时间:2016-6-25 15:08:08

线程0释放锁..

线程2获取锁成功..

线程2开始执行,当前时间:2016-6-25 15:08:08

线程2结束执行,当前时间:2016-6-25 15:08:09

线程2释放锁..

线程3获取锁成功..

线程3开始执行,当前时间:2016-6-25 15:08:09

线程3结束执行,当前时间:2016-6-25 15:08:10

线程3释放锁..

线程4获取锁成功..

线程4开始执行,当前时间:2016-6-25 15:08:10

线程4结束执行,当前时间:2016-6-25 15:08:11

线程4释放锁..

可以看到,我们的独占锁的确是起作用了,任意一时刻只有一个线程在运行。

请读者注意输出结果线程启动的顺序:1,0,2,3,4。线程1先获取到了锁并执行,而0、2、3、4被加入到了等待队列。而后面获取到锁的顺序也是0,2,3,4。这是因为AQS内部是使用一个FIFO队列,所以先进入等待队列的先获取到锁。

不可重入演示

public class MutextNoReentryMain {
    public static void main(String[] args) {
        Mutex mutex=new MutexImpl();
        mutex.lock();
        mutex.lock();//重复lock
        System.out.println("运行结束");
    }
}

上述代码中,永远不会打印出"运行结束"这句话,程序会一直阻塞,因为我们的锁是不可重入的。

说明:所谓不可重入,指的是,一个线程在释放锁之前,不能再次获取这个锁。上述代码中,第一次调用lock时,主线程获取到锁,可以运行,可以在主线程第二次获取锁的时候,因为锁已经被占用了,所以第二次无法获取。由于我们对于一个线程无法获取锁时,就会对其进行阻塞,并加入等待队列。因此第二次获取不到锁,其结果是导致主线程被阻塞了,最终程序就会一直被阻塞。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值