聊聊并发:(七)concurrent包之AbstractQueuedSynchronizer分析

前言

在上几章中,我们介绍了Java并发相关的基础知识,了解了并发中常用的关键字synchronized与volatile的使用,以及线程的基本知识,本章开始,我们开始了解一下concurrent包中相关类的使用,util.concurrent包是在JDK1.5以后引入的工具包,该包中提供了多种并发编程场景下使用的工具类,帮助我们构建编写更加强大的多线程程序。

一、concurrent包结构

首先,我们来看一下concurrent包的结构组成:

image

concurrent包位于java.util包下,其下面包含两个子包,atomic、locks以及一些并发编程的辅助工具类(阻塞队列、线程池、并发集合类等等),atomic包下是提供原子操作的辅助工具类,locks包下是锁相关的工具类,后续的章节中,我们会选择一些重要的工具类进行介绍。

首先,我们先一起来了解一下locks包下的锁。

二、Lock

首先,我们还是先来看一下locks包下的目录结构:

image

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。

在Lock接口出现之前,Java中是依靠synchronized关键字实现锁功能的,而在JDK1.5之后,concurrent包中新增了Lock接口以及相关类来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。

虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

Lock接口提供的synchronized关键字不具备的主要特性可以看一下下面这张表格:

image

Lock接口提供的方法非常的简单,我们可以看一下:

image

其中最常使用的就是lock()与unlock()方法了,我们来看一下它们的使用方式:

public class LockDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            System.out.println("get lock success");
        } finally {
            lock.unlock();
        }
    }
}

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。

不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

Lock接口有多个实现,从上面的locks包的结构可以看到,ReentrantLock、ReentrantReadWriteLock都是它的实现类,后面的文章我们会介绍这两个类,去翻看这两个类的源码,你会发现一件事情,基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。

因此,我们接下来先了解一下AbstractQueuedSynchronizer的实现机制。

三、AbstractQueuedSynchronizer队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。

可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;

同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

同步器接口使用方式

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

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

  • int getState():返回同步状态的当前值。
  • void setState(long newState):设置同步状态的值。
  • boolean compareAndSetState(int expect, int update):如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。

同步器可重写的方法与描述如下:

image

同步器提供的模板方法基本上分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况

上面我们提到,ReentrantLock的实现中,基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,下面我们就一起看一下,其具体的实现:

/**
 * Created by xuanguangyao on 2018/8/25.
 */
public class Mutex implements Lock, java.io.Serializable {

   // Our internal helper class
   private static class Sync extends AbstractQueuedSynchronizer {

       // Reports whether in locked state
       @Override
       protected boolean isHeldExclusively() {
           return getState() == 1;
       }

       // Acquires the lock if state is zero
       @Override
       public boolean tryAcquire(int acquires) {
           // Otherwise unused
           assert acquires == 1;
           if (compareAndSetState(0, 1)) {
               setExclusiveOwnerThread(Thread.currentThread());
               return true;
           }
           return false;
       }

       // Releases the lock by setting state to zero
       @Override
       protected boolean tryRelease(int releases) {
           // Otherwise unused
           assert releases == 1;
           if (getState() == 0) {
               throw new IllegalMonitorStateException();
           }
           setExclusiveOwnerThread(null);
           setState(0);
           return true;
       }

       // Provides a Condition
       Condition newCondition() {
           return new ConditionObject();
       }

       // Deserializes properly
       private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
           s.defaultReadObject();
           // reset to unlocked state
           setState(0);
       }
   }

    // The sync object does all the hard work. We just forward to it.
    private final Sync sync = new Sync();

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

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

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

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

上面的例子,是AbstractQueuedSynchronizer的源代码注释中给出的示例,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。

用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。

而ReentrantLock就是基于这种方式进行实现的,后面的文章中我们会具体介绍。

结语

本章,我们介绍了concurrent包的基本组成,以及其Lock接口与AbstractQueuedSynchronizer类,理解AbstractQueuedSynchronizer类对我们后面了解ReentrantLock、ReentrantReadWriteLock、ReadWriteLock等锁非常的重要,因为AbstractQueuedSynchronizer是构成它们的基石所在,后面的篇幅中,我将会介绍locks包下几种常见锁的使用及其实现,敬请期待!

本文参考:JDK1.8 API中文文档
《Java并发编程的艺术》

更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值