队列同步器AbstractQueuedSynchronizer

  • 概述
    队列同步器AbstractQueuedSynchronizer以下简称同步器,是用来构建锁或者构建其他同步组件的基础,他使用了一个int成员变量
    表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望他能够成为实现大部分同步需求的基础。
    同步器的主要使用方式是继承,子类通过继承同步器并实现他抽象方法来管理同步状态,在抽象方法的实现中免不了对同步状态的更改,这时就需要使用同步器提供的3个方法(getState(),setState(int newState),cpmpareAndSetState(int expect,int update))来进行操作,因为他们能够保证改变时安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步解耦,他仅仅是定义了若干同步状态获取和释放的方法来攻同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享方式获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock,ReentrantReadWriteLock和CountDownLatch)

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系,所示面向使用者的,他定义了使用者和锁交互接口(比如可以允许两个线程并行访问),隐藏了实现了细节;同步器面向的是所得实现者,他简化了所得实现方式,屏蔽了同步管理,线程的排队,等待与唤醒等底层操作,锁和同步器很好地隔离了使用者和实现这所关注的领域。

  • 队列同步器的接口与示例
    同步器的设计是基于模板方法模式的,也就是说,使用这需要继承同步器并重写制定的方法,随后将同步器组合在自定义的同步组件的实现中,并调用同步器提供的模板方法,而这写模板方法将会调用使用者重写的方法。
    重写同步器自定的方法时,需要使用同步器提供的如下3个方法来访问或者修改同步状态,

    • getState(): 获取当前同步状态
    • setState(int newState) : 设置当前同步状态
    • compareAndSetState(int except,int update): 使用CAS设置当前状态,该方法能够保证状态设置的原子性。

同步器可以重写的方法如下表所示:

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

实现自定义同步组件时,将会调用同步提供模板方法,这些部分模板方法描述如下:

方法名称 描述
void acquire(int arg) 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列中等待,该方法将会调用重写tryAcquire(int arg) 方法
void acquireInterruotibly(int arg) 与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态二进入同步队列中,

如果当前线程被中断,则该方法会抛出InterruptedException比返回
boolean tryAcquireNanos(int arg)|在该acquireInterruptibly(int arg)基础上增加了超时限制,如果当时线程在超时时间内没有获取到获取到同步状态,那么将会返回false,如果获取到了就返回true
boolean acquireShare(int arg)|共享式地获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占锁获取的主要区别是在同一时刻多个线程获取同步状态
void acquireShareInterruptibly(int arg)|与acquireShare(int arg)相同,该方法响应中断
boolean tryAcquireShareNanos(int arg)|在acquireShareInterruptibly(int arg)的基础上增加了超时限制
boolean release(int arg)|独占式的释放同步状态,该方法在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
boolean releaseShared(int arg)|共享式释放同步状态
Collection getQueuedThread()|获取等待在同步队列的线程集合

同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态,共享方式获取和释放同步状态和查询同步队列中等待线程的情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
只有掌握了同步器的工作原理才能更加深入理解并发包中的其它并发组件,所以下面通过一个独占锁的实例来深入理解一下同步器的工作原理。
顾名思义,独占锁就是同一时刻只能一个线程获取到锁,而其他获取锁的线程只能处于同步队列中进行等待,只有获取锁的锁释放了锁,后续的线程才能获取锁,如下面的代码清单

class Mutex implements Lock{
//静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer{
          //是否处于占用状态
          protected boolean idHeldExclusively(){
                  return getState() == 1;
          }
      
        //当状态为0的时候获取锁
        public boolean tryAcquire(int acquires){
                 if(compareAndSetState(0,1)){
                      setExclusiveOwnerThread(Thread.currentThread());
                      return true;
                  } 
                 return false;
        }
      
      //释放锁,将状态设置为0
      public boolean tryAcquire(int release){
            if(getState() == 0) throw new IllegaMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
      }

