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


    目录
    相关文章
    |
    6天前
    ReentrantLock
    ReentrantLock
    6 1
    |
    10月前
    |
    Java C++
    什么是AQS?
    AQS(AbstractQueuedSynchronizer)是Java中的一个同步器框架
    379 1
    |
    11月前
    |
    计算机视觉
    AQS
    AQS
    53 0
    |
    11月前
    |
    Java
    16.ReentrantLock全解读
    大家好,我是王有志。今天和大家一起聊聊ReentrantLock,它是我们最常见的基于AQS实现的互斥锁。
    76 0
    |
    存储 安全
    AQS
    一、为什么需要AQS?以及AQS的作用和重要性? AQS(AbstractQueuedSynchronizer)的重要性 AQS被用在ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor的Worker中都有运用(JDK1.8)。AQS是这些类的底层原理,JUC包里很多重要的工具类背后都离不开AQS框架。
    102 0
    |
    设计模式 安全 Java
    ReentrantLock介绍
    ReentrantLock介绍
    128 0
    |
    安全 Java
    浅谈AQS
    说到Java的并发编程包,就一定少不了一个东西,它就是AQS,可能有些同学是第一次遇到这个名词,没关系,并发包里的ReentrantLock你总用过吧?那么你有没有想过,为什么简简单单地调用lock()、unlock()方法就能够解决线程的安全问题呢?
    98 0
    |
    存储 设计模式 Java
    深入理解ReentrantLock
    同步锁synchronized和重入锁ReentrantLock都是用于并发程序设计必不可少的手段,在JDK 5.0早期版本中,同步锁性能远远低于重入锁,但是在6.0版本之后,jdk对同步锁做了大量的优化,使得同步锁跟重入锁性能差距并不大,并且jdk团队表示,同步锁还有进一步升级优化的空间
    深入理解ReentrantLock
    |
    算法 Java Linux
    AQS这样学就很简单了
    哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。
    103 0
    AQS这样学就很简单了