Jdk1.5中包含了并发大神Doug Lea写的并发工具包java.util.concurrent,这个工具包中包含了显示锁和其他的实用同步组件。Doug Lea在构建锁和组件的时候,大多是以队列同步器(AbstractQueuedSynchronizer)为基础的,因此AbstractQueuedSynchronizer可以看作是并发包的基础框架。因此掌握了AbstractQueuedSynchronizer的实现原理,也就掌握了大多数并发组件的实现原理。

  AbstractQueuedSynchronizer使用一个int变量state表示同步状态,使用一个隐式的FIFO队列(隐式队列就是并没有声明这样一个队列,只是通过每个节点记录它的上个节点和下个节点来从逻辑上产生一个队列)来完成阻塞线程的排队。

  AQS是一个抽象类,当我们要构建一个同步组件的时候,需要定义一个子类继承AQS,这里应用了模板方法设计模式,我们简单复习一下模板模式:

  1. 模板模式由一个抽象类和一个实现类组成,抽象类中主要有三类方法:
  2. 模板方法:实现了算法主体框架,供外部调用。里面会调用原语操作和钩子操作。
  3. 原语操作:即定义的抽象方法,子类必须重写。
  4. 钩子操作:和原语操作类似,也是供子类重写的,区别是钩子操作子类可以选择重写也可以选择不重写,如果不重写则使用抽象类默认操作,通常是一个空操作或抛出异常。

  在AQS中没有原语操作,也就是说自定义的子类继承AQS后,不会强制子类重写任何方法。AQS只提供了若干钩子操作,这些钩子操作的默认实现都是直接抛出异常。子类不需要重写所有的钩子操作,只需要根据要构建的同步组件的类型来决定要调用AQS中的哪些模板方法,再实现这些模板方法中用到了的钩子操作即可。

  AQS中可供子类重写的钩子操作有:

方法名称 描述
boolean tryAcquire(int arg) 独占式获取同步状态,成功返回true,失败返回false。
boolean tryRelease(int arg) 独占式释放同步状态,成功返回true,失败返回false。
int tryAcquireShared(int arg) 共享式获取同步状态,获取成功则返回值>=0
boolean tryReleaseShared(int arg) 共享式释放同步状态,成功返回true,失败返回false。
boolean isHeldExclusively() 判断同步器是否在独占模式下被占用,一般用来表示同步器是否被当前线程占用

可以看到可以重写的钩子操作既有独占式同步状态的获取与释放,也有共享式同步状态的获取与释放,这样就能支持构建不同类型的同步组件,如ReentrantLock使用时同一时刻只有一个线程可以获得锁,因此就可以通过重写tryAcquire和tryRelease实现;而Semaphore在同一时刻可以有多个线程获得许可,因此就可以通过重写tryAcquireShared(int arg)和tryReleaseShared(int arg)实现,事实上,这两个同步组件正是这么实现的。

在子类重写钩子操作的时候,可以调用AQS中的以下方法来获取/设置AQS的同步状态state:

方法 描述
int getState() 获取当前同步状态
void setState(int newState) 设置当前同步状态
boolean compareAndSetState(int expect, int update) 使用CAS设置当前状态,保证状态更新的原子性

子类重写相关钩子操作后,AQS中提供的模板方法才能正常调用(如果模板方法中使用的钩子方法没有被子类重写,将抛出异常),AQS中提供的模板方法有(这里列出了所有的模板方法,只挑了比较常用了写了描述,其他的可以自行查看源码注释):

方法 描述
void acquire(int arg) 独占式获取同步状态,该方法会调用子类重写的tryAcquire(int arg),如果tryAcquire返回true则该方法直接返回,否则先将当前线程加入同步队列的尾部,然后阻塞当前线程
void acquireInterruptibly(int arg) 和acquire类似,只是当线程获取同步状态失败被阻塞后,可以响应中断,收到中断后将会取消获取同步状态
boolean tryAcquireNanos(int arg, long nanosTimeout) 在acquireInterruptibly的基础上加了超时限制,如果在超时时间内获取到同步状态返回true,否则返回false
boolean release(int arg) 独占式释放同步状态,该方法会在释放同步状态后将第一个节点(对应刚刚释放同步状态的线程)的后继节点对应的线程唤醒。
void acquireShared(int arg) 共享式获取同步状态,该方法会调用子类重写的tryAcquireShared(int arg),如果tryAcquireShared返回true则该方法直接返回,否则先将当前线程加入同步队列的尾部,然后阻塞当前线程
void acquireSharedInterruptibly(int arg) 和acquireShared类似,只是当线程获取同步状态失败被阻塞后,可以响应中断,收到中断后将会取消获取同步状态
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) 在acquireSharedInterruptibly的基础上加了超时限制,如果在超时时间内获取到同步状态返回true,否则返回false
boolean releaseShared(int arg) 共享式的释放同步状态
boolean hasQueuedThreads()  
boolean hasContended()  
Thread getFirstQueuedThread()  
boolean isQueued(Thread thread)  
boolean hasQueuedPredecessors()  
int getQueueLength()  
Collection<Thread> getQueuedThreads()  
Collection<Thread> getExclusiveQueuedThreads()  
Collection<Thread> getSharedQueuedThreads()  
boolean owns(ConditionObject condition)  
boolean hasWaiters(ConditionObject condition)  
int getWaitQueueLength(ConditionObject condition)  
Collection<Thread> getWaitingThreads(ConditionObject condition)  

