AbstractQueuedSynchronizer AQS 源码分析之 如何应用

今天开始写JDK开发包源码分析的第一篇,后面每周更新一篇。先从最重要的AbstractQueuedSynchronizer (AQS 也叫队列同步器,这名字应该是因为其内部是通过队列实现同步状态管理、线程排队、等待与唤醒的) ,它是CountDownLacth、Semaphore、ReentrantLock、ReentrantReadWriteLock实现的基础。CountDownLacth、Semaphore、ReentrantLock、ReentrantReadWriteLock 都采用组合的方式使用AQS,其内部分别有各自具体实现了AQS的Syn类。下面是对应UML图。

AQS是基本模板方法设计模式实现的。一般使用者根据具体需要,组合一个实现了AQS的子类,该类重写了AQS中的相应的模板方法来满足使用者特定的同步语义。上面提到的CountDownLacth、Semaphore、ReentrantLock、ReentrantReadWriteLock都是采用这种方式实现的。 AQS提供的可重写方法如下:

/* 独占模式(排它模式)获取同步状态,获取同步状态前要检查当前线程是否可以获取。因为可能已被其它线程获取,
 * 当同步状态允许被线程多次获取时,独占模式只能被同一个线程多次获取。未获取同步状态的线程,将在同步队列中
 * 等待。
 */
 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();}

默认情况这个方法都直接抛出UnsupportedOperationException异常,使用者可以根据情况以独占或是共享的模式来完成具体的同步语义。

AQS内部的同步状态是通过state表示的,state 被volatile修饰保证了不依赖了state自身的set, get方法操作的可见性。

 /*The synchronization state.
  */
  private volatile int state;

AQS 提供了如下三个方法对应state状态进行管理

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

同步状态 state的具体语义与具体使用者有关。如在Semaphore中state表示可获取的信号量,当调用acquire方法成功获取一次后state值将减一,用完调用release方法释放后state的值将加一 ; 在CountDownLatch中 state表示调用await的方法的线程还要等待调用多少次countDown方法,该线程才能继续执行; 在ReentrantLock中state表示,线程调用lock方法的次数。后面会分别分析源码。

实现自定的同步组件时,将会调用AQS提供的模板方法,这些模板方法内部又将调用上面重写的tryAcquire、 tryRelease、 tryAcquireShared 、tryReleaseShared isHeldExclusively方法。模板方法描述如下:

/*独占模式获取同步状态,该方法会先调用重写的tryAcquire方法尝试获取同步状态,如果当前线程成功获取同步状态,
 * 该方法直接返回,否则当前线程会被封装成一个节点放入同步队列中等待
 */
public final void acquire(int arg) {//现实省去...}
/*与acquire方法类似,但该方法能响应中断,如果当前线程未能获取到同步状态,而在同步队列中,当这个线程被中断时,
 *则该抛出InterruptedException 异常并返回
 */
public final void acquireInterruptibly(int arg){//现实省去...}
/*在acquireInterruptibly方法上加了超时机制,如果当前线程在超过时间内没有获取同步状态,那么将返回false,
 *获取到返回ture
 */
public final boolean tryAcquireNanos(int arg, long nanosTimeout){//现实省去...}
/*共享式获取同步状态,该方法会先调用重写的tryAcquireShared方法尝试获取同步状态,如果当前线程成功获取同步状态,
 *该方法直接返回,否则当前线程会被封装成一个节点放入同步队列中等待
 */
public final void acquireShared(int arg) {//现实省去...}
/*以acquireShared方法类似,在其基础上增加了响应中断的功能
 */
public final void acquireSharedInterruptibly(int arg){//现实省去...}
/*在acquireSharedInterruptibly方法的基础上增加了超时限制
 */
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout){//现实省去...}
/*独占模式释放同步状态,该方法会先调用重写的tryRelease方法,如果释放同步状态成功,将同步队列中第一个节点中
 *对应线程唤醒
 */
public final boolean release(int arg) {//现实省去...}
/*共享模式释放同步状态,该方法会先调用重写的tryReleaseShared方法
 */
public final boolean releaseShared(int arg) {//现实省去...}

同步器提供的模板方法可分为二大类:独占模式获取与释放同步状态,共享模式获取与释放同步状态。

看了这么多,会应用解决实际问题才是关键。来个看一个JDK,AQS源码中给的示例吧。

import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/*
 * 互斥锁,一次只能一个线程获取到,同步状态,获取的线程不可重入
 * @author yeweigen
 */
public class Mutex implements Lock, Serializable {

    private static final Sync sync = new Sync();

    private static class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean isHeldExclusively() {
            if (getState() == 1) {
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryAcquire(int arg) {
           assert  arg == 1;
           /*此处使用原子操作是为了确保并发时只有一个线程成功获取同步状态,
            *直接采用先getState方法后setState会有问题
            */
           if (compareAndSetState(0, 1)) {
               setExclusiveOwnerThread(Thread.currentThread());
               return true;
           }
           return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            assert arg == 1;
            if (getState() == 1) {
                setState(0);
                setExclusiveOwnerThread(null);
                return true;
            }
            return false;
        }
        Condition newCondition() { return new ConditionObject(); }
    }
    @Override
    public void lock() { sync.acquire(1); }
    @Override
    public boolean tryLock()  { return sync.tryAcquire(1); }
    @Override
    public void unlock()              { sync.release(1); }
    @Override
    public Condition newCondition()   { return sync.newCondition(); }
    @Override
    public void lockInterruptibly() throws InterruptedException {
      sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
      return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    public boolean isLocked()         { return sync.isHeldExclusively(); }
}

上面实现的互斥锁Mutex,一次只能被一个线程持有。其内部类Sync通过重写AQS的tryAcquire与tryRelease方法实现锁获取与释放的语义。Mutex为不可重入的锁,下一篇文章会介绍何为可重。Mutex 中的方法,则通过调用Sync实现。

测试代码: 

/**
 * @author yeweigen
 */
public class MutexTest {

    public static void main (String [] args) {
        Mutex lock = new Mutex();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Date now = new Date();
                    System.out.println(" thread1 running now:" + now);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    lock.unlock();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Date now = new Date();
                    System.out.println(" thread2 running now:" + now);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    lock.unlock();
                }
            }
        }).start();
    }
}

可能的运行结果:

可以看到一个线程晚于另外一个线程3秒后才执行,因为第一个线程执行后sleep了3秒后才释放了之前获取的锁。下一篇文章将分析,AQS内部的实现原理与对应源码。

 

转载于:https://my.oschina.net/u/1421030/blog/1838419

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值