【深入理解同步器AQS】

简介: 【深入理解同步器AQS】

AQS(AbstractQueuedSynchronizer)是Java中提供的一个同步器框架,可以用来实现各种同步工具,如ReentrantLock、Semaphore、CountDownLatch等。本文将从原理、实现方式、应用场景以及代码实现等方面来深入理解AQS。

1. AQS原理

AQS通过一个FIFO队列来管理线程的竞争,队列中的每个节点都代表一个等待线程,当某个线程请求共享资源时,如果发现有其他线程已经占用了这个资源,它就会被放到队列的尾部等待;当占用资源的线程释放资源时,它会通知队列的头部节点,使它可以重新竞争资源。整个过程就是一个典型的“先进先出”的同步机制。

AQS的核心是一个volatile变量state,通过对state的操作来判断当前线程是否可以获得共享资源。当state为0时,表示当前没有线程占用资源,其他线程可以通过compareAndSetState()方法来竞争资源;当state为1时,表示当前已有线程占用资源,其他线程必须进入队列等待。

此外,AQS还提供了两个方法acquire()和release()来实现单线程对共享资源的获取和释放。

2. AQS实现方式

AQS的实现方式主要包括三个部分:同步状态的管理、等待队列的管理以及线程的阻塞与唤醒。

2.1 同步状态的管理

AQS通过一个volatile变量state来管理同步状态。在AQS中,state代表的是资源的数量,一般情况下,state等于0表示资源没有被占用,大于0表示资源已经被占用。在使用AQS实现同步器时,我们需要根据具体使用场景来赋予state合适的含义。

AQS提供了compareAndSetState()方法来对state进行原子操作,保证并发下的操作的正确性。compareAndSetState()方法的实现代码如下:

protected final boolean compareAndSetState(int expect, int update) {
    // see below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
static {
    try {
        stateOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
    } catch (Exception ex) { throw new Error(ex); }
}

在上面的代码中,我们可以看到,compareAndSetState()方法实际上是通过调用sun.misc.Unsafe类的compareAndSwapInt()方法来实现的。

2.2 等待队列的管理

AQS中的等待队列是一个FIFO队列,可以通过同步器提供的acquire()方法向队列中添加一个节点,也可以通过release()方法将头节点出队,唤醒等待队列中的下一个节点。AQS中等待队列的实现是通过一个双向链表来实现的,每个节点都有前驱节点和后继节点。

在AQS中,队列中的每个节点都是一个ConditionObject对象,它是AQS的内部类,继承了Condition接口。ConditionObject实现了Condition接口的所有方法,它能够使线程以安全的方式等待特定的条件。

2.3 线程的阻塞与唤醒

当线程请求共享资源时,如果发现资源已被占用,线程就会被阻塞,并被加入到等待队列中。线程被阻塞后,它就会进入WAITING状态。

在AQS中,线程的阻塞与唤醒是通过LockSupport类来实现的。LockSupport是Java中用于线程阻塞与唤醒的工具类,它提供了park()和unpark()方法来控制线程的阻塞和唤醒。

3. AQS应用场景

AQS广泛应用于Java并发包中的各种同步器,如ReentrantLock、Semaphore、CountDownLatch等。它为我们提供了一种可扩展、可重用的同步机制,使我们能够方便地实现各种同步工具。

4. AQS代码实现

下面我们以ReentrantLock为例,演示如何使用AQS实现一个可重入的互斥锁。

Lock接口是Java并发包中提供的一个用于互斥同步的接口,它定义了lock()、unlock()、tryLock()等方法。在AQS中,我们可以通过继承AbstractQueuedSynchronizer类来实现Lock接口的所有方法。

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
        // 其他方法略
    }
    static final class NonfairSync extends Sync {
        // 具体实现略
    }
    static final class FairSync extends Sync {
        // 具体实现略
    }
}

在上面的代码中,我们可以看到,ReentrantLock中定义了一个Sync类,它继承了AbstractQueuedSynchronizer,Sync类中定义了lock()、unlock()等方法,这些方法实际上是通过调用AQS的acquire()、release()等方法来实现的。

为了提高性能,ReentrantLock还提供了两种Sync的实现方式:NonfairSync和FairSync。NonfairSync采用非公平锁的方式,即线程可以随时争夺锁,无需排队;而FairSync则采用公平锁的方式,即线程按照先来先服务的原则进行排队争夺锁。在ReentrantLock的构造函数中,我们可以根据需要来选择使用哪种Sync的实现方式。

最后,我们来看一下ReentrantLock的lock()方法的实现:

