理解 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时,需要根据具体的应用场景和需求进行权衡。


    目录
    相关文章
    数据结构 | 堆【图解】
    数据结构 | 堆【图解】
    |
    Java BI 开发工具
    静态代码自动扫描p3c的使用
    静态代码自动扫描p3c的使用
    1484 0
    |
    前端开发 Java UED
    已解决错误代码: MethodArgumentTypeMismatchException(方法参数类型不匹配异常)
    已解决错误代码: MethodArgumentTypeMismatchException(方法参数类型不匹配异常)
    |
    Arthas 前端开发 Java
    类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类
    类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类
    类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类
    |
    Java 应用服务中间件 Linux
    JVM调优总结(一)之参数配置说明与实例
    JVM调优总结(一)之参数配置说明与实例
    1477 0
    |
    设计模式 算法 Java
    面试官:JDK1.8 HashMap扩容rehash算法是如何优化的?
    本文跟大家聊一聊一个常见的面试题,那就是JDK1.8 HashMap扩容rehash算法是如何优化的?
    |
    存储 物联网 区块链
    未来已来:新技术革命下的生活与工作
    在技术不断进步的今天,我们正站在一个新时代的门槛上。新兴技术如区块链、物联网、虚拟现实等正在改变我们的生活和工作方式。本文将探讨这些技术的发展趋势和应用场景,以及它们如何影响我们的生活和工作。让我们一起探索这个充满无限可能的未来世界吧!
    191 34
    |
    存储 资源调度 算法
    操作系统的心脏:内核深入解析
    本文将带你走进操作系统的核心—内核,通过浅显易懂的语言解释什么是内核、它如何工作以及为什么它对整个系统至关重要。我们将从内核的定义和功能出发,逐步深入到它的结构和设计哲学,最后探讨内核在现代计算环境中面临的挑战和未来发展方向。无论你是计算机新手还是有一定基础的学习者,这篇文章都会为你揭开操作系统内核的神秘面纱。
    533 3
    |
    存储 监控 算法
    (六)JVM成神路之GC基础篇:对象存活判定算法、GC算法、STW、GC种类详解
    经过前面五个章节的分析后,对于JVM的大部分子系统都已阐述完毕,在本文中则开始对JVM的GC子系统进行全面阐述,GC机制也是JVM的重中之重,调优、监控、面试都逃不开的JVM话题。
    1264 9
    |
    机器学习/深度学习
    HashMap中tableSizeFor()方法详解
    HashMap中tableSizeFor()方法详解
    HashMap中tableSizeFor()方法详解