Java-并发-ReentrantLock

参考文章:

ReentrantLock学习(三)公平锁与非公平锁

0.什么是ReentrantLock

ReentrantLock 是一种提供了可重入、灵活锁定控制和支持公平锁与非公平锁选择的互斥锁

ReentrantLockjava.util.concurrent.locks 包中的一个类,提供了比 synchronized 关键字更灵活和强大的锁机制。

ReentrantLock 实现了 Lock 接口,它允许显式地加锁和解锁,并提供了一些高级功能,如中断锁请求、超时锁请求、公平锁和非公平锁选择等。

1.为什么要有ReentrantLock

在Java诞生之初,就有了synchronizedReentrantLock 是在 JDK 1.5 中引入的,为啥要引入这个呢?

引入 ReentrantLock 的主要原因是为了解决 synchronized 关键字的一些局限性,并提供更灵活和强大的锁机制,以应对复杂的并发编程需求。

1.1 synchronized的缺点

序号 局限性 描述
1 锁的细粒度控制 synchronized 是隐式锁定的,无法显式地控制锁的获取和释放,锁的范围是整个方法或代码块。
2 锁获取不可中断 线程在等待获取 synchronized 锁时,不能被中断,可能导致长时间等待,无法响应中断请求。
3 非公平锁 synchronized 锁是非公平的,不能保证锁的获取顺序,可能导致某些线程长时间得不到锁,造成“饥饿”现象。
4 缺乏条件变量支持 synchronized 只提供了 wait()notify()notifyAll() 方法进行线程间的协调,但这些方法的使用和控制相对简单,无法实现复杂的等待条件。

简单总结下:

  • 能控制粒度更细,意味着能更快。

  • synchronized拿不到锁会阻塞,直到它能够获得锁为止。在这种状态下,即使该线程接收到中断信号(即其他线程调用了 interrupt() 方法),它也不会响应中断,而是继续等待锁的释放。

  • synchronized非公平锁是随机选择一个请求锁的线程来获取锁,没有严格的顺序控制,可能存在“插队”。

  • synchronized缺乏条件变量,意思就是你无法指定唤醒那个特定线程。现在你20个线程在wait了,假设我想t1来操作,你就有个notify() (随机一个)和 notifyAll() ,你怎么区分20个线程谁是谁啊?

1.2 ReentrantLock的优点

序号 优势 描述
1 显式锁定和解锁 需要显式调用 lock() 获取锁,并显式调用 unlock() 释放锁,提供了对锁的细粒度控制。
2 可中断锁获取 支持 lockInterruptibly() 方法,在等待锁的过程中可以响应中断。
3 超时锁获取 支持 tryLock(long timeout, TimeUnit unit) 方法,允许在指定时间内尝试获取锁,如果超时则放弃获取。
4 公平锁和非公平锁 可以选择公平锁(按请求顺序获取锁)和非公平锁(可能会插队获取锁)。
5 条件变量支持 提供 newCondition() 方法,可以创建多个 Condition 对象,实现复杂的线程间协调。
6 查询锁状态 提供方法查询锁的状态,如 isLocked() 判断锁是否被任何线程持有,isHeldByCurrentThread() 判断锁是否被当前线程持有。