这里我们借助一个例子来加深对AQS的理解,我们用AQS来自定义一个独占锁MutexLock(独占锁就是同一时刻只有一个线程可以获得锁,而其他线程只能被阻塞,一直到当前线程释放了锁,其他线程才有机会再获取锁):

  1. package com.gome;
  2.  
  3. import java.util.concurrent.TimeUnit;
  4. import java.util.concurrent.locks.AbstractQueuedSynchronizer;
  5. import java.util.concurrent.locks.Condition;
  6. import java.util.concurrent.locks.Lock;
  7.  
  8. public class MutexLock implements Lock{
  9. private MutexSynchronizer synchronizer=new MutexSynchronizer();
  10.  
  11. private static class MutexSynchronizer extends AbstractQueuedSynchronizer{
  12. /**
  13. * @param unused 这个参数是用来传同步状态的累加值的,因为我们实现的是独占锁,
  14. * 因此这个参数实际用不到,我们在方法里累加值恒为1
  15. */
  16. @Override
  17. protected boolean tryAcquire(int unused) {
  18. /**
  19. * 用CAS来更新AQS的同步状态,如果原值是0则更新为1代表已经有线程获取了独占锁
  20. */
  21. if (compareAndSetState(0, 1)) {
  22. setExclusiveOwnerThread(Thread.currentThread()); //设置当前独占锁的所有者线程
  23. return true;
  24. }
  25. return false;
  26. }
  27.  
  28. /**
  29. * @param unused 这个参数是用来传同步状态的递减值的,因为我们实现的是独占锁,
  30. * 因此这个参数实际用不到,我们在方法里递减值恒为1
  31. */
  32. @Override
  33. protected boolean tryRelease(int unused) {
  34. //如果当前AQS同步状态是0,说明试图在没有获得同步状态的情况下释放同步状态,直接抛异常
  35. if (getState()==0)
  36. throw new IllegalMonitorStateException();
  37.  
  38. //这里不需要CAS而是直接把同步状态设置为0,因为我们实现的是独占锁,正常情况下会执行释放操作的线程只有同步状态的所有者线程
  39. setState(0);
  40. setExclusiveOwnerThread(null);
  41. return true;
  42. }
  43.  
  44. protected Condition newCondition() {
  45. return new ConditionObject();
  46. }
  47. }
  48.  
  49. @Override
  50. public void lock() {
  51. synchronizer.acquire(1);
  52. }
  53.  
  54. @Override
  55. public void lockInterruptibly() throws InterruptedException {
  56. synchronizer.acquireInterruptibly(1);
  57. }
  58.  
  59. @Override
  60. public boolean tryLock() {
  61. return synchronizer.tryAcquire(1);
  62. }
  63.  
  64. @Override
  65. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  66. return synchronizer.tryAcquireNanos(1, unit.toNanos(time));
  67. }
  68.  
  69. @Override
  70. public void unlock() {
  71. synchronizer.release(1);
  72. }
  73.  
  74. @Override
  75. public Condition newCondition() {
  76. return synchronizer.newCondition();
  77. }
  78.  
  79. }

解释:

我们让MutexLock实现了Lock接口,因此MutexLock就必须实现以下方法:

方法名称 描述
void lock() 获取锁
void lockInterruptibly() throws InterruptedException 可中断地获取锁,在线程获取锁的过程中可以响应中断
boolean tryLock()   尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断
void unlock(); 释放锁
Condition newCondition(); 获取等待组件,等待组件实现类似于Object.wait()方法的功能

然后我们在MutexLock中定义内部类MutexSynchronizer继承AQS类,可以看到MutexLock中的方法本身都没有做任何操作,都是把请求委托给MutexSynchronizer的实例。

为了实现MutexLock中的方法,我们需要调用AQS的acquire、acquireInterruptibly、tryAcquire、tryAcquireNanos、release方法,这几个方法都是独占式获取、释放同步状态的方法,因此子类MutexSynchronizer需要重写和独占同步状态获取、释放相关的钩子操作:tryAcquire、tryRelease。

只需要以上的代码,我们就拥有了一个可以使用的独占锁。可以看到,需要我们自己写的主要就是tryAcquire()和tryRelease()这两个方法,其他的操作,如对获取锁失败线程的阻塞、唤醒,都是AQS替我们实现的。

这一篇就到这里,这篇只介绍作为一个同步组件的构建者,如何使用AQS来构建我们自己的同步组件。

