ReentrantReadWriteLock详解

news2024/11/13 6:32:40

目录

  • ReentrantReadWriteLock详解
    • 1、ReentrantReadWriteLock简介
    • 2、ReentrantReadWriteLock类继承结构和类属性
    • 3、ReentrantReadWriteLock的读写锁原理分析
    • 4、ReentrantReadWriteLock.WriteLock类的核心方法详解
      • 非公平写锁的获取
      • 非公平写锁的释放
      • 公平写锁的获取
      • 公平写锁的释放
    • 5、ReentrantReadWriteLock.ReadLock类的核心方法详解
      • 非公平读锁的获取
      • 非公平读锁的释放
      • 公平读锁的获取
      • 非公平读锁的释放
    • 6、读写锁的使用注意事项
      • 利用锁降级保证可见性和效率的做法
      • 举例:
      • 谈一下 Single Threaded Execution模式
      • 谈一下Read-Write Lock模式
    • 7、总结

ReentrantReadWriteLock详解

1、ReentrantReadWriteLock简介

ReentrantReadWriteLock 是 Java 并发包中的一个类,这个类的字面意思是可重入的读写锁。 在关于ReentrantLock这篇文章中https://deepjava.blog.csdn.net/article/details/140112505 对ReentrantLock进行了详细说明,ReentrantLock是独占锁,某一时刻只有一个线程可以获取该锁,这就导致在读多写少的场景下浪费了性能。因为多个线程对共享资源的读操作不加锁也不会出现线程安全问题。

ReentrantReadWriteLock 就是为了提升这种读多写少场景下的并发性能而设计的。 其采用读写分离的策略,允许多个线程可以同时获取读锁,也就是读锁是共享的,写锁依然是互斥的。

2、ReentrantReadWriteLock类继承结构和类属性

类继承结构:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable

abstract static class Sync extends AbstractQueuedSynchronizer

static final class NonfairSync extends Sync

static final class FairSync extends Sync

public static class ReadLock implements Lock, java.io.Serializable

public static class WriteLock implements Lock, java.io.Serializable

在这里插入图片描述
描述:
ReentrantReadWriteLock 实现了 ReadWriteLock 接口,并且是 Serializable,允许其对象进行序列化。这个类主要提供读写锁的实现,并且管理读锁和写锁的状态。

SyncReentrantReadWriteLock 的内部抽象类,继承自 AbstractQueuedSynchronizer (AQS)。Sync 负责管理读写锁的同步机制,处理锁的获取和释放。它有两个主要的具体实现类:NonfairSyncFairSync

NonfairSyncSync 的具体实现,代表非公平锁的同步策略。它允许线程在获取锁时插队,从而可能提升性能,但可能导致线程饥饿。

FairSyncSync 的具体实现,代表公平锁的同步策略。它按照线程请求的顺序来获取锁,避免线程饥饿,但可能会带来额外的性能开销。

ReadLock 实现了 Lock 接口,并且是 Serializable。它提供读锁的具体实现,允许多个线程同时持有读锁,只要没有线程持有写锁。ReadLock 提供了获取和释放读锁的方法。

类属性

// 读锁  ReadLock 是 ReentrantReadWriteLock 的静态内部类
private final ReentrantReadWriteLock.ReadLock readerLock;

// 写锁  WriteLock 是 ReentrantReadWriteLock 的静态内部类
private final ReentrantReadWriteLock.WriteLock writerLock;

// Sync  也是 ReentrantReadWriteLock 的静态内部类
final Sync sync;

构造方法:

  • ①、无参构造
public ReentrantReadWriteLock() {
       // 调用有参构造  默认非公平锁
        this(false);
    }
  • ②、有参构造
    true表示公平锁,传false表示非公平锁
public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        // 初始化读锁
        readerLock = new ReadLock(this);
        // 初始化写锁
        writerLock = new WriteLock(this);
    }

3、ReentrantReadWriteLock的读写锁原理分析

这里先说结论,方便下面对方法深入分析。(下面这段话部分截取自《Java并发编程之美》,我觉得这本书对并发编程的解释比较通俗易懂,比较推荐阅读。)

由上面ReentrantReadWriteLock类的继承结构可以看出读写锁的内部维护了一个 ReadLock 和一个 WriteLock,它们依赖 Sync 实现具体功能。而 Sync 继承自 AQS,并且也提供了公平和非公平的实现。我们知道 AQS 中只维护了一个 state 状态,而 ReentrantReadWriteLock 则需要分别维护读状态和写状态。
一个 int 类型的state字段 怎么表示写和读两种状态呢 ? ReentrantReadWriteLock 巧妙地使用 state 的高 16 位表示读状态,也就是获取到读锁的次数 ;使用低 16 位表示获取到写锁的线程的可重入次数。

