java并发编程4.2AQS详解

环境:

jdk1.8

摘要说明:

上一章节我们主要阐述了显示锁的使用详解

本章节主要阐述下AQS原理及其使用:

1.模版方法设计模式

2.AQS详解

3.AQS实战

步骤

1.模版方法模式

模版方法设计模式本质上就是在父类中编排主流程,将步骤实现延迟到子类去实现。主类中需要定义各个步骤的方法但不具体实现,但主类中还需要规定框架方法来组合这些方法来确定主流程;

举例:

消息发送我们可以普遍认为需要有发送方,接收方,发送内容,发送时间,发送方式;其次我们可以定义框架方法来确定发送流程即确定发送方--》确定接收方--》发送内容编写--》确定发送时间--》确定发送方式;

父类代码如下:

package pers.cc.lockAndAQS;

import java.util.Date;

/**
 * 使用模版方法模式写一个消息发送
 * 
 * @author cc
 *
 */
public abstract class MessageSendTmep {
    /**
     * 确定发送方
     */
    public abstract void from();

    /**
     * 确定接收方
     */
    public abstract void to();

    /**
     * 确定发送内容
     */
    public abstract void content();

    /**
     * 确定发送时间
     */
    public void date() {
        System.out.println(new Date());
    }

    /**
     * 确定发送方式
     */
    public abstract void send();

    /**
     * 框架方法,即将子方法的实现交给子类去实现,定义出方法组合
     */
    public void messageSend() {
        from();
        to();
        content();
        date();
        send();
    }
}

子类代码如下:

package pers.cc.lockAndAQS;

/**
 * 实现发送短险
 * 
 * @author cc
 *
 */
public class SendSms extends MessageSendTmep {

    @Override
    public void from() {
        System.out.println("18888888888");

    }

    @Override
    public void to() {
        System.out.println("16666666666");

    }

    @Override
    public void content() {
        System.out.println("hello world");

    }

    @Override
    public void send() {
        System.out.println("电信发送");

    }

    public static void main(String[] args) {
        MessageSendTmep mt = new SendSms();
        mt.messageSend();
    }
}

运行结果:

18888888888
16666666666
hello world
Mon Mar 04 16:22:11 CST 2019
电信发送

总结

模版方法模式其实是一个比较容易而且很好理解的模式,主要流程如下:

1、先将主流程框架逻辑设计完成

2、再实现各模块小步骤。

3、不能确实的步骤,作为虚拟方法,甩锅给子类实现。

4、子类只需要聚焦自己的小步骤逻辑。

需要注意以下几点:

1. 保护抽象类中定义算法顺序的方法不被子类修改。

2. 分离可变及不可变部分,让子类自己决定可变部分的实现。

3. 让算法的具体实现对子类开放,对其他类关闭。

2.AQS初体验

AQS全称AbstractQueuedSynchronizer,API上给的定义是提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。

首先我们可以看看有多少工具类的实现用到了抽象类AQS,我们经常使用的ReentrantLock、CountDownLatch等均是AQS的实现:

下面我们先解读CountDownLatch的源码来了解下AQS的具体应用,其中CountDownLatch的具体使用请参考之前的文章