下一篇我们将介绍AQS的底层实现,探究AQS是如何利用内部的同步队列帮我们实现同步组件的。

Java显式锁学习总结之二:使用AbstractQueuedSynchronizer构建同步组件的更多相关文章

  1. Java显式锁学习总结之六:Condition源码分析

    概述 先来回顾一下java中的等待/通知机制 我们有时会遇到这样的场景:线程A执行到某个点的时候,因为某个条件condition不满足,需要线程A暂停:等到线程B修改了条件condition,使con ...

  2. Java显式锁学习总结之五:ReentrantReadWriteLock源码分析

    概述 我们在介绍AbstractQueuedSynchronizer的时候介绍过,AQS支持独占式同步状态获取/释放.共享式同步状态获取/释放两种模式,对应的典型应用分别是ReentrantLock和 ...

  3. Java显式锁学习总结之三:AbstractQueuedSynchronizer的实现原理

    概述 上一篇我们讲了AQS的使用,这一篇讲AQS的内部实现原理. 我们前面介绍了,AQS使用一个int变量state表示同步状态,使用一个隐式的FIFO同步队列(隐式队列就是并没有声明这样一个队列,只 ...

  4. Java显式锁学习总结之四:ReentrantLock源码分析

    概述 ReentrantLock,即重入锁,是一个和synchronized关键字等价的,支持线程重入的互斥锁.只是在synchronized已有功能基础上添加了一些扩展功能. 除了支持可中断获取锁. ...

  5. Java显式锁学习总结之一:概论

    我们都知道在java中,当多个线程需要并发访问共享资源时需要使用同步,我们经常使用的同步方式就是synchronized关键字,事实上,在jdk1.5之前,只有synchronized一种同步方式.而 ...

  6. Java显式锁

    Java 显式锁. 一.显式锁 什么是显式锁? 由自己手动获取锁,然后手动释放的锁. 有了 synchronized(内置锁) 为什么还要 Lock(显示锁)? 使用 synchronized 关键字 ...

  7. “全栈2019”Java多线程第三十二章:显式锁Lock等待唤醒机制详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. Java并发编程之显式锁机制

    我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加 ...

  9. “全栈2019”Java多线程第三十一章:中断正在等待显式锁的线程

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

随机推荐

  1. IOS三种归档(NSKeyArchieve)的总结

    IOS三种归档(NSKeyArchieve)的总结 归档是一种IOS中常用来存储文件的一种方法,在面向对象的语言中,归档也就实际上可以将一切对象存储在文件中,以下是IOS开发中常见的三种文件归档方式, ...

  2. Unity中使用扩展方法解决foreach导致的GC

    对于List这种顺序表,我们解决的时候还是可以使用for代替foreach即可.但是对于非顺序表,比如Dictionary或者Set之类,我们可以扩展方法Foreach,ForeachKey和Fore ...

  3. Nodejs中Mongodb使用

    Mongodb使用 打开解压后的Mongodb文件夹,新建data.logs文件夹,并在logs文件夹中新建mongodb.log文档. 添加后Mongod文件夹示意图: 用cmd命令行启动Mongo ...

  4. ipyparallel 中的 pi的求法

    1.PI的求法的数学依据 如图,可以看见在边长为1的正方形里面,有一个1/4圆,我们随机在正方形中取点,点在圆内的概率和点在正方形内的概率之比正好为两者的面积之比.这样就有在圆内的点的数目比所有点的数 ...

  5. iOS 之 后台下载,前台显示模式,双 block

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //耗时的操作 NSURL *url ...

  6. XML解析之DOM解析技术案例

    Java代码: package com.xushouwei.xml; import java.io.File; import javax.xml.parsers.DocumentBuilder; im ...

  7. 在官网下载了最新版的PHP,解压后的安装包里为什么没有php5isapi.dll这个dll文件?

    因为自PHP 5.3.1版本开始,PHP便已不在支持ISAPI模式,所以你在PHP5.3.1版本以上的php目录中看不到php5isapi.dll文件. 那么,IIS6下跑PHP 5.3.1以上版本时 ...

  8. mySQL内存及虚拟内存优化设置

    为了装mysql环境测试,装上后发现启动后mysql占用了很大的虚拟内存,达8百多兆.网上搜索了一下,得到高人指点my.ini.再也没见再详细的了..只好打开my.ini逐行的啃,虽然英文差了点,不过 ...

  9. vue.js学习笔记(二):如何加载本地json文件

    在项目开发的过程中,因为无法和后台的数据做交互,所以我们可以自建一个假数据文件(如data.json)到项目文件夹中,这样我们就可以模仿后台的数据进行开发.但是,如何在一个vue.js 项目中引入本地 ...

  10. Fiddler 模拟请求的操作方法

    此文记录使用Fidder Web Debugger工具,模拟请求的操作步骤! 首先简述一下fiddler的使用: 1.下载安装Fidder抓包工具. 2.打开fiddler发现有左边的栏有请求的url ...