看下Sync类的具体实现:

static final int SHARED_SHIFT   = 16; // 读锁计数的位移量,定义读锁计数在状态中的位置
static final int SHARED_UNIT    = (1 << SHARED_SHIFT); // 读锁计数的单位,等于 2^16
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1; // 读锁计数的最大值,等于 2^16 - 1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 用于提取写锁计数的掩码

static int sharedCount(int c) {
    // 从状态中提取读锁的计数
    return c >>> SHARED_SHIFT; // 右移 SHARED_SHIFT 位,提取高 16 位
}

static int exclusiveCount(int c) {
    // 从状态中提取写锁的计数
    return c & EXCLUSIVE_MASK; // 使用 EXCLUSIVE_MASK 掩码提取低 16 位
}

static final class HoldCounter {
    int count = 0; // 当前线程获取写锁的次数  
    final long tid = getThreadId(Thread.currentThread()); // 线程 ID,避免垃圾回收
}

static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
    @Override
    public HoldCounter initialValue() {
        // 初始化每个线程的 HoldCounter 实例
        return new HoldCounter();
    }
}


// 下面是对锁持有情况的高性管理设计
private transient ThreadLocalHoldCounter readHolds; // 每个线程的读锁持有计数器
private transient HoldCounter cachedHoldCounter; // 当前线程的写锁持有计数器缓存
private transient Thread firstReader = null; // 记录第一个获取读锁的线程
private transient int firstReaderHoldCount; // 第一个获取读锁的线程持有的读锁次数

// Sync 构造函数
Sync() {
    // 初始化 ThreadLocalHoldCounter,用于每个线程独立的读锁持有计数
    readHolds = new ThreadLocalHoldCounter();
    
    // 确保 readHolds 字段的可见性
    setState(getState());
}

// Sync类的其他方法 省略

总结:
ReentrantReadWriteLock 中,state 字段的高 16 位用于存储读锁计数,低 16 位用于存储写锁计数。这种设计巧妙地利用了位运算来在一个整数中同时存储读锁和写锁的状态。具体来说:

  • 读锁计数:通过将 state 右移16位来提取。
  • 写锁计数:通过掩码操作提取。

此外,为了支持写锁的可重入性,HoldCounter 类用于记录线程对写锁的持有次数,并通过 ThreadLocalHoldCounter 来保证每个线程有自己的 HoldCounter 实例。这样设计不仅提高了锁的性能,还简化了锁的管理。

4、ReentrantReadWriteLock.WriteLock类的核心方法详解

非公平写锁的获取

// 创建非公平的 ReentrantReadWriteLock 
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 获取写锁实例
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
// 写锁上锁
writeLock.lock();

WriteLock类的lock方法

public void lock() {
	// 实际调用的是 Sync内部类的 acquire方法
	// acquire方法 是Sync内部类 继承 AQS的 acquire方法
    sync.acquire(1);
}

AQS的 acquire方法

public final void acquire(int arg) {
    // 尝试获取锁,调用 内部类Sync的 tryAcquire 方法
    // 如果 tryAcquire 返回 false,说明无法直接获取锁
    // 需要将当前线程加入等待队列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 尝试将当前线程加入等待队列
        selfInterrupt(); // 如果线程被中断,进行自我中断处理
}

Sync内部类实现的tryAcquire方法

protected final boolean tryAcquire(int acquires) {
    
    Thread current = Thread.currentThread(); // 获取当前线程
    int c = getState(); // 获取当前锁的状态
    int w = exclusiveCount(c); // 获取当前写锁的计数

    if (c != 0) { // 如果当前锁的状态不为 0,说明有锁持有
        // 如果当前写锁的计数为 0,说明有读锁持有,或者持有锁的线程不是当前线程
        if (w == 0 || current != getExclusiveOwnerThread())
            return false; // 当前线程无法获取写锁

        // 检查写锁计数是否超过最大允许值
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded"); // 超过最大值,抛出异常

        // 如果是重入锁,更新锁的状态
        setState(c + acquires); // 增加写锁计数
        return true; // 成功获取写锁
    }

    // 检查写锁是否应该阻塞(例如是否存在其他写锁,writerShouldBlock主要用于实现公平\非公平锁)
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires)) // 尝试更新锁的状态
        return false; // 更新失败,返回 false

    // 更新当前线程为写锁的持有者
    setExclusiveOwnerThread(current); // 设置当前线程为写锁持有者
    return true; // 成功获取写锁
}

NonfairSync类的writerShouldBlock方法

final boolean writerShouldBlock() {
            return false; // writers can always barge
        }

总结
WriteLock.lock():
调用 Sync.acquire(1) 尝试获取写锁。

Sync.acquire(int arg):
尝试通过 tryAcquire(arg) 方法直接获取写锁。
如果直接获取失败,将当前线程加入等待队列,并尝试从等待队列中获取锁。