public void lock() {
    sync.acquire(1);
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在lock()方法中,我们调用了Sync的acquire()方法来获取锁。在acquire()方法中,我们首先调用tryAcquire()方法尝试获取锁,如果获取成功,直接返回;否则,将当前线程加入到等待队列中,并阻塞当前线程。当其他线程释放锁时,acquireQueued()方法会对等待队列中的下一个节点进行唤醒,让它重新尝试获取锁。

总结

通过上述分析,我们可以发现,AQS是Java中用于实现同步器的一个重要框架。借助AQS,我们可以方便地实现各种同步工具,如ReentrantLock、Semaphore、CountDownLatch等,为并发编程提供了强有力的支持。

小故事

从前有个村子,村子里的人们每天都要从家里到集市上买卖东西。为了让大家更有规律地出门,村子里决定修建一座门,每天早上门口一打开,大家就可以出门了。但是,每天出门的人太多了,门口经常会拥堵,导致一些人出门需要等待很长时间。于是,村子里的人又决定在门口添加一个能够控制出门的机制,这个机制能够很好地控制出门的人数,让大家更有规律地出门。

这个门口的机制就像是同步器AQS的底层工作原理。AQS其实就是一种锁的机制,它可以让代码块在并发环境中互斥地执行,避免竞争问题,并提高并发性能。

在同步器AQS的底层实现中,比如ReentrantLock,就是通过“门”的模式进行控制。首先,AQS会维护一个state状态值,它代表着当前的锁状态。当一个线程想要进入同步代码块时,它就需要先尝试去获取锁。如果此时state值为0,那么线程就可以获取到锁,并将state值设置为1,表示它已经拿到了锁。

但是,如果此时state值不为0,那么线程就不能获取到锁,它就需要进入等待状态,等待其他线程释放锁。等待的线程会被放到同步队列中,等待队列中的线程都会被阻塞,直到某个线程释放了锁,才会有一个线程从等待队列中被唤醒并进入同步代码块执行。

这个“门”的机制就是同步器AQS的底层工作原理,通过控制进入同步代码块的线程数,避免竞争问题,让代码块在并发环境中互斥地执行,提高并发性能。


相关文章
|
存储 Java
AQS(AbstractQueuedSynchronizer,队列同步器)源码解读
AQS(AbstractQueuedSynchronizer,队列同步器)源码解读
|
存储 关系型数据库 数据库
聊多版本并发控制(MVCC)
MVCC是数据库并发控制技术,用于减少读写冲突。它维护数据的多个版本,使事务能读旧数据而写新数据,无需锁定记录。当前读获取最新版本,加锁防止修改;快照读不加锁,根据读取时的读视图(readview)决定读哪个版本。InnoDB通过隐藏字段(DB_TRX_ID, DB_ROLL_PTR)和undo log存储版本,readview记录活跃事务ID。读已提交每次读取都创建新视图,可重复读则在整个事务中复用一个视图,确保一致性。MVCC通过undo log版本链和readview规则决定事务可见性,实现了非阻塞并发读。
881 5
聊多版本并发控制(MVCC)
|
消息中间件 存储 运维
Rabbitmq消息大量堆积怎么办?
该文讨论了一个系统架构问题,主要涉及RabbitMQ在处理订单消息时遇到的性能瓶颈。首先,系统使用RabbitMQ是为了解耦和提高性能,前端创建订单后通过RabbitMQ发送消息给订单履约系统消费并执行后续操作。当订单流量激增时,消息堆积导致服务器压力增加。 排查解决方案: 1. 增加消费者以提高消费速度,但发现即使增加消费者,消息堆积问题仍未解决。 2. 分析消费者逻辑,发现调用库存系统接口可能导致处理速度慢。库存系统压力大,接口响应慢,加剧问题。 3. 实施清空堆积消息的策略,新建消费者快速消费消息并存储在表中,减轻服务器压力。待库存服务恢复后,再将消息推回RabbitMQ处理。
966 1
|
2月前
|
缓存 NoSQL 关系型数据库
MySQL 与 Redis 如何保证双写一致性?
我是小假 期待与你的下一次相遇 ~
349 7
|
消息中间件 存储 Java
最经典的消息中间件:RabbitMQ
最经典的消息中间件:RabbitMQ
1194 1
|
5月前
|
Java 数据库连接 开发者
了解在什么情况下应当使用@MapperScan注解
总结而言,`@MapperScan`注解的使用能大大简化MyBatis与Spring Boot结合时的配置工作,特别是在中大型项目中,它能有效减轻开发人员的负担,确保持久化层的整洁性和可维护性。在设计系统的初期就考虑使用 `@MapperScan`注解,可以为后续的项目维护打下良好基础。
419 14
1.5w字,30图带你彻底掌握 AQS!(建议收藏)
AQS( AbstractQueuedSynchronizer )是一个用来构建锁和同步器(所谓同步,是指线程之间的通信、协作)的框架,Lock 包中的各种锁(如常见的 ReentrantLock, ReadWriteLock), concurrent它包中的各种同步器(如 CountDownLatch, Semaphore, CyclicBarrier)都是基于 AQS 来构建,所以理解 AQS 的实现原理至关重要,AQS 也是面试中区分候选人的常见考点,我们务必要掌握,本文将用循序渐进地介绍 AQS,相信大家看完一定有收获。文章目录如下
|
消息中间件 设计模式 安全
深入理解AQS队列同步器原理-从ReentrantLock的非公平独占锁实现来看AQS的原理
深入理解AQS队列同步器原理-从ReentrantLock的非公平独占锁实现来看AQS的原理
|
11月前
|
存储 Prometheus Cloud Native
分布式系统架构6:链路追踪
本文深入探讨了分布式系统中的链路追踪理论,涵盖追踪与跨度的概念、追踪系统的模块划分及数据收集的三种方式。链路追踪旨在解决复杂分布式系统中请求流转路径不清晰的问题,帮助快速定位故障和性能瓶颈。文中介绍了基于日志、服务探针和边车代理的数据收集方法,并简述了OpenTracing、OpenCensus和OpenTelemetry等链路追踪协议的发展历程及其特点。通过理解这些概念,可以更好地掌握开源链路追踪框架的使用。
1099 41
|
安全 Java
Java 并发编程之AQS
Java 并发编程之AQS
534 0