`
森林的天空
  • 浏览: 15239 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

JAVA并发编程学习笔记之AQS简介

 
阅读更多

1、引言

JAVA内置的锁(使用同步方法和同步块)一直以来备受关注,其优势是可以花最小的空间开销创建锁(因为每个JAVA对象或者类都可以作为锁使用)和最少的时间开销获得锁(单线程可以在最短时间内获得锁)。线程同步越来越多地被用在多处理器上,特别是在高并发的情况下,然而,JVM内置锁表现一般,而且不支持任何公平策略。从JAVA 5开始在java.util.concurrent包中引入了有别于Synchronized的同步框架。

下面谈谈它的设计思路:

设计一个同步器至少应该具以下有两种操作:一个获取方法,如果当前状态不允许,将一直阻塞这个线程;一个释放方法,修改状态,让其他线程有运行的机会。并发包中并没有为同步器提供一个统一的API,获取和释放方法在不同的类中的名称不同,比如获取方法有:Lock.lock,Semaphore.acquire, CountDownLatch.await和FutureTask.get.这些方法一般都重载有多种版本:阻塞与非阻塞版本、支持超时、支持中断。

java.util.concurrent包中有很多同步类,比如互斥锁、读写锁、信号量等,这些同步类几乎都可以用不同方式来实现,但是如果这样做,那么这样的项目充其量只能算一个二流工程。JSR166并没有生搬硬套,而是建立了一个同步中心类AbstractQueuedSynchronizer(简称:AQS)的框架,其中提供了大量的同步操作,而且用户还可以在此类的基础上自定义自己的同步类。其设计目标主要有两点:

1、提高可扩展性,用户可以自定义自己的同步类

2、最大限度地提高吞吐量,提供自定义公平策略

 

2、设计和实现

同步器的设计比较直接,前面提到包含获取和释放两个操作:
获取操作过程如下:
while (synchronization state does not allow acquire) {
    enqueue current thread if not already queued;
    possibly block current thread;
}
dequeue current thread if it was queued;
释放操作:
update synchronization state;
if (state may permit a blocked thread to acquire)
    unblock one or more queued threads;
要满足以上两个操作,需要以下3点来支持:
1、原子操作同步状态;
2、阻塞或者唤醒一个线程;

3、内部应该维护一个队列。

 

2.1同步状态

AQS用的是一个32位的整型来表示同步状态的,可以通过以下几个方法来设置和修改这个状态字段:getState(),setState(),compareAndSetState().这些方法都需要java.util.concurrent.atomic包的支持,采用CAS操作.将state设置为32位整型是一个务实的决定,虽然JSR166提供了64位版本的原子操作,但它还是使用对象内部锁来实现的,如果采用64位的state会导致同步器表现不良好。32位同步器满足大部分应用,如果确实需要64位的状态,可以使用AbstractQueuedLongSynchronizer类.AQS是一个抽象类,如果它的实现类想要想要拥有对获取和释放的控制权,那它必须实现tryAcquire和tryRelease两个方法。

  1. protected final int getState() {  
  2.     return state;  
  3. }  
  4.   
  5.   
  6. protected final void setState(int newState) {  
  7.     state = newState;  
  8. }  
  9.   
  10.   
  11. protected final boolean compareAndSetState(int expect, int update) {  
  12.     return unsafe.compareAndSwapInt(this, stateOffset, expect, update);  
  13. }  
  14.   
  15.   
  16. protected boolean tryAcquire(int arg) {  
  17.     throw new UnsupportedOperationException();  
  18. }  
  19.   
  20.   
  21. protected boolean tryRelease(int arg) {  
  22.     throw new UnsupportedOperationException();  
  23. }  

2.2阻塞

JSR166以前还没有好的阻塞和解除阻塞线程的API可以使用!只有Thread.suspend 和 Thread.resume,但这两个方法已经被废弃了,原因是有可能导致死锁。如果一个线程拥有监视器然后调用 Thread.suspend 使自已阻塞,另一个线程试图调用Thread.resume去唤醒它,那么这个线程去获取监视器时即出现死锁。直到后来出现的LockSupport解决了这个问题,LockSupport.park可以阻塞一个线程,LockSupport.unpack可以解除阻塞,调用一次park,然后调用多次unpack只会唤醒一个线程,阻塞针对线程而不是针对同步器。特别的,如果一个线程在一个新的同步器上调用pack方法有可能立即返回,因为可能有剩余的unpack存在。虽然调用多次unpack是想彻底清除阻塞状态,但这显得很笨拙,而且不划算,更有效的做法是在多次park的时候才多次unpark.

2.3队列

同步框架最重要的是要有一个同步队列,在这里被严格限制为FIFO队列,因此这个同步框架不支持基于优先级的同步策略。同步队列采用非阻塞队列毋庸置疑,当时非阻塞队列只有两个可供选择CLH队列锁和MCS队列锁.原始的CLH Lock仅仅使用自旋锁,但是相对于MSC Lock它更容易处理cancel和timeout,所以选择了CLH Lock。

CLH队列锁的优点是:进出队快,无锁,畅通无阻(即使在有竞争的情况下,总有一个线程总是能够很快插入到队尾);检查是否有线程在等待也是很容易的(只需要检查头尾指针是否相同)。最后设计出来的变种CLH Lock和原始的CLH Lock有较大的差别:

1、为了可以处理timeout和cancel操作,每个node维护一个指向前驱的指针。如果一个node的前驱被cancel,这个node可以前向移动使用前驱的状态字段。

2、第二个变动是在每个node里使用一个状态字段去控制阻塞,而不是自旋。一个排队的线程调用acquire,只有在通过了子类实现的tryAcquire才能返回,确保只有队头线程才允许调用tryAcquire。

3、另外还有一些微小的改动:head结点使用的是傀儡结点。

变种的CLH队列如下图所示:

2.4条件队列

同步框架提供了一个ConditionObject,一般和Lock接口配合来支持互斥模型,它提供类似JVM同步器的操作。条件对象可以和其他同步器有效的整合,它修复了JVM内置同步器的不足:一个锁可以有多个条件。条件结点内部也有一个状态字段,条件结点是通过nextWaiter指针串起来的一个独立的队列。条件队列中的线程在获取锁之前,必须先被transfer到同步队列中去。transfer先断开条件队列的第一个结点,然后插入到同步队列中,这个新插入到同步队列中的结点和同步队列中的结点一起排队等待获取锁。

3、用法

AbstractQueuedSynchronizer是一个采用模板方法模式实现的同步器基类,子类只需要实现获取和释放方法。子类一般不直接用于同步控制,而是采用代理模式。因为获取和释放方法一般是私有的,实现细节不必暴露出来,所以常用委派的方法来使用同步器类:在一个类的内部申请一个私有的AQS的子类,委派它的所有同步方法。
  1. class Mutex {  
  2.     class Sync extends AbstractQueuedSynchronizer {  
  3.         public boolean tryAcquire(int ignore) {  
  4.             return compareAndSetState(01);  
  5.         }  
  6.         public boolean tryRelease(int ignore) {  
  7.             setState(0);   
  8.             return true;  
  9.         }  
  10.     }  
  11.   
  12.     private final Sync sync = new Sync();  
  13.   
  14.     public void lock() {   
  15.         sync.acquire(0);   
  16.     }  
  17.   
  18.     public void unlock() {   
  19.         sync.release(0);   
  20.     }  
  21. }  
java.util.concurrent包中的所有同步工具类都依赖于AQS,其类型程序结构图如下:
 

AbstractQueuedSynchronizer类还提供了其他一些同步控制方法,包括超时和中断版的获取方法,还集成了独占模式的同步器,如acquireShared,tryReleaseShared等方法。

3.1控制公平

虽然这个队列被设计为FIFO,但并不意味着这个同步器一定是公平的,前面谈到,在tryAcquire检查之后再排队。因此,新线程完全可以偷偷排在第一个线程前面。之所以不采用FIFO,有时候是想获得更高的吞吐量,为了减少等待时间,新到的线程与队列头部的线程一起公平竞争,如果新来的线程比队头的线程快,那么这个新来的线程就获取锁。队头线程失去竞争会再次阻塞,它的继任也将会被阻塞,但这样能避免饥饿。

如果需要绝对公平,那很简单,只需要在tryAcquire方法,不在队头返回false即可。检查是否在队头可以使用getFirstQueuedThread方法。有一情况是,队列是空的,同时有多个线程一拥而入,谁先抢到锁就谁运行,这其实与公平并不冲突,是对公平的补充。

3.2同步器

JAVA并发框架是如何使用AQS的:
ReentrantLock类使用同步状态来代表持有锁的数量,当一个锁被获得,会记录获取该锁的线程身份,如果一个非当前线程试图释放锁是不合法的。该类也使用了ConditionObject类,和一些监视和检查方法。该类支持公平与非公平两种模式,是通过AQS的两个子类来实现的。
ReentrantReadWriteLock类将32位的state分成高位和低位,16位用于写锁计数,其余16位用于读锁计数。
Semaphore类使用同步状态保持当前计数,acquireShared减少计数,tryRelease的增加计数,如果state是正数就唤醒线程。
CountDownLatch类使用同步状态代表计数。所有线程都获得锁时,状态为0,就唤醒。

当然用户可以定义自己的应用程序同步器。例如:事件,集中管理的锁,基于树的障碍等。

 

参考资料:

分享到:
评论

相关推荐

    Java并发编程学习笔记

    Java并发编程是Java开发中必不可少的一部分,涉及到多线程、同步机制、线程池以及并发工具类等多个核心知识点。以下是对这些主题的详细说明: 1. **线程安全与锁 Synchronized 底层实现原理**: 线程安全是指在多...

    java并发编程:juc、aqs

    Java并发编程中的`JUC`(Java Util Concurrency)库是Java平台中用于处理多线程问题的核心工具包,它提供了一系列高效、线程安全的工具类,帮助开发者编写并发应用程序。`AQS`(AbstractQueuedSynchronizer)是JUC库中的...

    Java并发编程解析 | 解析AQS基础同步器的设计与实现

    "Java并发编程解析 | 解析AQS基础同步器的设计与实现" 在Java领域中,解决并发编程问题的关键是解决同步和互斥的问题。同步是指线程之间的通信和协作,互斥是指同一时刻只能允许一个线程访问共享资源。Java领域中有...

    Java 并发编程实战.pdf

    在学习并发编程时,理解如何启动和管理线程是基础中的基础。 其次,Java中的同步机制是并发编程中十分重要的部分。它主要通过synchronized关键字来实现,确保在任意时刻只有一个线程可以访问某个方法或者代码块。...

    Java并发编程全景图.pdf

    学习并发编程不仅是对技术的挑战,也是对程序员逻辑思维和问题解决能力的考验。随着硬件性能的持续提升和应用需求的多样化,Java并发编程的实践和理论研究将不断深化,为开发高性能、高可用性的系统提供支持。

    阿里专家级并发编程架构师课程 彻底解决JAVA并发编程疑难杂症 JAVA并发编程高级教程

    阿里专家级并发编程架构师级课程,完成课程的学习可以帮助同学们解决非常多的JAVA并发编程疑难杂症,极大的提高JAVA并发编程的效率。课程内容包括了JAVA手写线程池,UC线程池API详解,线程安全根因详解,锁与原子类...

    JAVA并发编程与高并发解决方案-并发编程四之J.U.C之AQS.docx

    ### JAVA并发编程与高并发解决方案-并发编程四之J.U.C之AQS #### 引言 《JAVA并发编程与高并发解决方案-并发编程四之J.U.C之AQS》是一篇详细介绍Java实用并发工具包(Java Util Concurrency,简称J.U.C.)中重要...

    Java并发编程:深入解析抽象队列同步器(AQS)及其在Lock中的应用

    本文深入探讨了Java并发编程的关键组件——抽象队列同步器(AQS)及其在ReentrantLock的应用。AQS是处理线程同步问题的高效工具,是Java并发编程中的核心。文章首先简要介绍了并发编程领域的先驱Doug Lea。重点在于...

    阿里专家级并发编程架构师课程-网盘链接提取码下载 .txt

    阿里专家级并发编程架构师级课程,完成课程的学习可以帮助同学们解决非常多的JAVA并发编程疑难杂症,极大的提高JAVA并发编程的效率。课程内容包括了JAVA手写线程池,UC线程池API详解,线程安全根因详解,锁与原子类...

    图灵Java高级互联网架构师第6期并发编程专题笔记.zip

    05-并发编程之深入理解Java线程-fox 06-并发编程之CAS&Atomic原子操作详解-fox 07-并发锁机制之深入理解synchronized(一)-fox 08-并发锁机制之深入理解synchronized(二)-fox 09-深入理解AQS之独占锁...

    Java并发编程原理与实战

    Spring对并发的支持:Spring的异步任务.mp4 使用jdk8提供的lambda进行并行计算.mp4 了解多线程所带来的安全风险.mp4 从线程的优先级看饥饿问题.mp4 从Java字节码的角度看线程安全性问题.mp4 synchronized保证线程...

    java并发编程-AQS和JUC实战

    ### Java并发编程-AQS和JUC实战 #### 一、ReentrantLock 重入锁 **1.1 概述** - **基本介绍**: `ReentrantLock` 是一个实现了 `Lock` 接口的可重入互斥锁,提供比 `synchronized` 更丰富的功能。与 `synchronized...

    龙果 java并发编程原理实战

    第4节学习并发的四个阶段并推荐学习并发的资料 [免费观看] 00:09:13分钟 | 第5节线程的状态以及各状态之间的转换详解00:21:56分钟 | 第6节线程的初始化,中断以及其源码讲解00:21:26分钟 | 第7节多种创建线程的...

    Java并发之AQS详解.pdf

    Java并发之AQS详解 AbstractQueuedSynchronizer(AQS)是 Java 并发编程中的一个核心组件,提供了一套多线程访问共享资源的同步器框架。AQS 定义了两种资源共享方式:Exclusive(独占)和 Share(共享)。在 AQS 中...

    java并发编程面试题

    java并发编程 基础知识,守护线程与线程, 并行和并发有什么区别? 什么是上下文切换? 线程和进程区别 什么是线程和进程? 创建线程有哪几种方式?,如何避免线程死锁 线程的 run()和 start()有什么区别? 什么是 ...

    多线程与高并发编程笔记、源码等

    标题“多线程与高并发编程笔记、源码等”表明了资源的核心内容,涵盖了多线程和高并发编程的理论与实践。多线程允许一个应用程序同时执行多个任务,而高并发则指系统能够处理大量并发请求的能力。这两个概念在现代...

    龙果java并发编程完整视频

    第4节学习并发的四个阶段并推荐学习并发的资料 [免费观看] 00:09:13分钟 | 第5节线程的状态以及各状态之间的转换详解00:21:56分钟 | 第6节线程的初始化,中断以及其源码讲解00:21:26分钟 | 第7节多种创建线程的...

    Java并发编程与高并发解决方案.txt

    Java.util.concurrent(简称J.U.C)是Java并发编程的核心库之一,提供了大量的并发容器和原子变量类,以及各种同步工具类。它简化了并发编程中的许多复杂细节,提高了开发效率和程序的稳定性。 ##### 4.2 原子类 J...

    Java 并发编程原理与实战视频

    第4节学习并发的四个阶段并推荐学习并发的资料 [免费观看] 00:09:13分钟 | 第5节线程的状态以及各状态之间的转换详解00:21:56分钟 | 第6节线程的初始化,中断以及其源码讲解00:21:26分钟 | 第7节多种创建线程的...

Global site tag (gtag.js) - Google Analytics