package java.util.concurrent;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CountDownLatch {
    /**
     * 定义一个共享信号量
     *
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        /**
         * 通过父类AQS的setState方法来编写构造方法
         * 
         * @param count
         */
        Sync(int count) {
            setState(count);
        }

        /**
         * 通过父类AQS的getState来获取信号量
         * 
         * @return
         */
        int getCount() {
            return getState();
        }

        /**
         * 实现父类AQS定义的tryAcquireShared方法,该方法为共享式获取信号量;此处实现除非信号量为0,否则获取失败,线程进入排队
         */
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        /**
         * 实现父类AQS的tryReleaseShared方法,该方法为共享式释放信号量方法
         */
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    /**
     * 初始化时同时初始化共享信号量
     * 
     * @param count
     */
    public CountDownLatch(int count) {
        if (count < 0)
            throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    /**
     * 调用AQS定义的acquireSharedInterruptibly获取信号量,
     * 但上述重写了AQS的tryAcquireShared方法就导致调用此方法的线程全部堵塞
     * 
     * @throws InterruptedException
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    /**
     * 调用AQS定义的acquireSharedInterruptibly获取信号量,
     * 但上述重写了AQS的tryAcquireShared方法就导致调用此方法的线程全部堵塞,超时则返回
     * 
     * @param timeout
     * @param unit
     * @return
     * @throws InterruptedException
     */
    public boolean await(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    /**
     * 调用重新的releaseShared方法跟新信号量
     */
    public void countDown() {
        sync.releaseShared(1);
    }

    /**
     * 获取信号量大小
     * 
     * @return
     */
    public long getCount() {
        return sync.getCount();
    }

    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

通过上述注释我们可以看到,CountDownLatch的实现原理就是利用AQS对信号量的控制:

1、定义了一个内部类实现了抽象类AQS初始化同步状态大小即信号量大小;

2、后续定义共享式释放方法提供修改信号量大小的方法,此方法使用了CAS具有原子性;

3、定义共享式获取方法达到多线程获取信号量时进行堵塞,除非信号量大小为0;

其中的关键点就是:

tryAcquireShared(共享获取方法):通过判断信号量大小是否为0来堵塞获取的线程,达到CountDownLatch的await()堵塞效果;

3.AQS设计思路:

AQS本质提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。

核心设计大多数都是依赖定义的同步状态值state(也被成为信号量),其本质是一个原子的int值;子类需要定义该同步状态值state改变方法,及state的获取和释放方法;且state值只能够使用getState()、setState(int)和compareAndSetState(int, int)方法操作的原子更新的int值以便实现队列阻塞机制;

当线程获取同步状态值state失败时,线程将进入阻塞队列,直到state值发生变更重新唤醒;

4.AQS堵塞队列数据模型

同步器和同步队列:

增加节点到尾部:

首节点的变化即释放节点:

AQS保存堵塞线程的数据模型如上:

  • 通过同步器每次添加线程到双向链表的尾部(tail);
  • 且线程节点间上下(prev/next)双向关联;
  • 添加尾部节点时使用的是cas原子操作
  • 释放堵塞线程先从头部进行释放,且将该线程的next线程设置成头部节点

Node节点(保存线程节点):

节点的五种状态值:

  • 0:初始状态
  • CANCELLED(1)线程等待超时或者被中断了,需要从队列中移走
  • SIGNAL(-1)后续的节点等待状态,当前节点,通知后面的节点去运行
  • CONDITION(-2) 当前节点处于等待队列
  • PROPAGATE(-3)共享,表示状态要往后面的节点传播

其他:

  • Node SHARED表示共享模式下节点
  • Node EXCLUSIVE表示独占模式下节点
  • int waitStatus:标识节点状态即上述5中状态
  • Node prev:上一线程节点
  • Node next:下一线程节点
  • Thread thread:节点线程
  • Node nextWaiter:条件模式下的等待节点
  • boolean isShared():判断节点是否是共享模式下
  • Node predecessor():获取上一节点,可能返回null

5.AQS的两种模式(独占和共享):

独占模式就是表示同一时刻只能有一个线程获取同步状态state,其他状态获取同步状态则返回false即进入堵塞队列;子类需实现tryAcquire方法来决定如何独占;代表子类为ReentrantLock;

共享模式表示可以有多个线程获取同步状态state大于0即不进入堵塞,知道state减小到小于0才进入堵塞队列:子类需是实现tryAcquireShared来获取同步状态state;代表子类为CountDownLatch;

从这两种模式我们可以将AQS的方法结合模版方法模式区分为以下几种:

框架方法:

独占式获取:

  • accquire
  • acquireInterruptibly
  • tryAcquireNanos

独占式释放锁:

  • release

共享式获取:

  • acquireShared
  • acquireSharedInterruptibly
  • tryAcquireSharedNanos

共享式释放锁:

  • releaseShared

需要子类覆盖的方法:

流程方法:

  • 独占式获取  tryAcquire
  • 独占式释放  tryRelease
  • 共享式获取 tryAcquireShared
  • 共享式释放  tryReleaseShared
  • 同步器是否处于独占模式  isHeldExclusively

同步状态state操作方法:

  • getState:获取当前的同步状态
  • setState:设置当前同步状态
  • compareAndSetState 使用CAS设置状态,保证状态设置的原子性

其中同步状态流程如下:

流程说明:

  1. 调用如tryAcquireNanos框架方法获取同步状态state;
  2. 通过子类实现的如tryAcquire方法获取state值
  3. 判断state值是否大于0;大于则获取成功直接退出返回;小于则生成堵塞节点
  4. 后续state释放,判断堵塞队列头部线程是否中断,中断则变更头部节点,反之则获取同步状态state,同时加下一节点设置成头节点;

6.AQS的ConditionObject

前面我们介绍显示锁的等待唤醒靠lock的condition;他的实现其实就是依赖于AQS的ConditionObject;

一个Conditon包含一个等待队列:

同步队列和等待队列是同时存在的,一个同步器可以控制多个等待队列:

condition的await方法:本质上就是将线程从同步节点构造成等待节点移入等待队列;

 condition的singal方法:本质上就是将线程从等待队列中构造成同步节点移到等待队列尾部;

7.实战

我们使用AQS实现一个简单的独占锁:

package pers.cc.lockAndAQS;

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

import pers.cc.tools.SleepTools;

/**
 * 实现AQS构造一个独占锁
 * 
 * @author cc
 *
 */
public class SelfLock implements Lock {
    /**
     * 构建一个内部类来实现AQS
     * 
     * @author cc
     *
     */
    private static class Sync extends AbstractQueuedSynchronizer {
        /**
         * 判断state()大小是否为1,为1则被占用
         */
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        /**
         * 获取信号量
         */
        protected boolean tryAcquire(int arg) {
            // 原子操作设置state值为1,成功返回true
            if (compareAndSetState(0, 1)) {
                // 设置独占线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 释放信号量。若当前信号量未被占用则抛出异常,反之清空占用线程,设置state为0
         */
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new UnsupportedOperationException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sycn = new Sync();

    @Override
    public void lock() {
        // 调用框架方法获取1个信号量
        sycn.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sycn.acquireInterruptibly(1);

    }

    @Override
    public boolean tryLock() {
        return sycn.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit)
            throws InterruptedException {
        return sycn.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sycn.release(1);

    }

    @Override
    public Condition newCondition() {
        return sycn.newCondition();
    }

    public static void main(String[] args) {
        Lock lock = new SelfLock();

        class Worker extends Thread {
            public void run() {
                while (true) {
                    lock.lock();
                    try {
                        SleepTools.second(1);
                        System.out.println(Thread.currentThread().getName());
                        SleepTools.second(1);
                    }
                    finally {
                        lock.unlock();
                    }
                    SleepTools.second(2);
                }
            }
        }
        // 启动10个子线程
        for (int i = 0; i < 10; i++) {
            Worker w = new Worker();
            w.setDaemon(true);
            w.start();
        }
        // 主线程每隔1秒换行
        for (int i = 0; i < 20; i++) {
            SleepTools.second(1);
            System.out.println();
        }
    }
}

运行结果可以看到每次只有一个线程进行打印;

8.总结

AQS本身提供了一个框架,具体要看子类如何去实现;其框架方法主要都是通过同步器来控制,总的来说同步器中包含一个可表征同步状态的变量可操作该变量的方法集,以及可阻塞或唤醒其他来修改该状态的线程的方法集。互斥锁,读写锁,信号量,屏障,事件指示器等等都是同步器。

学习AQS主要学习他的思想,进而了解其子类的实现;

9.源码

https://github.com/cc6688211/concurrent-study.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值