理解 AQS 和 ReentrantLock

其他系列文章导航

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);  
        }  
    }  
}

在上面的例子中,我们创建了一个公平锁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(); // 释放锁  
    }  
}

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

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

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

1.2.3 公平和非公平的区别:

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


二、AQS

2.1 AQS 的含义

给我们实现锁的一个框架内部实现的关键就是维护了一个先进先出的队列以及state状态变量。
先进先出队列存储的载体叫做Node节点,该节点标识着当前的状态值、是独占还是共享模式以及它的前驱和后继节点等等信息。

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

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

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

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


三、ReentrantLock

3.1 ReentrantLock 加锁和解锁的过程

加锁:当线程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时,需要根据具体的应用场景和需求进行权衡。