2 ReentrantLock怎么用

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        lock.lock(); // 获取锁
        try {
            // 同步区代码
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

2.1 Lock和Condition

在Java中,LockCondition接口的引入主要是为了提供更灵活和可扩展的线程同步机制。

2.1.1 Lock接口(灵活的锁控制)

传统的synchronized块自动获取和释放锁,无法提供如下灵活性:

  • 尝试获取锁:使用Lock接口的tryLock()方法,可以尝试获取锁而不必一直等待,从而避免死锁的可能。
  • 超时获取锁Lock接口的tryLock(long time, TimeUnit unit)方法允许在指定的时间内尝试获取锁,如果在该时间内未能获取锁,则返回false,这提供了超时控制。
  • 可中断的锁获取Lock接口的lockInterruptibly()方法允许在获取锁的过程中响应中断,从而更好地控制线程的中断响应。
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;

public interface Lock {

    /**
     * 获取锁。
     */
    void lock();

    /**
     * 可中断地获取锁。
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 尝试获取锁。
     */
    boolean tryLock();

    /**
     * 在指定时间内尝试获取锁。
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 释放锁。
     */
    void unlock();

    /**
     * 创建一个新的条件变量。
     */
    Condition newCondition();
}

2.1.2 Condition接口(灵活的通知)

Condition接口提供了比Object的wait/notify更加强大的等待/通知机制:

  • 多个条件变量:一个Lock可以有多个Condition实例,使得可以更精确地控制线程的等待和通知,而synchronized块只有一个隐含的条件变量(即this对象的监视器)。
  • 灵活的等待/通知Condition接口提供了await()signal()signalAll()等方法,可以精确地控制哪些线程需要等待、哪些线程需要被通知。
package java.util.concurrent.locks;

import java.util.concurrent.TimeUnit;
import java.util.Date;

public interface Condition {

    /**
     * 使当前线程等待,直到被通知或中断。
     */
    void await() throws InterruptedException;

    /**
     * 使当前线程等待,直到被通知。此方法不响应中断。
     */
    void awaitUninterruptibly();

    /**
     * 使当前线程等待指定的时间,直到被通知、中断或等待时间达到指定的纳秒数。
     */
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    /**
     * 使当前线程等待指定的时间,直到被通知、中断或等待时间达到指定的时间单位。
     */
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 使当前线程等待,直到被通知、中断或达到指定的截止日期。
     */
    boolean awaitUntil(Date deadline) throws InterruptedException;

    /**
     * 唤醒一个等待的线程。
     */
    void signal();

    /**
     * 唤醒所有等待的线程。
     */
    void signalAll();
}

2.2 AQS

2.2.1 AQS是什么

抽象的队列同步器,ReentrantLock 主要依赖于 AbstractQueuedSynchronizer(AQS)来实现其同步机制。

AQS 是一个用于构建锁和同步器的框架,它使用一个volatile int state变量来表示同步状态,并通过一个FIFO(先进先出)队列来管理线程的排队等待。

JUC并发包中常用的锁和同步器如ReentrantLockReentrantReadWriteLockCountDownLatchSemaphore等都是基于AQS实现的。

最前面我们提到了synchronized的几个缺点,synchronized是干嘛的,是用于解决并发场景下的同步问题。

AQS就是针对这个场景的顶层框架,让我们可以具备更灵活的能力来处理同步问题。

嗯,我自己搞也可以呀?但是我们得知道,我们都是菜鸡,我们考虑不到那么全面。

跟我们平时一样,我们只会cv抄代码。

AQS的存在,就是提供一个良好的模版,让我们在这个框架内做实现,降低难度。

嗯,没有AQS的话?

好,想必现在你已经学会Java的基本语法了。

现在,需要你来设计一套框架,用于解决并发场景下的各种问题。

来吧。

1.Abstract(抽象)

抽象意味着AQS是一个抽象类,不能直接实例化,而是作为一个基础类提供给其他同步器来继承和扩展。

  • 抽象方法:AQS提供了一些抽象方法,如tryAcquiretryReleasetryAcquireSharedtryReleaseShared。这些方法定义了基本的获取和释放同步状态的操作,但具体实现需要由子类来提供。
  • 模板方法:AQS采用模板方法模式,定义了获取和释放锁的通用逻辑,而具体的同步策略由子类通过实现抽象方法来确定。
  • 通用框架:AQS提供了一个通用的框架,用于构建各种类型的同步器(如独占锁、共享锁、信号量、倒计时锁存器等)。这种设计使得开发者可以通过继承AQS并实现其抽象方法,快速实现自定义的同步器。
2. Queued(队列化)

队列化指的是AQS使用一个FIFO(先进先出)的等待队列来管理竞争同步资源的线程。

  • 等待队列:AQS内部维护了一个由节点(Node)组成的FIFO队列。当一个线程尝试获取锁而失败时,它会被放入这个等待队列中,等待其他线程释放锁。
  • Node类:每个等待队列中的节点都是一个Node对象,包含了线程的信息、等待状态等。Node类是AQS内部的一个静态嵌套类,用于表示等待队列中的每一个节点。
  • 挂起与唤醒:当线程不能获取锁时,AQS会将其挂起,并在适当的时候唤醒。这些操作是通过对队列中的节点进行管理来实现的。
3. Synchronizer(同步器)

同步器表示AQS的主要功能是作为一个同步工具,用于管理对共享资源的访问。其同步机制包括:

  • 同步状态:AQS通过一个整数状态(state)来表示同步状态。这个状态可以被多个线程安全地访问和修改,AQS提供了getState、setState和compareAndSetState等方法来操作同步状态。
  • AQS支持两种同步模式
    • 独占模式(Exclusive Mode):只有一个线程可以获取锁,其他线程必须等待(如ReentrantLock)。
    • 共享模式(Shared Mode):多个线程可以共享同步状态(如CountDownLatch和Semaphore)。
  • Condition支持:AQS还提供了对条件变量的支持,可以通过newCondition方法创建Condition对象,使用条件变量来实现更复杂的同步条件(如等待特定条件满足)。

2.2.2 为什么要用AQS

AQS 的设计和存在主要是为了简化和统一各种并发控制机制的实现, 提供了一个通用的基础结构,使得实现这些同步器变得更加简单和一致。

以ReentrantLock为例,其中有几个内部类。

image-20240518110117951

    abstract static class Sync extends AbstractQueuedSynchronizer

我们调用ReentrantLock中的lock方法,实际上调用的是:

image-20240518110247395

Sync继承了AQS,所以我们调用ReentrantLock中的一些方法,就是在使用AQS的方法。

现在我们自己写一个MyAqs类继承AQS类。

image-20240518111118636

从左侧可以看出,当继承后,有以下内容:

1.获取和释放锁
  • acquire(int arg):独占模式下尝试获取锁,如果获取失败则进入等待队列。
  • acquireInterruptibly(int arg):独占模式下尝试获取锁,如果获取失败则进入等待队列,支持中断。
  • release(int arg):独占模式下释放锁。
2.共享模式下的获取和释放
  • acquireShared(int arg):共享模式下尝试获取锁,如果获取失败则进入等待队列。
  • acquireSharedInterruptibly(int arg):共享模式下尝试获取锁,如果获取失败则进入等待队列,支持中断。
  • releaseShared(int arg):共享模式下释放锁。
3.同步状态操作
  • compareAndSetState(int expect, int update):原子地比较并设置同步状态。
  • getState():获取当前的同步状态。
  • setState(int newState):设置同步状态。
4.队列和线程管理
  • hasQueuedThreads():检查是否有线程在等待队列中。
  • hasQueuedPredecessors():检查当前线程是否有前驱节点,是否需要等待。
  • getQueueLength():获取等待队列中线程的数量。
  • getQueuedThreads():获取等待队列中的所有线程。
5.条件队列支持
  • getWaitQueueLength(ConditionObject condition):获取等待特定条件的线程数。
  • getWaitingThreads(ConditionObject condition):获取等待特定条件的所有线程。
  • hasWaiters(ConditionObject condition):检查是否有线程在等待特定条件。
  • owns(ConditionObject condition):检查条件是否属于当前同步器。
6.中断支持
  • isHeldExclusively():判断当前线程是否持有独占锁。
  • isQueued(Thread thread):检查线程是否在等待队列中。
  • tryAcquire(int arg):尝试独占模式获取锁,子类需要实现。
  • tryRelease(int arg):尝试独占模式释放锁,子类需要实现。
  • tryAcquireShared(int arg):尝试共享模式获取锁,子类需要实现。
  • tryReleaseShared(int arg):尝试共享模式释放锁,子类需要实现。
7.实用方法
  • toString():返回同步器的字符串表示。
  • tryAcquireNanos(int arg, long nanosTimeout):尝试在给定的时间内获取锁,支持中断。
  • tryAcquireSharedNanos(int arg, long nanosTimeout):尝试在给定的时间内共享模式获取锁,支持中断。
8.内部状态和队列节点
  • exclusiveQueuedThreads:独占模式下等待的线程集合。
  • firstQueuedThread:第一个等待线程。
  • queuedThreads:所有等待线程的集合。
  • queueLength:等待队列的长度。
  • sharedQueuedThreads:共享模式下等待的线程集合。
  • heldExclusively:标志当前线程是否独占持有锁。
  • state:同步状态。

很复杂对不对,没关系,每个类都是很复杂的。

但是现在我们起码知道,AQS是一套方法论,当我们继承它,针对同步的这个场景,我们已经能出来大部分的问题了,套好各种方法就行。

翻看下AQS的方法,可以发现,虽然AQS是一个abstract类,但是其中的方法都不是abstract的。

说明AQS针对大部分场景做了默认实现,当你有需要时,你可以选择性的自己重写。

2.3 独占锁和共享锁

AbstractQueuedSynchronizer(AQS)既支持独占锁也支持共享锁。

2.3.1 独占锁

独占锁是指同一时刻只能有一个线程获取锁,其它线程必须等待。例如:

  • ReentrantLock: ReentrantLock 是一个典型的独占锁实现,基于 AQS 实现。

    只有一个线程可以持有锁,其他线程尝试获取锁时会被阻塞,直到持有锁的线程释放锁。

AQS 提供的方法,如 acquire(int arg)release(int arg),用于管理独占模式下的锁获取和释放。

独占锁的实现通常需要覆盖 tryAcquire(int arg)tryRelease(int arg) 方法。

2.3.2 共享锁

共享锁允许同一时刻有多个线程获取锁。例如:

  • ReentrantReadWriteLock: ReentrantReadWriteLock 提供了一个典型的共享锁实现,其中读锁是共享的,而写锁是独占的。

    多个线程可以同时持有读锁,但如果有任何一个线程持有写锁,其他线程(无论读写)都必须等待。

AQS 也提供了用于共享模式的方法,如 acquireShared(int arg)releaseShared(int arg),用于管理共享模式下的锁获取和释放。

共享锁的实现通常需要覆盖 tryAcquireShared(int arg)tryReleaseShared(int arg) 方法。

2.3.3 总结

独占模式

  • acquire(int arg): 独占获取锁。如果当前线程不能立即获取锁,则进入等待队列。
  • release(int arg): 释放锁,如果有其他线程在等待,则唤醒。
  • tryAcquire(int arg): 尝试以独占模式获取锁,具体实现由子类提供。
  • tryRelease(int arg): 尝试以独占模式释放锁,具体实现由子类提供。

共享模式

  • acquireShared(int arg): 共享获取锁。如果当前线程不能立即获取锁,则进入等待队列。
  • releaseShared(int arg): 共享释放锁,如果有其他线程在等待,则唤醒。
  • tryAcquireShared(int arg): 尝试以共享模式获取锁,具体实现由子类提供。
  • tryReleaseShared(int arg): 尝试以共享模式释放锁,具体实现由子类提供。

2.4 ReentrantLock与AQS

嗯,AQS实在是很难,只能笨办法硬磨了。

2.4.1 ReentrantLock中的state

state变量通常是由一个整数来表示,它的值反映了锁的当前状态:

  • 锁的持有情况
    • state为0时,表示锁没有被任何线程持有。
    • state为1时,表示锁被一个线程持有。
    • state为大于1的值时,表示锁被一个线程重入持有。即,持有锁的线程再次获取了同一个锁。
  • 锁的重入计数
    • ReentrantLock是可重入锁,这意味着同一个线程可以多次获取同一个锁,而不会被阻塞。每次同一个线程获取锁时,state变量会增加1。
    • 当线程释放锁时,state变量会减少1,直到state为0时,锁才真正被释放,其他线程才有机会获取该锁。

2.4.2 ReentrantLock中3个内部类

经过前面可以知道,ReentrantLock同时支持公平锁和非公平锁,默认是与synchronized一样的非公平锁。

    public ReentrantLock() {
        sync = new NonfairSync();
    }

然后,我们也知道其中有内部类Sync(基于AQS),而公平锁和非公平锁是基于Sync类来操作的。

	abstract static class Sync extends AbstractQueuedSynchronizer {
    static final class FairSync extends Sync {     
	static final class NonfairSync extends Sync {

2.4.3 非公平锁NonfairSync

非公平锁中,主要重写了来自Sync的lock方法和来自AQS的tryAcquire方法。

image-20240518130558918

  • lock方法

    lock来源于内部类Sync,容易知道,对于公平锁和非公平锁,我们的lock方式应该不一样。

    注意,AQS是没有lock的,它只是提供了线程状态、线程队列这些内部结构给你和一些基本的操作方法给你,你的lock动作就是要操作这些内部变量。

    这是Lock接口的用意,AQS不提供lock方法哦。

  • tryAcquire方法

    尝试获取一个独占锁,嗯,我们知道AQS是一个顶层框架,你这个类实现的到底是独占锁还是共享锁,AQS是无法预知的,我们可以看到AQS的默认实现是直接抛出异常。这样,如果你为了实现共享锁,而不重写尝试获取独占锁的方法,可以直接抛出异常。

    嗯,所以这里,咱们的ReentrantLock是独占锁,重写了这段逻辑,咱们可不是抛异常,咱们是需要有具体的操作的。

    image-20240518131247865

1.lock
        final void lock() {
            // 获取锁 cas更新state 前面不是提到了嘛 0代表没有现成持有 1代表已经被一个线程持有了
            if (compareAndSetState(0, 1))
                // 标记类中的当前线程 互斥嘛 一次只能一个线程来用
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 如果未能获取锁
                acquire(1);
        }

if成功了很好理解,主要是下面这个acquire,来自于AQS。

独占模式下尝试获取锁,如果获取失败则进入等待队列。

image-20240518133522941

2.tryAcquire

拿不到走AQS的acquire方法,上来就是个tryAcquire()

image-20240518141232794

刚不是提到了这个AQS默认实现是抛异常,我们得自己重写tryAcquire。

image-20240518133700780

这个方法又调回去,回到了Sync类中。

等等!先捋一下什么情况下,咱们会走到else。

  • 锁已被其他线程持有

    • 如果当前锁已经被其他线程持有,则 state 不等于 0。

    • 当线程尝试获取锁时,compareAndSetState(0, 1) 会失败,因为 state 的当前值不是 0。

  • 当前线程重入

    ReentrantLock 支持同一线程多次获取同一个锁(重入锁)。

    • 如果当前线程已经持有锁并尝试再次获取锁,state 的值已经不为 0。在这种情况下,compareAndSetState(0, 1) 也会失败,因为 state 的当前值不是 0。

其实无所谓啦,不管重入的线程是不是当前线程,反正不是0就代表锁已经被抢了。无非就是咱们等看看是不是我自己,什么我是我自己?搞哲学呢。

Sync类中是这样实现的:

image-20240518133722455

		// 记得这里acquires 刚才传了1
        final boolean nonfairTryAcquire(int acquires) {
            // 当前线程
            final Thread current = Thread.currentThread();
            
            // 获取状态(锁标记)
            int c = getState();
            
            // 再次检查锁状态
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            
            // 重入判断 持有线程是不是当前线程
            else if (current == getExclusiveOwnerThread()) {
                // 加就完事
                int nextc = c + acquires;
                
                // 溢出判断 int是2的32次方减一 啥玩意补码的原理要复习下 再大就溢出 变成负数了
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                
                // 更新State
                setState(nextc);
                return true;
            }
            
            // 不是持有线程 拿不到 等着吧你
            return false;
        }

这里还是比较好理解的哈,非公平锁获取的时候,无非就是更新state的值,不过有一个点。

不是刚刚才判断为0了吗?又来?

这里我猜测再次判断的用意是,竞争条件,就是说,万一这么小会你已经把琐释放了呢?是不是就能立马获取到锁啦。

在多线程环境下,锁的状态可能在 lock 方法的初次尝试和 nonfairTryAcquire 方法被调用之间发生变化。另一个线程可能在这段时间内释放了锁,使得锁的状态从非零变为零。因此,重新检查锁的状态有助于捕捉这种变化,从而避免不必要的等待。

嗯,现在才捋清楚了tryAcquire,尝试拿到互斥锁,结果就是拿到了or没拿到。

3.acquire

现在回到AQS的acquire方法

  • acquire方法,首先尝试直接获取锁,如果失败,则将当前线程添加到等待队列中,并进行自旋等待。

  • 在线程成功获取锁之前,它会不断重试。

  • 如果线程在等待过程中被中断,获取锁成功后会自我中断,以正确处理中断状态。

image-20240518142006119

注意啊,这里有个,就是拿没拿到,拿到了取反得到false,就不用往后走了。

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

尝试获取锁

if (!tryAcquire(arg))
  • 调用 tryAcquire(arg) 尝试获取锁
    • 如果 tryAcquire(arg) 返回 true,表示锁获取成功,acquire 方法结束。
    • 如果 tryAcquire(arg) 返回 false,表示锁获取失败,继续执行。

好哇,接下来的方法全被封装了。

4.Node

上面的代码中出现了一个Node.EXCLUSIVE,我们先来了解下这个Node

Node是啥玩意?EXCLUSIVE又是啥?先把Node的代码抄过来。

Node 类是用于 AbstractQueuedSynchronizer (AQS) 的一个内部类,主要用于实现线程的同步队列

static final class Node {
        
        static final Node SHARED = new Node();
        
        static final Node EXCLUSIVE = null;
    
        static final int CANCELLED =  1;
        
        static final int SIGNAL    = -1;
        
        static final int CONDITION = -2;
        
        static final int PROPAGATE = -3;
     
        volatile int waitStatus;
        
        volatile Node prev;
        
        volatile Node next;
        
        volatile Thread thread;
        
        Node nextWaiter;
        
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
        
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    
        }

        Node(Thread thread, Node mode) {     
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { 
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

静态字段

static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;

SHAREDEXCLUSIVE 是静态常量,分别表示共享和独占模式的节点。

  • SHARED:共享,是一个新的节点实例。
  • EXCLUSIVE:独占, null表示独占模式。

等待状态常量

  • CANCELLED:表示节点被取消。
  • SIGNAL:表示后继节点需要被唤醒。
  • CONDITION:表示节点在条件队列中。
  • PROPAGATE:表示下一次共享模式获取应该传播。

实例变量

volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
  • waitStatus:表示节点的等待状态,使用上面的常量来标识。
  • prevnext:前驱节点和后继节点,组成双向链表结构。
  • thread:当前节点对应的线程。
  • nextWaiter:用于条件队列或共享模式。
5.addWaiter

创建一个新的等待节点

从上面我们可以看到,入参是这样的,放入一个独占节点。

addWaiter(Node.EXCLUSIVE)
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        
        // 尾结点不为空 放入
        if (pred != null) {
            node.prev = pred;
            // CAS设置
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        
        // 入队
        enq(node);
        return node;
    }

这里的tail节点在哪?是AQS自身的成员变量。

image-20240518153735895

enq方法,使用无限循环和原子操作,将一个新节点安全地添加到AQS队列的尾部,初始化头节点(若为空),并更新尾节点。

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

嗯,还是之前的思路,addWaiter方法的主要目的是将一个新的等待节点添加到AQS队列中。

为了提高效率,它首先尝试通过快速路径直接将节点添加到队列的尾部,如果失败,则回退到使用enq方法以忙等待的方式来添加节点。

6.acquireQueued

acquireQueued方法尝试获取锁,失败时将线程加入等待队列,并处理中断情况。

你看,for循环里不就有之前提到的tryAcquire()方法吗?

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

7.总结

非公平锁它在尝试获取锁时不考虑线程进入的顺序,直接尝试获取锁,如果锁是空闲的就立即获得锁,否则进入等待队列。

  • 尝试获取锁
    • NonfairSync 直接调用 tryAcquire 方法,尝试获取锁而不考虑队列中的顺序。
    • 如果锁是空闲的,当前线程将立即获得锁。
  • 失败进入等待队列
    • 如果获取锁失败,当前线程会被封装成一个节点并添加到同步队列中。
    • addWaiter(Node.EXCLUSIVE) 方法会创建一个新的节点并尝试将其添加到队列的尾部。
  • 排队等待
    • 线程进入队列后,通过调用 acquireQueued 方法进行自旋,尝试获取锁。
    • 在自旋过程中,如果前驱节点是头节点且锁可用,线程将尝试再次获取锁。
  • 挂起线程
    • 如果前驱节点不是头节点,或者锁仍然不可用,线程会挂起自己,等待唤醒。
    • 挂起和检查中断状态通过 parkAndCheckInterrupt 方法实现。
  • 获取锁和设置头节点
    • 一旦线程成功获取锁,当前节点会被设置为头节点,前驱节点的 next 引用被清除以帮助垃圾回收。
  • 处理中断
    • 如果线程在排队过程中被中断,会在恢复时记录中断状态,最终处理中断情况。

img

2.4.4 公平锁FairSync

公平锁中主要重写了Sync类中的lock方法和AQS中的tryAcquire方法。

image-20240518160519759

1.lock

公平锁的lock方法,直接使用了AQS的acquire方法。

  • acquire方法,首先尝试直接获取锁,如果失败,则将当前线程添加到等待队列中,并进行自旋等待。

  • 在线程成功获取锁之前,它会不断重试。

  • 如果线程在等待过程中被中断,获取锁成功后会自我中断,以正确处理中断状态。

2.tryAcquire
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

和非公平锁的差别,就在于hasQueuedPredecessors(),其目的是如果队列中有其他线程在等待,当前线程就不会尝试获取锁,以保证先到先得的原则。

3.总结

公平锁通过在尝试获取锁时检查等待队列中是否有比当前线程更早的线程,以确保先到先得的原则,从而避免“插队”现象,保证线程按照到达的顺序依次获取锁。

  • 线程请求锁

    • 线程调用锁的lock方法请求获取锁。
    • 公平锁首先检查当前锁是否可用(即状态是否为0)。
    • 如果锁是空闲的,且等待队列中没有其他线程,则当前线程成功获取锁,并将锁状态设置为已占用(状态设为1)。
  • 线程进入等待队列

    • 如果锁已被其他线程持有,或者等待队列中有其他线程,当前线程将进入等待队列。
    • 当前线程被包装成一个节点,添加到等待队列的尾部。
  • 线程排队等待

    • 当前线程进入等待状态,挂起自己,等待被唤醒。
    • 等待队列确保线程按请求顺序排队,新的线程总是添加到队列的尾部。
  • 锁释放和唤醒

    • 持有锁的线程完成任务后,调用unlock方法释放锁。
    • 锁状态被重置为0,表示锁可用。
    • 公平锁将等待队列中的第一个线程唤醒。
  • 线程被唤醒

    • 被唤醒的线程重新尝试获取锁。
    • 如果等待队列中仍有其他线程,当前线程需要继续排队等待,直到轮到它获取锁。
  • 获取锁的尝试

    • 被唤醒的线程再次尝试获取锁,如果锁是空闲的,且没有其他线程在前面排队,则成功获取锁。
    • 该线程将锁状态设置为已占用,继续执行其任务。
  • 响应中断

    • 如果等待中的线程被中断,它将被从等待队列中移除。
    • 线程可以处理中断,防止长时间阻塞或死锁。

img

3 实例

前面提到了ReentrantLock相比synchronized的几个优点,其他的现在给出实例。

序号 优势 描述
1 显式锁定和解锁 需要显式调用 lock() 获取锁,并显式调用 unlock() 释放锁,提供了对锁的细粒度控制。
2 可中断锁获取 支持 lockInterruptibly() 方法,在等待锁的过程中可以响应中断。
3 超时锁获取 支持 tryLock(long timeout, TimeUnit unit) 方法,允许在指定时间内尝试获取锁,如果超时则放弃获取。
4 公平锁和非公平锁 可以选择公平锁(按请求顺序获取锁)和非公平锁(可能会插队获取锁)。
5 条件变量支持 提供 newCondition() 方法,可以创建多个 Condition 对象,实现复杂的线程间协调。
6 查询锁状态 提供方法查询锁的状态,如 isLocked() 判断锁是否被任何线程持有,isHeldByCurrentThread() 判断锁是否被当前线程持有。

3.1 显示锁定/解锁

public void doSomething() {
    lock.lock(); // 获取锁
    try {
        // 同步区代码
    } finally {
        lock.unlock(); // 释放锁
    }
}

3.2 可中断式获取锁

使用 lockInterruptibly 可以使线程在尝试获取锁时响应中断,从而提高程序的灵活性和响应性。

适用于那些需要在等待锁的过程中能够中断的场景,例如处理用户取消操作或系统优雅关闭的情况。

首先来一个synchronized的,它在获取锁的过程中,无法响应中断。

package cn.yang37.thread;

import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @class: LockDemo3
 * @author: yang37z@qq.com
 * @date: 2024/5/18 23:10
 * @version: 1.0
 */
@Slf4j
public class LockDemo3 {

    public static void main(String[] args) throws InterruptedException {

        LockDemo3 lockDemo3 = new LockDemo3();

        Thread t1 = new Thread(
                () -> {
                    lockDemo3.m1();
                }, "t1");

        Thread t2 = new Thread(
                () -> {
                    lockDemo3.m2();
                }, "t2");


        t1.start();
        t2.start();

        lockDemo3.m3(t1, t2);

    }

    private synchronized void m1() {
        try {
            for (; ; ) {
                log.info("1111");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private synchronized void m2() {
        try {
            for (; ; ) {
                log.info("1111");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void m3(Thread t1, Thread t2) throws InterruptedException {
        for (; ; ) {
            t2.interrupt();
            log.info("t1: {},t2: {}", t1.getState(), t2.getState());
            Thread.sleep(1000);
        }
    }
}
  • m1,sync方法,每1s输出下1111
  • m2,sync方法,输出2222
  • m3,普通方法,每次都会对t2发出中断信号,并且每s输出下线程1和2的状态。

在main方法中,启动了2个线程t1和t2,由于都是由lockDemo1对象调用,所以锁住的是同一个对象。

t1直接拿到锁,由于t1是个死循环在输出1111,会导致t2拿不到锁,结果就是t2线程一直阻塞。

此时,哪怕我们对t2发出中断信号,t2也无法响应。

image-20240518231639013

修改为ReentrantLock。

package cn.yang37.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @description:
 * @class: LockDemo3
 * @author: yang37z@qq.com
 * @date: 2024/5/18 23:10
 * @version: 1.0
 */
@Slf4j
public class LockDemo4 {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock lock = new ReentrantLock();
        LockDemo4 lockDemo4 = new LockDemo4();


        Thread t1 = new Thread(
                () -> {
                    lockDemo4.m1(lock);
                }, "t1");

        Thread t2 = new Thread(
                () -> {
                    try {
                        lockDemo4.m2(lock);
                    } catch (InterruptedException e) {
                        log.error("interrupt exception was received while acquiring the lock.");
                    }
                }, "t2");


        t1.start();
        t2.start();

        lockDemo4.m3(t1, t2, lock);

    }

    private void m1(ReentrantLock lock) {
        lock.lock();
        try {
            for (; ; ) {
                log.info("1111");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }

    }

    private void m2(ReentrantLock lock) throws InterruptedException {
        // 可中断式获取锁
        lock.lockInterruptibly();
        try {
            for (; ; ) {
                log.info("1111");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    private void m3(Thread t1, Thread t2, ReentrantLock lock) throws InterruptedException {
        for (; ; ) {
            t2.interrupt();
            log.info("t1: {},t2: {}", t1.getState(), t2.getState());
            Thread.sleep(1000);
        }
    }
}

这里我们改为了lock.lockInterruptibly()来获取锁,当获取不到锁时,如果尝试发出中断信号,是能接收到中断并抛出异常的。

image-20240518232444094

3.3 超时式获取锁

先演示下synchronized的问题,如果拿不到锁,线程会阻塞在那里。

package cn.yang37.thread;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LockDemo1 {

    public static void main(String[] args) throws InterruptedException {
        LockDemo1 lockDemo1 = new LockDemo1();

        Thread t1 = new Thread(
                () -> {
                    lockDemo1.m1();
                }, "t1");

        Thread t2 = new Thread(
                () -> {
                    lockDemo1.m2();
                }, "t2");


        t1.start();
        t2.start();

        lockDemo1.m3(t1, t2);
    }

    private synchronized void m1() {
        try {
            for (; ; ) {
                log.info("1111");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private synchronized void m2() {
        log.info("2222");
    }

    private void m3(Thread t1, Thread t2) throws InterruptedException {
        for (; ; ) {
            log.info("t1: {},t2: {}", t1.getState(), t2.getState());
            Thread.sleep(1000);
        }
    }
}
  • m1,sync方法,每1s输出下1111
  • m2,sync方法,输出2222
  • m3,普通方法,每s输出下线程1和2的状态。

在main方法中,启动了2个线程t1和t2,由于都是由lockDemo1对象调用,所以锁住的是同一个对象。

t1直接拿到锁,由于t1是个死循环在输出1111,会导致t2拿不到锁,结果就是t2线程一直阻塞。

t1的状态是TIMED_WAITING还是RUNNABLE不重要,因为毕竟用的是sleep方法,会抱着锁睡眠,进入超时等待状态。

关键点在于,t2它啊,一直在阻塞,完全动不了。

image-20240518224702544

下面改成ReentrantLock。

package cn.yang37.thread;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @description:
 * @class: MyReentrantLock2
 * @author: yang37z@qq.com
 * @date: 2024/5/18 22:49
 * @version: 1.0
 */
@Slf4j
public class LockDemo2 {


    public static void main(String[] args) throws InterruptedException {

        LockDemo2 lockDemo2 = new LockDemo2();

        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(
                () -> {
                    lockDemo2.m1(lock);
                }, "t1");

        Thread t2 = new Thread(
                () -> {
                    lockDemo2.m2(lock);
                }, "t2");

        t1.start();
        t2.start();

        lockDemo2.m3(t1, t2);
    }

    private void m1(ReentrantLock lock) {
        lock.lock();
        try {
            for (; ; ) {
                log.info("1111");
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            log.error("error", e);
        } finally {
            lock.unlock();
        }
    }

    @SneakyThrows
    private void m2(ReentrantLock lock) {
        // 标记首次for循环
        boolean flag = true;
        
        boolean tryLock = lock.tryLock(5, TimeUnit.SECONDS);

        if (tryLock) {
            log.info("2222");
            lock.unlock();
        } else {
            for (; ; ) {
                if (flag) {
                    log.warn("t2 failed to acquire the lock!");
                    flag = false;
                } else {
                    log.info("2222");
                    Thread.sleep(1000);
                }
            }
        }

    }

    private void m3(Thread t1, Thread t2) throws InterruptedException {
        for (; ; ) {
            log.info("t1: {},t2: {}", t1.getState(), t2.getState());
            Thread.sleep(1000);
        }
    }

}

基本逻辑与之前synchronized的一样,只不过m2中,我们使用了tryLock方法,拿不到锁的时候,可以执行其他操作。

比如,拿不到锁后,我们可以走到else分支中,可以看到在此之后,有2222的输出。

image-20240518230719668

3.4 公平锁和非公平锁

前面提到,synchronized和ReentrantLock都是默认非公平式的,这样随机式的抢占,插队并不奇怪。

这样可能导致某些运气不好的线程一直拿不到锁,进而导致饿死现象。

  • 非公平锁
package cn.yang37.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @description:
 * @class: LockDemo5
 * @author: yang37z@qq.com
 * @date: 2024/5/18 23:28
 * @version: 1.0
 */
@Slf4j
public class LockDemo5 {
    private static final ReentrantLock lock = new ReentrantLock(); // 默认是非公平锁

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Task(), "t" + i).start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    log.info("{}", Thread.currentThread().getName());
                    // 模拟任务执行时间
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                } finally {
                    lock.unlock();
                }

                // 让出CPU时间,让其他线程有机会获取锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.error("{} was interrupted during sleep.", Thread.currentThread().getName());
                }
            }
        }
    }
}

image-20240518235324032

  • 公平锁
package cn.yang37.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @description:
 * @class: LockDemo5
 * @author: yang37z@qq.com
 * @date: 2024/5/18 23:28
 * @version: 1.0
 */
@Slf4j
public class LockDemo5 {
    private static final ReentrantLock lock = new ReentrantLock(true); // 默认是非公平锁

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Task(), "t" + i).start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    log.info("{}", Thread.currentThread().getName());
                    // 模拟任务执行时间
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                } finally {
                    lock.unlock();
                }

                // 让出CPU时间,让其他线程有机会获取锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.error("{} was interrupted during sleep.", Thread.currentThread().getName());
                }
            }
        }
    }
}

image-20240518235427912

3.5 支持条件变量

前面提到,synchronized唤醒线程的时候,无法指定到某个特殊的。

在ReentrantLock中,我们可以通过不同的Condition来实现。

package cn.yang37.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @description:
 * @class: LockDemo6
 * @author: yang37z@qq.com
 * @date: 2024/5/18 23:55
 * @version: 1.0
 */
@Slf4j
public class LockDemo6 {

    public static void main(String[] args) throws InterruptedException {
        LockDemo6 demo6 = new LockDemo6();

        ReentrantLock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();

        Thread t1 = new Thread(() -> demo6.m1(lock, condition1), "t1");
        Thread t2 = new Thread(() -> demo6.m1(lock, condition2), "t2");

        t1.start();
        t2.start();

        demo6.m3(t1, t2);
    }

    private void m1(ReentrantLock lock, Condition condition) {
        lock.lock();
        try {
            Thread.sleep(1000);
            log.info(Thread.currentThread().getName());

            if ("t2".equals(Thread.currentThread().getName())) {
                condition.await();
            }
        } catch (Exception e) {
            log.error("error", e);
        } finally {
            lock.unlock();
        }
    }

    private void m3(Thread t1, Thread t2) throws InterruptedException {
        for (; ; ) {
            log.info("t1: {},t2: {}", t1.getState(), t2.getState());
            Thread.sleep(1000);
        }
    }
}

这段代码中,我们通过condition2使得指定的t2线程进入等待状态。

image-20240519000913409

posted @ 2024-05-16 22:10  羊37  阅读(51)  评论(0编辑  收藏  举报