Sync.tryAcquire(int acquires):
检查锁状态:
确保当前线程可以获取写锁,如果锁被其他线程持有,或计数超出最大值,则返回 false。
尝试获取锁:
如果没有其他线程持有锁,且状态更新成功,将当前线程设置为写锁持有者,并返回 true。

非公平写锁的释放

WriteLock类的unlock方法

// 释放写锁
public void unlock() {
    // 调用同步器(AQS)的release方法来释放锁
    sync.release(1);
}

AQS的release方法

// 释放锁,尝试解除对当前线程的持有
public final boolean release(int arg) {
    // 调用tryRelease尝试释放锁并更新状态
    if (tryRelease(arg)) {
        // 获取当前的头结点
        Node h = head;
        // 如果头结点不为空且其状态不为0,唤醒头结点的后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒头结点的后继节点(如果有)
        return true;
    }
    return false;
}

Sync的tryRelease方法

protected final boolean tryRelease(int releases) {
    // 检查当前线程是否持有锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException(); // 如果没有持有锁,抛出异常

    // 计算释放后剩余的状态值
    int nextc = getState() - releases;
    // 判断释放后是否没有线程持有锁(即状态值为0)
    
    // 从状态中提取写锁的计数  这里不用考虑读锁(高16位),因为获取写锁时读锁状态值肯定为0
    boolean free = exclusiveCount(nextc) == 0;

    // 如果锁已经完全释放,将独占线程设置为null
    if (free)
        setExclusiveOwnerThread(null);
    
    // 更新状态值
    setState(nextc);
    return free; // 返回锁是否完全释放的状态
}

总结:
WriteLock.unlock():
sync.release(1) 调用会尝试释放一个写锁,如果锁成功释放,会通知等待的线程。

AQS.release(int arg):
tryRelease(arg) 方法尝试释放锁并更新锁的状态。如果锁成功释放(即 tryRelease 返回 true),并且头结点的 waitStatus 不为0(表示有线程在等待),则调用 unparkSuccessor(h) 唤醒头结点的后继节点,使其能够尝试获得锁。

Sync.tryRelease(int releases):
首先检查当前线程是否持有锁,如果没有持有,则抛出 IllegalMonitorStateException 异常。然后计算释放锁后的状态值,如果状态值为0,则表示锁完全释放,这时候将独占线程设置为 null。最后更新状态值并返回锁是否完全释放的状态。

公平写锁的获取

公平写锁的获取过程和非公平写锁类似,但通过 FairSync 类的 writerShouldBlock 方法实现公平性。

FairSync的writerShouldBlock 方法

final boolean writerShouldBlock() {
    // 检查队列中是否存在其他线程(例如,读线程或写线程)在当前线程之前
    return hasQueuedPredecessors();
}

AQS的hasQueuedPredecessors方法

public final boolean hasQueuedPredecessors() {
    // 检查当前线程前面是否有其他线程在等待
    // 正确性依赖于head在tail之前初始化,并且head.next在当前线程是队列中的第一个线程时准确
    Node t = tail; // 读取尾节点
    Node h = head; // 读取头节点
    Node s;

    // 判断队列中是否有线程在当前线程之前
    // h != t 确保队列中至少有两个节点
    // (s = h.next) == null 说明头节点的下一个节点为空,即当前线程是第一个
    // s.thread != Thread.currentThread() 确保队列中的下一个节点不是当前线程
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

总结:
公平性实现:
FairSync.writerShouldBlock() 方法调用 hasQueuedPredecessors() 来检查队列中是否有其他线程在当前线程之前,从而实现公平性。

hasQueuedPredecessors() 方法:
检查队列: 通过比较头节点和尾节点,以及检查头节点的下一个节点来确定是否存在其他线程在当前线程之前,确保公平性。

公平写锁的释放

同上面非公平写锁的释放步骤。

5、ReentrantReadWriteLock.ReadLock类的核心方法详解

非公平读锁的获取

因为读锁是共享锁,所以调用的方法都是xxxShared命名的方法。

ReadLock的lock方法

public void lock() {
    // 调用同步器(AQS)的 acquireShared 方法来尝试获取一个共享读锁
    sync.acquireShared(1);
}

AQS的releaseShared方法

public final void acquireShared(int arg) {
    // 尝试获取共享锁,通过 tryAcquireShared 方法
    // 如果 tryAcquireShared 返回负值,说明无法直接获取锁
    // 需要进入等待队列
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg); // 进入等待队列并尝试获取锁
}

