理解 AQS 和 ReentrantLock

简介: 在多线程编程中,同步机制是确保线程安全的关键。AQS(AbstractQueuedSynchronizer)和ReentrantLock是Java中两种常见的同步机制,它们各自具有不同的特性和适用场景。了解和掌握这两种机制对于编写高效、安全的并发程序至关重要。这篇文章将带你取了解和掌握这两种机制!另外值得一提的是:公平锁的实现与非公平锁是很像的,只不过在获取锁时不会直接尝试使用CAS来获取锁。只有当队列没节点并且state为0时才会去获取锁,不然都会把当前线程放到队列中。

 其他系列文章导航

Java基础合集

数据结构与算法合集

设计模式合集

多线程合集

分布式合集

ES合集


文章目录

其他系列文章导航

文章目录

前言

一、公平锁和非公平锁

1.1 含义

1.2 如何自我实现

1.2.1 公平锁实现:

1.2.2 非公平锁实现:

1.2.3 公平和非公平的区别:

二、AQS

2.1 AQS 的含义

三、ReentrantLock

3.1 ReentrantLock 加锁和解锁的过程

四、总结


前言

在多线程编程中,同步机制是确保线程安全的关键。AQS(AbstractQueuedSynchronizer)和ReentrantLock是Java中两种常见的同步机制,它们各自具有不同的特性和适用场景。

了解和掌握这两种机制对于编写高效、安全的并发程序至关重要。

这篇文章将带你取了解和掌握这两种机制!


一、公平锁和非公平锁