    //返回一个Condition,每个condition都包含了一个condition队列
    Condition newCondition(){
          return new ConditionObject();
    }
}
private final Sync sync = new Sync();
public void lock(){
  sync.acquire(1);
}
public boolean tryLock(){
    return sync.tryAcquire(1);
}
public void unLock(){
    return sync.release(1);
}

public Condition newCondition(){
    return sync.newCondition();
}

public boolean isLocked(){
    return sync.isHeldExclusiveLy();
}

public boolean hasQueueThreads(){
    return sync.hasQueueThreads();
}

public void lockInterruptibly() throws InterruptedException(){
    sync.acquireInterruptibly(1);
}

public boolean tryLock(long timeout,TieUnit unit) throws InterruptedException{
    return sync.tryAcquireNanos(1,unit.toNanos(timeout));
}

}

上述实例中,独占锁Mutex是一个自定义同步组件,他在同意时刻只允许一个线程占有锁,Mutex中的定义了一个静态内部类,该内部类继承了同步器并实现独占式获取和释放同步状态,在tryAcquire(int arg)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int release)方法中只是将同步状态重置为0.用户使用Mutex时并不会直接和内部同步器直接打交道,而是调用Mutex提供的方法,在Mutex中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这就大大降低了实现一个可靠自定义同步组件的门槛。

  • 队列同步器的实现方式
    接下来将从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列,独占式同步状态的获取和释放,共享式同步状态的获取和释放以及超时获取同步状态等同步器的核心数据结构与模板方法。

1.同步队列
同步器依赖内部的同步的队列(一个FIFO的双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构建一个节点并将其加入到同步队列,同时会堵塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,将使其再次尝试获取同步状态。
同步队列中的节点用来保存获取同步状态失败的线程引用,等待状态以及前驱和后续节点,节点的属性类型与名称以及描述如下:

属性类型与名称 描述
int waitStatus 等待状态,包含如下状态:1).CANNELLED,值为1,由于再同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入改状态将不会发生变化 2).SIGNAL,值为-1,后继节点的想成处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行 3).CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取。 4).PROPAGATE 值为-3,表示下一次的共享式同步状态获取将会无条件传播下去 5.INITIAL 值为0,初始状态
Node pre 前驱节点,当节点加入到同步队列时被设置(尾部添加)
Node next 后续节点
Node nextWaiter 等待队列中的后续节点,如果当前节点是共享的,那么这个字段将会是一个SHARED常量,也就是节点类型(独占和共享)和等待队列中的后续节点共用一个字段
Thread thread 获取同步状态的线程

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


队列同步器AbstractQueuedSynchronizer_第1张图片
image.png

在上图中,同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点,试想一下,当一个线程成功地获取同步状态,其他线程将无法获取到同步状态,转而被构造成节点并加入到同步队列,而这个加入队列的过程中必须保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法;compareAndSetTail(Node except,Node update),他需要传递当前线程‘认为’的尾节点和当前节点,只有设置成功了,当前节点才正式与之前的为节点建立关联。同步器将节点加入到同步队列的过程如下图所示:

队列同步器AbstractQueuedSynchronizer_第2张图片
image.png

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功是将自己设置为首节点。


队列同步器AbstractQueuedSynchronizer_第3张图片
image.png

设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取同步状态,因此设置头节点的方法设置头节点的方法并不需要使用CAS来保证,他只需要将首节点设置为原来首节点的后继节点并断开原首节点的next引用就可以。

2.独占式同步状态获取和释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程的获取同步状态失败后进入同步状态中,后继对线程进行中断操作,线程不会从同步队列中移出,该方法代码如下:

public final void acquire(int arg){
      if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
      selfInterrupt();
}

上述代码主要完成同步状态的获取,节点构造,加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用自定义的同步器实现tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node EXCLUSIVE,同一时刻只能有一个线程成功获取状态)并通过addWaiter(Nod).

你可能感兴趣的:(队列同步器AbstractQueuedSynchronizer)