Sync的tryReleaseShared方法

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread(); // 获取当前线程
    int c = getState(); // 获取当前锁的状态
    // 如果有独占锁(写锁)持有,并且持有锁的线程不是当前线程,不能获取读锁
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;

    int r = sharedCount(c); // 获取当前读锁的计数
    // 如果读锁计数小于最大允许值且当前线程可以获取锁  (readerShouldBlock 用来保证是否公平)
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 如果当前读锁计数为 0,说明当前线程是第一个获取读锁的线程
        if (r == 0) {
            firstReader = current; // 设置当前线程为第一个读线程
            firstReaderHoldCount = 1; // 设置持有读锁的计数
        } else if (firstReader == current) {
            // 如果当前线程已经是第一个读线程,增加持有读锁的计数
            firstReaderHoldCount++;
        } else {
            // 更新读锁计数的缓存
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1; // 成功获取读锁
    }
    // 如果直接获取失败,调用 fullTryAcquireShared 方法进一步处理
    return fullTryAcquireShared(current);
}

NonfairSync的readerShouldBlock方法

// 使用启发式方法来避免写线程的饥饿,决定读线程是否应该阻塞
final boolean readerShouldBlock() {
   	// 调用AQS的apparentlyFirstQueuedIsExclusive
    return apparentlyFirstQueuedIsExclusive();
}

// 检查队列中的头节点的下一个节点是否是一个非共享模式的节点(即写线程)。
// 如果是,则说明队列中有写线程等待,当前读操作应该阻塞
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null && // 确保头节点存在
        (s = h.next)  != null && // 确保头节点的下一个节点存在
        !s.isShared() && // 检查下一个节点是否不是共享模式(即是写线程)
        s.thread != null; // 确保下一个节点的线程存在
}

Sync的fullTryAcquireShared方法

final int fullTryAcquireShared(Thread current) {
    /*
     * 这个方法在功能上与 tryAcquireShared 方法有一些重复,
     * 但它整体上更简单,因为它不涉及重试和延迟读取持有计数的交互。
     */
    HoldCounter rh = null; // 用于缓存持有计数的变量

    for (;;) {
        int c = getState(); // 获取当前锁的状态
        // 检查是否有独占锁持有
        if (exclusiveCount(c) != 0) {
            // 如果独占锁被持有且持有者不是当前线程,无法获取读锁
            if (getExclusiveOwnerThread() != current)
                return -1; // 返回 -1 表示无法获取锁
            // 否则,当前线程持有独占锁;在此阻塞将导致死锁。
        } else if (readerShouldBlock()) {
            // 检查是否需要阻塞读者线程(确保不是递归获取读锁)
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0; // 当前线程是第一个读线程,持有计数应大于 0
            } else {
                // 当前线程不是第一个读线程,检查读锁持有计数
                if (rh == null) {
                    rh = cachedHoldCounter; // 获取缓存的持有计数
                    if (rh == null || rh.tid != getThreadId(current)) {
                        // 如果缓存为空或不匹配当前线程 ID,则获取持有计数
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove(); // 如果计数为 0,则移除
                    }
                }
                if (rh.count == 0)
                    return -1; // 如果读锁持有计数为 0,则返回 -1 表示无法获取锁
            }
        }
        // 检查读锁计数是否超过最大允许值
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded"); // 超过最大值,抛出异常

        // 尝试更新锁的状态
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 更新成功,检查是否需要更新读线程的信息
            if (sharedCount(c) == 0) {
                firstReader = current; // 设置当前线程为第一个读线程
                firstReaderHoldCount = 1; // 设置持有读锁的计数
            } else if (firstReader == current) {
                // 如果当前线程已经是第一个读线程,增加持有计数
                firstReaderHoldCount++;
            } else {
                // 更新读锁计数的缓存
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // 缓存用于释放读锁
            }
            return 1; // 成功获取读锁
        }
    }
}

AQS 的 doAcquireShared(int arg) 方法

private void doAcquireShared(int arg) {
    // 将当前线程封装为共享节点并加入等待队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true; // 标记是否获取锁失败
    try {
        boolean interrupted = false; // 标记是否中断
        for (;;) {
            final Node p = node.predecessor(); // 获取当前节点的前驱节点
            // 如果前驱节点是头节点,尝试获取共享锁
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 如果成功获取锁,设置头节点并传播锁
                    setHeadAndPropagate(node, r);
                    p.next = null; // 解除前驱节点的引用,帮助垃圾回收
                    if (interrupted)
                        selfInterrupt(); // 处理中断
                    failed = false;
                    return;
                }
            }
            // 如果获取锁失败,判断是否需要挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node); // 如果获取锁失败,取消加入等待队列
    }
}

总结:
ReadLock.lock() 方法:
调用 Sync.acquireShared(1) 尝试获取共享读锁。

Sync.acquireShared(int arg) 方法:
直接调用 tryAcquireShared(arg) 尝试获取读锁。
如果直接获取失败,则调用 doAcquireShared(arg) 方法将线程加入等待队列。