1.1 含义

    • 公平锁:在竞争环境下,先到临界区的线程比后到的线程一定更快地获取得到锁。
    • 非公平锁:先到临界区的线程未必比后到的线程更快地获取得到锁。

    1.2 如何自我实现

    1.2.1 公平锁实现:

    可以把竞争的线程放在一个先进先出的队列上。只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁就好了。

    公平锁的实现通常涉及到线程同步和队列的概念。在Java中,java.util.concurrent.locks.ReentrantLock是一个常用的公平锁实现。公平锁保证了线程按照请求锁的顺序获取锁,即先来先服务(First In First Out,FIFO)。

    下面是一个简单的公平锁实现的例子:

    import java.util.concurrent.locks.ReentrantLock;  
    import java.util.concurrent.locks.Condition;  
    import java.util.Queue;  
    import java.util.LinkedList;  
    public class FairLockExample {  
        private final ReentrantLock lock = new ReentrantLock(true); // 创建一个公平锁  
        private final Condition condition = lock.newCondition(); // 创建一个条件变量  
        private final Queue<Thread> queue = new LinkedList<>(); // 创建一个等待线程队列  
        public void lock() {  
            Thread currentThread = Thread.currentThread();  
            queue.add(currentThread); // 将当前线程放入队列  
            lock.lock(); // 尝试获取锁  
            // 将当前线程从队列中移除,表示已经获取到锁  
            queue.remove(currentThread);  
        }  
        public void unlock() {  
            lock.unlock(); // 释放锁  
            // 将队列中的下一个线程唤醒并通知它可以尝试获取锁了  
            if (!queue.isEmpty()) {  
                Thread nextThread = queue.poll();  
                condition.signal(nextThread);  
            }  
        }  
    }

    image.gif

    在上面的例子中,我们创建了一个公平锁ReentrantLock,并使用一个队列来保存等待获取锁的线程。

    当一个线程尝试获取锁时,它首先将自己放入队列中,然后尝试获取锁。如果获取成功,它将从队列中移除自己,表示已经获取到锁。

    如果获取失败(即锁已经被其他线程持有),则该线程将继续等待,直到它被唤醒并重新尝试获取锁。

    当一个线程释放锁时,它会检查队列中是否还有等待的线程,如果有,它将唤醒下一个等待的线程并通知它可以尝试获取锁了。这样就实现了公平锁的机制。

    1.2.2 非公平锁实现:

    后到的线程可能比前到临界区的线程获取得到锁。那实现也很简单,线程先尝试能不能获取得到锁,如果获取得到锁了就执行同步代码了。如果获取不到锁,那就再把这个线程放到队列呗 。

    非公平锁的实现与公平锁的实现类似,主要的区别在于线程获取锁的顺序不是按照请求锁的顺序,而是由锁的实现机制决定。在Java中,java.util.concurrent.locks.ReentrantLock是一个常用的非公平锁实现。

    下面是一个简单的非公平锁实现的例子:

    import java.util.concurrent.locks.ReentrantLock;  
    public class NonFairLockExample {  
        private final ReentrantLock lock = new ReentrantLock(); // 创建一个非公平锁  
        public void lock() {  
            lock.lock(); // 尝试获取锁  
        }  
        public void unlock() {  
            lock.unlock(); // 释放锁  
        }  
    }

    image.gif

    在上面的例子中,我们创建了一个非公平锁ReentrantLock

    由于是非公平锁,线程获取锁的顺序是不确定的,可能先请求锁的线程需要等待很长时间才能获取到锁,而其他后请求锁的线程可能先获取到锁。

    因此,非公平锁可能会导致线程饥饿问题,即某些线程长时间无法获取到锁。

    1.2.3 公平和非公平的区别:

    线程执行同步代码块时,是否会去尝试获取锁。如果会尝试获取锁,那就是非公平的如果不会尝试获取锁,直接进队列,再等待唤醒,那就是公平的。


    二、AQS

    2.1 AQS 的含义

    给我们实现锁的一个框架内部实现的关键就是维护了一个先进先出的队列以及state状态变量。

    先进先出队列存储的载体叫做Node节点,该节点标识着当前的状态值、是独占还是共享模式以及它的前驱和后继节点等等信息。

    简单理解就是: AQS定义了模板,具体实现由各个子类完成。

    总体的流程可以总结为: 会把需要等待的线程以Node的形式放到这个先进先出的队列上,state变量则表示为当前锁的状态。

    实现:像ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore这些常用的实现类都是基于AQS实现的。

    AQS支持两种模式: 独占 (锁只会被个线程独占)和共享 (多个线程可同时执行)。


    三、ReentrantLock

    3.1 ReentrantLock 加锁和解锁的过程

    image.gif编辑

    加锁:当线程CAS获取锁失败,将当前线程入队列,把前驱节点状态设置为SIGNAL状态,并将自己挂起。

    解锁: 把state置0,唤醒头结点下个合法的节点,被唤醒的节点线程自然就会去获取锁。

    疑问:为什么要设置前驱节点为SIGNAL状态?

    其实归终结底就是为了判断节点的状态,去做些处理。

    Node 中节点的状态有4种,分别是: CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)和0

    在ReentrantLock解锁的时候,会判断节点的状态是否小于0,小于等于0才说明需要被唤醒。


    四、总结

    另外值得一提的是: 公平锁的实现与非公平锁是很像的,只不过在获取锁时不会直接尝试使用CAS来获取锁。只有当队列没节点并且state为0时才会去获取锁,不然都会把当前线程放到队列中。

    AQS和ReentrantLock为Java并发编程提供了强大的支持。

    AQS作为同步器的基石,通过提供一个简单的框架和机制,使得各种同步器(如ReentrantLock)的实现变得相对简单和一致。

    而ReentrantLock作为AQS的具体实现之一,提供了更多高级的功能和更好的控制,使得开发者能够更加灵活地处理并发问题。

    在选择使用AQS和ReentrantLock时,需要根据具体的应用场景和需求进行权衡。


    目录
    相关文章
    |
    3月前
    |
    弹性计算 负载均衡 安全
    阿里云轻量应用服务器与ECS区别
    阿里云轻量应用服务器与ECS云服务器对比:轻量版适合新手和小型应用,套餐化设计,含大流量,易上手但性能和灵活性有限;ECS为专业版,配置灵活,性能强,适合企业级应用,但操作复杂,生态丰富,按需计费。两者定位不同,适用于不同场景。
    淘宝API接口( item_detail - 淘宝商品详情查询)
    淘宝商品详情查询 API(item_detail)用于获取淘宝商品的详细信息。请求参数包括商品唯一 ID(num_iid)和是否获取促销价(is_promotion)。响应参数包含商品标题、价格、库存、图片链接、品牌等详细信息。
    |
    8月前
    |
    消息中间件 安全
    为什么会选择使用RabbitMQ ? 有什么好处 ?
    选择使用RabbitMQ是因为RabbitMQ的功能比较丰富 , 支持各种消息收发模式(简单队列模式, 工作队列模式 , 路由模式 , 直接模式 , 主题模式等) , 支持延迟队列 , 惰性队列而且天然支持集群, 保证服务的高可用, 同时性能非常不错 , 社区也比较活跃, 文档资料非常丰富 使用MQ有很多好处: ● 吞吐量提升:无需等待订阅者处理完成,响应更快速 ● 故障隔离:服务没有直接调用,不存在级联失败问题 ● 调用间没有阻塞,不会造成无效的资源占用 ● 耦合度极低,每个服务都可以灵活插拔,可替换 ● 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去
    |
    9月前
    |
    XML 安全 Java
    Spring AOP—深入动态代理 万字详解(通俗易懂)
    Spring 第四节 AOP——动态代理 万字详解!
    430 24
    |
    11月前
    |
    存储 安全 Java
    最爱问的高频ConcurrentHashMap原理,你会了吗?
    ConcurrentHashMap 是 Java 中的线程安全散列表实现,允许多个线程同时访问和修改数据。它在 JDK 1.7 中通过分段锁机制将 HashMap 分为多个段,每个段使用独立的锁来保证线程安全;而在 JDK 1.8 中则采用 CAS 和 synchronized 结合的方式,提高了并发性能。与 HashMap 相比,ConcurrentHashMap 是线程安全的,支持更高的并发性能,且不支持 null 键和值。CAS(Compare-and-Swap)是一种无锁原子操作,用于确保多线程环境下的数据一致性,避免竞态条件。
    510 5
    |
    存储 Java
    JAVA并发编程AQS原理剖析
    很多小朋友面试时候,面试官考察并发编程部分,都会被问:说一下AQS原理。面对并发编程基础和面试经验,专栏采用通俗简洁无废话无八股文方式,已陆续梳理分享了《一文看懂全部锁机制》、《JUC包之CAS原理》、《volatile核心原理》、《synchronized全能王的原理》,希望可以帮到大家巩固相关核心技术原理。今天我们聊聊AQS....
    |
    前端开发 Java UED
    已解决错误代码: MethodArgumentTypeMismatchException(方法参数类型不匹配异常)
    已解决错误代码: MethodArgumentTypeMismatchException(方法参数类型不匹配异常)
    |
    XML 缓存 搜索推荐
    RSS 解析:全球内容分发的利器及使用技巧
    RSS(Really Simple Syndication)是一种 XML 格式,用于网站内容的聚合和分发,让用户能快速浏览和跟踪更新。RSS 文档结构包括 `&lt;channel&gt;` 和 `&lt;item&gt;` 元素,允许内容创作者分享标题、链接和描述。通过 RSS,用户可以定制新闻源,过滤不相关信息,提高效率。RSS 支持不同版本,如 RSS 0.91 和 RSS 2.0,其中 RSS 2.0 语法简单且广泛使用。RSS 提高网站流量,适用于新闻、博客、日历等频繁更新的站点。RSS 的历史始于 1997 年,至今仍无官方标准,但已成为内容共享的重要工具。
    998 0
    |
    存储 JSON API
    淘宝订单接口对接实战(续):高级功能与实战案例
    在上一篇文章中,我们详细介绍了如何对接淘宝订单接口的基础知识,包括API申请、环境准备以及基础的API调用。本文将在此基础上,进一步探讨淘宝订单接口的高级功能,并通过实战案例,演示如何在实际业务场景中应用这些功能,全文约5000字。
    BXA
    |
    缓存 Kubernetes 负载均衡
    如何优化Kubernetes的性能和资源利用率优化
    根据业务实际需求可以添加或删除节点。如果我们的业务中有一段时间流量比较大可以考虑增加节点来增加集群的承载能力,等过了这段时间之后就可以减少节点了以节省成本
    BXA
    13308 2