Sync.tryAcquireShared(int unused) 方法:
检查锁状态:
如果独占锁存在且持有者不是当前线程,返回 -1。
尝试获取锁:
如果可以直接获取锁,则更新状态,并设置读线程信息。
如果直接获取失败,则调用 fullTryAcquireShared 进一步处理。

AQS.doAcquireShared(int arg) 方法:
将线程加入等待队列:
将当前线程封装为共享节点并加入等待队列。
尝试获取锁:
如果当前线程的前驱节点是头节点,则尝试获取共享读锁。
如果获取锁失败,则挂起当前线程,等待锁的释放。
非公平读锁在获取锁时不会强制保证线程的公平性。线程可以在任何时候被允许获取读锁,前提是没有其他线程持有独占锁(写锁)。

非公平读锁的释放

ReadLock的unlock() 方法

public void unlock() {
    sync.releaseShared(1); // 尝试释放一个共享读锁
}

AQS的releaseShared(int arg) 方法

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 尝试释放共享锁
        doReleaseShared(); // 确保释放后正确地唤醒等待线程
        return true;
    }
    return false;
}

Sync的tryReleaseShared(int unused) 方法

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread(); // 获取当前线程
    // 检查当前线程是否为第一个读线程
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null; // 如果持有计数为 1,清除第一个读线程
        else
            firstReaderHoldCount--; // 否则减少持有计数
    } else {
        // 当前线程不是第一个读线程,更新持有计数
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get(); // 获取持有计数
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove(); // 如果计数为 1 或更少,移除持有计数
            if (count <= 0)
                throw unmatchedUnlockException(); // 如果计数为 0 或更少,抛出异常
        }
        --rh.count; // 减少持有计数
    }
    // 尝试更新锁的状态
    for (;;) {
        int c = getState(); // 获取当前状态
        int nextc = c - SHARED_UNIT; // 减少共享单位
        if (compareAndSetState(c, nextc)) { // 更新状态
            // 释放读锁对读者没有影响,但可能允许等待的写线程继续
            return nextc == 0; // 如果状态为 0,返回 true 表示所有读锁已释放
        }
    }
}

AQS的doReleaseShared() 方法

private void doReleaseShared() {
    for (;;) {
        Node h = head; // 获取头节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue; // 循环以重新检查情况
                unparkSuccessor(h); // 唤醒后继节点
            } else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue; // 循环以重新检查 CAS 失败的情况
        }
        if (h == head) // 如果头节点改变,继续循环
            break;
    }
}

总结:
ReadLock.unlock() 方法:
调用 Sync.releaseShared(1) 尝试释放一个共享读锁。

AQS.releaseShared(int arg) 方法:
调用 tryReleaseShared(arg) 尝试释放共享读锁。
成功释放锁后,调用 doReleaseShared() 确保正确地唤醒等待线程。

Sync.tryReleaseShared(int unused) 方法:
检查并更新读锁持有计数:
如果当前线程是第一个读线程,更新相关信息。
如果当前线程不是第一个读线程,更新缓存的持有计数。
更新锁状态:
尝试减少共享单位并更新状态。
返回 true 表示所有读锁已释放。

AQS.doReleaseShared() 方法:
确保释放后正确地唤醒等待线程。
如果需要传播信号,将状态设置为 PROPAGATE。

公平读锁的获取

整体步骤和 非公平读锁的获取差不多。 公平性的保证主要通过readerShouldBlock方法保证。

FairSync类的readerShouldBlock方法

final boolean readerShouldBlock() {
    /*
     * 确保公平性:如果当前队列中存在其他线程,且这些线程在队列中处于当前读线程之前,
     * 则新来的读线程应当阻塞,以保证公平性。
     */
    return hasQueuedPredecessors();
}

// AQS的hasQueuedPredecessors方法          
public final boolean hasQueuedPredecessors() {
    Node t = tail; // 获取队列的尾节点
    Node h = head; // 获取队列的头节点
    Node s; // 下一个节点

    /*
     * 确保头节点存在并且不是队尾节点,且头节点的下一个节点不是当前线程。
     * 如果条件成立,说明队列中有其他线程在等待,当前线程需要阻塞。
     */
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

总结
FairSync.readerShouldBlock():
通过调用 hasQueuedPredecessors() 方法来决定当前读线程是否应该阻塞。主要用于维护公平性,确保新来的读线程在获取锁之前,如果队列中有其他等待的线程,则阻塞。

hasQueuedPredecessors():
检查队列中是否存在其他线程在当前线程之前等待。如果队列中有等待的线程(特别是写线程),则返回 true,表示当前线程应该阻塞。
这些方法通过确保读线程在获取锁之前,检查是否有其他等待的线程,从而保证了读锁的公平性。

非公平读锁的释放

同非公平读锁的释放步骤。

6、读写锁的使用注意事项

利用锁降级保证可见性和效率的做法

补充知识点:
这里说的锁降级是指线程在持有写锁的前提下,获取读锁,再释放写锁的过程。 注意全程都是有锁的状态。
但是不能进行锁升级,也就是持有读锁的前提下,获取写锁,因为写锁是互斥的。

举例:

我养了几只特别厉害的狗,这几只狗会做大骨汤,等骨头准备好了,所有的狗狗就可以同时并发的吃骨头。如果骨头没准备好,狗狗想吃骨头就得等做骨头的那只狗先把骨头汤煮好才能全部开吃。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class TestA {

    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static final Lock readLock = lock.readLock();
    private static final Lock writeLock = lock.writeLock();
    private static volatile boolean bonePrepared = false; // 是否准备好骨头

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

        Thread t1 = new Thread(() -> {
            try {
                useBone();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "秀逗");


        Thread t2 = new Thread(() -> {
            try {
                useBone();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "四眼");


        Thread t3 = new Thread(() -> {
            try {
                useBone();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "大黄");

        t1.start();
        t2.start();
        t3.start();
    }

    // 真正给狗狗调用的方法
    public static void useBone() throws Exception {
        readLock.lock();
        try {
            if (!bonePrepared) {
                // 如果骨头未准备好,先释放读锁
                readLock.unlock();
                writeLock.lock();
                try {
                    if (!bonePrepared) {
                        // 准备骨头
                        prepareBone();
                        bonePrepared = true;
                    }
                    // 准备好后,重新获取读锁
                    readLock.lock();
                } finally {
                    // 释放写锁
                    writeLock.unlock();
                }
                // 锁降级完成,读锁已重新获取
            }
            // 使用骨头的过程
            eatBone();
        } finally {
            // 释放读锁
            readLock.unlock();
        }
    }


    private static void prepareBone() throws Exception {
        // 骨头准备的具体过程
        System.out.println("机器开始自动准备骨头...");
        System.out.println("切骨头...");
        System.out.println("煮骨头...");
        System.out.println("放调味料...");
        System.out.println("准备好啦...");
        try {
            Thread.sleep(2000); // 模拟准备过程
        } catch (InterruptedException e) {
            throw e;
        }
    }

    private static void eatBone() throws Exception {
        // 吃骨头的过程
        System.out.println(Thread.currentThread().getName() + "开始吃骨头...");
        try {
            Thread.sleep(500); // 模拟吃骨头的过程
        } catch (InterruptedException e) {
            throw e;
        }
    }
    
}

总结:
假设秀逗跑的最快。

线程获取读锁:
秀逗首先尝试获取读锁,因为读取操作通常是安全的,多个线程可以并发读取数据。
由于初始时骨头还未准备好,秀逗发现需要准备骨头,于是释放读锁并获取写锁。

获取写锁并准备骨头:
秀逗获取写锁后开始准备骨头(如制作骨头汤)。写锁是独占的,这确保了在准备骨头的过程中没有其他线程能够修改或读取骨头。
由于写锁是互斥的,其他线程必须等待秀逗完成骨头准备。

锁降级:
准备完骨头后,秀逗需要释放写锁以允许其他线程访问骨头。
在释放写锁之前,秀逗再一次获取读锁。这样,秀逗在告知其他狗子骨头准备好之前,自己相当于先盛了一碗骨头汤。 (还是秀逗聪明~ 自己做的自己先盛一碗没毛病吧~ )

通知其他线程:
一旦秀逗获取到读锁,就释放写锁(虽然秀逗先偷偷盛了一碗,但仍然等通知了其他狗子之后再吃,秀逗还是很讲义气的~),其他狗子得到通知也能获取读锁并开始吃骨头。
通过这种方式,秀逗保证了自己首先获取读锁,同时公平地让其他线程也能得到通知获取读锁。

读写锁一般还可以用来实现线程安全的缓存。这里就不写示例了。

谈一下 Single Threaded Execution模式

下面摘自《图解Java多线程设计模式》
有一座独木桥,非常细,每次只允许一个人经过。如果这个人还没有走到桥的另一头,则下一个人无法过桥。如果同时有两个人上桥,桥就会塌掉,掉进河里。

所谓 Single Threaded Execution 模式,意即“以一个线程执行”。就像独木桥同一时间内只允许一个人通行一样,该模式用于设置限制,以确保同一时间内只能让一个线程执行处理。

Single Threaded Execution有时候也称为临界区(critical section)或临界域(critical region )Single Threaded Execution这个名称侧重于执行处理的线程(过桥的人),而临界区或临界域的名称则侧重于执行范围(人过的桥)。

我觉得这个模式算是多线程同步的基础。也可以算是互斥锁的基础思想。

上面例子中 秀逗准备骨头汤的过程就是 Single Threaded Execution。 而整个例子又是Read-Write Lock模式。

谈一下Read-Write Lock模式

学生们正在一起看老师在黑板上写的板书。这时,老师想擦掉板书,再写新的内容。而学生们说道:“老师,我们还没看完,请先不要擦掉!”于是,老师就会等待大家都看完。

我觉得这个解释的角度很有意思,从读锁的角度解释。 一般我们理解读写锁,容易从写锁角度去理解,比如写的过程中不能读不能写。 上面的解释也很到位,因为写锁是互斥的也要等没有读锁的时候才能获取。

在 Read-Write Lock模式中,读取操作和写入操作是分开考虑的。在执行读取操作之前,线程必须获取用于读取的锁。而在执行写入操作之前,线程必须获取用于写人的锁。
由于当线程执行读取操作时,实例的状态不会发生变化,所以多个线程可以同时读取。但在读取时,不可以写入。
当线程执行写人操作时,实例的状态就会发生变化。因此,当有一个线程正在写入时,其他线程不可以读取或写入。

一般来说,执行互斥处理会降低程序性能。但如果把针对写入的互斥处理和针对读取的互斥处理分开来考虑,则可以提高程序性能。

7、总结

ReentrantReadWriteLock 只是读写锁思想的一个具体Java实现。 重要的是理解这种思想。掌握这些思想可以帮助我们在不同编程语言或框架中应用类似的锁机制。

参考资源(非常感谢下面这些资料):
《图解Java多线程设计模式》
《Java并发编程的艺术》
https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantReadWriteLock.html
https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html#reentrantreadwritelock

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1952867.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FPGA开发——独立仿真和联合仿真

一、概述 我们在进行FPGA开发的过程之中&#xff0c;大部分情况下都是在进行仿真&#xff0c;从而验证代码实现结果的正确与否&#xff0c;这里我们引入了独立仿真和联合仿真进行一个简单介绍。 联合仿真&#xff1a;一般我们在进行仿真之前需要在相应的软件中建立相应的工程…

C语言强化-3.单链表的新建、查找

与408的关联&#xff1a;1. 链表出了很多大题。 2. 任何数据结构&#xff0c;主要进行的操作就是增删改查。 头插法新建链表的实战 流程图 代码 #include <stdio.h> #include <stdlib.h>typedef int ElemType; typedef struct LNode{ElemType data;//数据域struc…

Docker 搭建Elasticsearch详细步骤

本章教程使用Docker搭建Elasticsearch环境。 一、拉取镜像 docker pull docker.elastic.co/elasticsearch/elasticsearch:8.8.2二、运行容器 docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-n

分布式锁的三种实现方式:Redis、基于数据库和Zookeeper

分布式锁的实现 操作共享资源&#xff1a;例如操作数据库中的唯一用户数据、订单系统、优惠券系统、积分系统等&#xff0c;这些系统需要修改用户数据&#xff0c;而多个系统可能同时修改同一份数据&#xff0c;这时就需要使用分布式锁来控制访问&#xff0c;防止数据不一致。…

一步步教你学会如何安装VMare虚拟机(流程参考图)

前言&#xff1a;一步步教你安装VMare虚拟机&#xff08;此版本为17.5。2版本&#xff09;。 1、安装 2、确认协议 3、选择位置存放 4、选择第二个 5、都不选。 6、都选提供便捷操作 7、点击许可证&#xff0c;将密钥输入&#xff08;可以在网络寻找自己版本的密钥&#xff…

学好C++之——函数重载、缺省参数、内联函数

函数重载、缺省参数、内联函数都是C不同于C语言的知识点&#xff0c;简单轻松&#xff0c;这里就放到一篇来讲—— 目录 1.缺省参数 1.1什么是缺省参数&#xff1f; 1.2为什么需要缺省参数&#xff1f; 1.3缺省参数的使用规则 2.函数重载 参数类型不同&#xff1a; 参数个…

错误代码0x80070035是什么情况?针对错误代码0x80070035的解决方法

错误代码 0x80070035 通常与网络连接和文件共享有关&#xff0c;表示“找不到网络路径”。这个问题可能由多种原因引起&#xff0c;包括网络设置不当、服务未启动、注册表配置错误等。今天这篇文章就和大家分享几种针对错误代码0x80070035的解决方法。 针对错误代码0x80070035问…

Linux权限维持篇

目录 SSH后门 &#xff08;1&#xff09;软链接sshd &#xff08;2&#xff09;SSH Key 生成公私钥 创建个authorized_keys文件来保存公钥 通过修改文件时间来隐藏authorized_keys &#xff08;3&#xff09;SSH Keylogger&#xff08;记录日志&#xff09; Linux的PA…

vue 给特定满足条件的表单数据添加背景颜色,组件的 row-class-name

1、:row-class-name"tableRowClassName" 可为表格每行根据后面的函数绑定class名 <!-- 列表框 --><div class"tableList"><el-table :data"teamModelListTable" style"width: 100%"selection-change"handleSele…

基于Python的哔哩哔哩国产动画排行数据分析系统

需要本项目的可以私信博主&#xff0c;提供完整的部署、讲解、文档、代码服务 随着经济社会的快速发展&#xff0c;中国影视产业迎来了蓬勃发展的契机&#xff0c;其中动漫产业发展尤为突出。中国拥有古老而又璀璨的文明&#xff0c;仅仅从中提取一部分就足以催生出大量精彩的…

数字图像处理和机器视觉中的常用特殊矩阵及MATLAB实现详解

一、前言 Matlab的名称来源于“矩阵实验室&#xff08;Matrix Laboratory&#xff09;”&#xff0c;其对矩阵的操作具有先天性的优势&#xff08;特别是相对于C语言的数组来说&#xff09;。在数字图像处理和机器视觉实践中&#xff0c;为了提高编程效率&#xff0c;MATLAB 提…

用Java手写jvm之实现查找class

写在前面 完成类加载器加载class的三阶段&#xff0c;加载&#xff0c;解析&#xff0c;初始化中的加载&#x1f600;&#x1f600;&#x1f600; 源码 。 jvm想要运行class&#xff0c;是根据类全限定名称来从特定的位置基于类加载器来查找的&#xff0c;分别如下&#xff1a;…

MySQL常见指令

MySQL中的数据类型 大致分为五种&#xff1a;数值&#xff0c;日期和时间&#xff0c;字符串&#xff0c;json&#xff0c;空间类型 每种类型也包括也一些不同的子类型&#xff0c;根据需要来选择。 如数值类型包括整数类型和浮点数类型 整数类型根据占用的存储空间的不同 又…

Javascript 沙漏图案(Hour-glass Pattern)

给定正整数 n&#xff0c;以沙漏形式打印数字模式。示例&#xff1a; 输入&#xff1a;rows_no 7 输出&#xff1a; 1 2 3 4 5 6 7 2 3 4 5 6 7 3 4 5 6 7 4 5 6 7 5 6 7 6 7 7 6 7 5 6 7 4 5 6 7 3 4 5 6 7 2 3 4 5 6 7 1 2 3 4 5 6…

指针的面试题

这里写目录标题 判断链表中是否有环描述代码检测链表中是否存在环链表中存在环想检测链表中是否存在环&#xff0c;而不需要找到环的入口 判断链表中是否有环 题目 描述 判断给定的链表中是否有环。如果有环则返回true&#xff0c;否则返回false。 数据范围&#xff1a;链表…

Java语言程序设计——篇九(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 内部类 概述内部类—内部类的分类成员内部类实战演练 局部内部类实战演练 匿名内部类实战演练 静态内部类实战演练 概述 内部类或嵌套类&#…

不支持jdk8的jenkins部署jdk8项目

1、背景 目前最新的jenkins必须基于jdk8以上&#xff0c;才能安装。jenkins最新的插件部分也不支持jdk8了。 2、全局工具配置 配置一个jdk8 配置一个jdk8以上的版本&#xff0c;如jdk17 3、部署maven项目 jdk17项目 可以直接使用maven插件&#xff0c;部署。 jdk8项目 由…

Zenario CMS 9.2 文件上传漏洞(CVE-2022-23043)

前言 CVE-2022-23043 是一个影响 Zenario CMS 9.2 的严重漏洞。该漏洞允许经过身份验证的管理员用户绕过文件上传限制。具体来说&#xff0c;管理员可以通过创建一个新的带有 ".phar" 扩展名的“文件/MIME 类型”&#xff0c;然后上传一个恶意文件。在上传过程中&am…

运维锅总详解NFS

NFS是什么&#xff1f;如何对NFS进行部署及优化&#xff1f;NFS工作流程是什么&#xff1f;NFS的性能及优缺点是什么&#xff1f;NFS发展历史又是怎样的&#xff1f;希望本文能帮您解答这些疑惑&#xff01; 一、NFS简介 NFS (Network File System) 是由 Sun Microsystems 在…

【最新】cudnn安装教程

最近换了新电脑需要重新安装cuda和cudnn&#xff0c;发现现在cudnn的安装比以前方便多了&#xff0c;直接在官网下载exe安装包一键运行即可。安装的时候注意cuda和cudnn的对应关系即可&#xff1a;【最新】cuda和cudnn和显卡驱动的对应关系-CSDN博客 访问cudnn下载链接cuDNN 9…