java并发编程 12:JUC之ReentrantReadWriteLock使用与原理

目录

  • 概述
  • 使用
    • 原理
    • 源码
    • 流程
  • StampedLock

概述

ReentrantReadWriteLock是可重入的读写锁。

其内部除了和一样有个同步器Sync,还有一个读锁和一个写锁:

/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
  1. 读锁(Read Lock):
    • 多个线程可以同时持有读锁,因此它支持并发读取操作。
    • 如果没有线程持有写锁,则读锁可以被获取。
    • 如果有线程持有写锁,其他线程请求读锁时,它们将被阻塞,直到写锁被释放。
  2. 写锁(Write Lock):
    • 只能被一个线程持有。
    • 写锁用于独占式访问共享资源,确保在写操作期间没有其他线程可以持有读锁或写锁。

ReentrantReadWriteLock的特点和使用场景如下:

  1. 读多写少的情况:在读操作比写操作频繁且读操作不会修改数据的场景下,使用读写锁可以允许多个线程同时读取数据,从而提高并发性能。
  2. 保护共享资源:读写锁适用于保护共享资源,当有多个读操作和较少的写操作时,读写锁可以提供更高的并发性,并发读取不会互斥,只有写操作会互斥。
  3. 可重入性:ReentrantReadWriteLock是可重入的,即同一个线程可以重复获取读锁或写锁,而不会造成死锁。这种机制允许线程在持有写锁时再次获取读锁,但反过来是不允许的(即持有读锁时请求写锁会导致线程阻塞)。
  4. 公平性:ReentrantReadWriteLock提供了公平和非公平两种模式,默认为非公平模式。在非公平模式下,锁的获取是基于竞争的,允许新的获取请求插队,可能导致某些线程长期等待。而在公平模式下,锁的获取按照先来先得的顺序进行,保证所有线程都能公平地获取锁,但可能会降低整体吞吐量。

使用ReentrantReadWriteLock时需要注意以下几点:

  1. 写锁的获取和释放要保证原子性,避免死锁和竞态条件的发生。
  2. 读锁不支持条件变量。
  3. 持有读锁时请求写锁会导致线程阻塞。因为写锁是独占锁,当一个线程持有写锁时,其他线程无法获取读锁或写锁。
  4. 尽量减少持有写锁的时间,以允许更多的读操作并发执行。
  5. 读写锁适合于处理读操作远远多于写操作的场景。如果写操作很频繁,可能会导致读操作的吞吐量下降。

使用

使用时可先创建ReentrantReadWriteLock读写锁对象,然后根据这个对象获取相应的读锁和写锁,读锁和写锁可以分开使用,但是注意都要最后去解锁。

示例:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockTest {
    public static void main(String[] args) throws InterruptedException {
        DataContainer dataContainer = new DataContainer();
        // 读线程
        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        Thread.sleep(100);

        // 写线程
        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }
}


@Slf4j
class DataContainer {
    private Object data;
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    /**
     * 读锁
     */
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    /**
     * 写锁
     */
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
    public Object read() {
        log.info("获取读锁...");
        r.lock();
        try {
            log.info("读取");
            Thread.sleep(1000);
            return data;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            log.info("释放读锁...");
            r.unlock();
        }
    }

    public void write() {
        log.info("获取写锁...");
        w.lock();
        try {
            log.info("写入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.info("释放写锁...");
            w.unlock();
        }
    }
}

运行结果如下:

2023-07-13 22:04:13,610 - 0    INFO  [t1] up.cys.chapter12.DataContainer:43  - 获取读锁...
2023-07-13 22:04:13,619 - 9    INFO  [t1] up.cys.chapter12.DataContainer:46  - 读取
2023-07-13 22:04:13,714 - 104  INFO  [t2] up.cys.chapter12.DataContainer:59  - 获取写锁...
2023-07-13 22:04:14,624 - 1014 INFO  [t1] up.cys.chapter12.DataContainer:53  - 释放读锁...
2023-07-13 22:04:14,628 - 1018 INFO  [t2] up.cys.chapter12.DataContainer:62  - 写入
2023-07-13 22:04:15,633 - 2023 INFO  [t2] up.cys.chapter12.DataContainer:67  - 释放写锁...

根据运行结果看到。当获取读锁后,必须等读锁释放之后,才能获取写锁进行写操作。

原理

读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个。

写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位 。status的值可以表示多种情况,具体如下:

  1. 未锁定状态:status为0,表示锁未被任何线程获取。
  2. 获取写锁状态:status为负数,表示有一个线程持有写锁,值的绝对值表示持有写锁的线程数。
  3. 获取读锁状态:status为正数,表示有一个或多个线程持有读锁,值表示持有读锁的线程数。
  4. 获取读锁和写锁状态:status为负数,但值的绝对值大于1,表示有一个线程持有写锁,且还有其他线程持有读锁。

下面是一些可能的status值和对应的含义:

  • status = 0:锁未被任何线程获取。
  • status = 1:有一个线程持有读锁。
  • status = 2:有两个线程持有读锁。
  • status = -1:有一个线程持有写锁。
  • status = -2:有一个线程持有写锁,且还有一个线程持有读锁。

其构造方法如下:

public ReentrantReadWriteLock() {
  	// 无参构造,默认是非公平锁
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

有参构造方法里,有一个同步器,根据参数来决定是公平还是非公平的,FairSyncNonfairSync是内部自己维护的Sync同步器,继承自AQS。

除此以外,构造方法里还有读锁readerLockwriterLock

源码

  1. 写锁上锁

下面来看下写锁的上锁代码,与前面一节讲到的非常类似,也是先调用同步器的方法:

public void lock() {
    sync.acquire(1);
}

// 然后是AQS的acquire代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  // tryAcquire尝试获取锁,如果失败
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 尝试创建一个Node对象,加到等待队列中,park等待
        selfInterrupt();  // 获取失败并且加入队列成功,就调用自己的Interrupt方法
}

然后是ReentrantReadWriteLock实现的tryAcquire方法:

@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
  	// 获取当前线程
    Thread current = Thread.currentThread();
  	// 获取当前的status状态值c,包含高位和低位,即读锁加写锁的值
    int c = getState();
  	// 获取写锁(也叫独占锁)的状态值
    int w = exclusiveCount(c);
  	// 如果c!=0,即可能是其他线程加了读锁或写锁
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
      	// 如果写锁等于0,说明已经被加了读锁(注意前面说过写锁是独占式的),或者锁的所有者不是当前线程,则返回false
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
      	// 如果加了写锁之后超过最大范围,则抛错
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire 发生可重入
      	// 修改写锁的状态,返回true
        setState(c + acquires);
        return true;
    }
  	// c = 0,即没有其他线程加锁
    if (writerShouldBlock() ||  // writerShouldBlock判断是否应该阻塞,如果是非公平则返回false
        !compareAndSetState(c, c + acquires))  // 如果不是阻塞的,则再尝试修改状态值,再不成功,返回false
        return false;
  	// 如果线程不应该阻塞,并且加锁成功了,则把锁的owner给当前线程
    setExclusiveOwnerThread(current);
    return true;
}
  1. 写锁解锁

解锁代码如下:

public void unlock() {
    sync.release(1);
}

调用同步器的release方法:

public final boolean release(int arg) {
  	// 如果尝试解锁成功
    if (tryRelease(arg)) {
        Node h = head;
      	// 如果头节点不为null,并且头节点的状态不为0,则进行唤醒流程
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

主要调用了tryRelease方法:

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
  	// 首先状态值减去值releases
    int nextc = getState() - releases;
  	// 然后看下写锁部分是否等于0
    boolean free = exclusiveCount(nextc) == 0;
  	// 如果写锁状态为0
    if (free)
      	// 把锁持有者设为null
        setExclusiveOwnerThread(null);
  	// 更新状态值
    setState(nextc);
  	// 返回free,表示写锁解开了
    return free;
}
  1. 读锁上锁

接下来看下读锁的上锁代码:

public void lock() {
    sync.acquireShared(1);
}

然后找到acquireShared代码,如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

主要有两个方法组成:tryAcquireShareddoAcquireShared

其中tryAcquireShared返回值:-1表示失败,大于等于0表示成功,且是需要唤醒的后继节点数量。找到对应的实现代码如下:

@ReservedStackAccess
protected final int tryAcquireShared(int unused) {
  	// 拿到当前线程
    Thread current = Thread.currentThread();
  	// 获取当前状态值
    int c = getState();
    if (exclusiveCount(c) != 0 &&  // 如果写锁不为0
        getExclusiveOwnerThread() != current)  // 并且写锁的持有者不是当前线程
        return -1;  // 则返回-1,因为写锁是独占式的,有了写锁,不允许任何线程再加读锁
  	// 获取读锁状态(共享锁)
    int r = sharedCount(c);
    if (!readerShouldBlock() &&  // 如果当前线程不需要被阻塞
        r < MAX_COUNT &&   // 并且读锁的数量没有超过最大值,
        compareAndSetState(c, c + SHARED_UNIT)) {  // 则可以获取共享锁,尝试修改状态值(总的状态值)成功
        if (r == 0) {  // 如果上面获取的读锁状态为0,说明前面没有其他线程获取读锁,当前线程是第一个获取读锁的
          	// firstReader和firstReaderHoldCount是用于记录第一个获取读锁的线程及其读锁的持有计数的变量,这是为了避免在没有其他读锁持有者的情况下,重复获取和释放读锁带来的开销
          	// 将当前线程的引用存储在firstReader变量中,并且firstReaderHoldCount的初始值会被设置为1。
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {  // 如果当前线程已经是第一个获取锁的线程,则是发生了锁重入
            firstReaderHoldCount++;  // 把firstReaderHoldCount的值加1
        } else {
          	// rh用于缓存当前线程持有的读锁计数
            HoldCounter rh = cachedHoldCounter;
          	// 如果为空或者cachedHoldCounter对应的线程与当前线程不一致
            if (rh == null ||
                rh.tid != LockSupport.getThreadId(current))
              	// 就会通过readHolds.get()方法获取当前线程的HoldCounter对象,并将其赋值给cachedHoldCounter
                cachedHoldCounter = rh = readHolds.get();
          	// 如果rh计数等于0,则设置readHolds
            else if (rh.count == 0)
                readHolds.set(rh);
          	// 
            rh.count++;
          	// cachedHoldCounter是用于缓存当前线程持有的读锁计数的变量,用于提高性能。
          	// 它的设计是为了避免频繁地创建和销毁HoldCounter对象,并通过缓存的方式快速访问和更新读锁计数。
        }
      	// 最后返回1,表示加锁成功
        return 1;
    }
  	// 如果当前线程需要阻塞,或者尝试获取锁失败了,再调用fullTryAcquireShared方法
  	// 此方法与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
    return fullTryAcquireShared(current);
}

然后是doAcquireShared方法,代码如下:

private void doAcquireShared(int arg) {
  	// / 将当前线程作为共享模式的等待节点加入队列,注意类型不一样,变成了SHARED类型
    final Node node = addWaiter(Node.SHARED);
  	// 标识是否获取共享锁失败
    boolean interrupted = false;
    try {
      	// for循环一直尝试获取锁
        for (;;) {
          	// 获取前驱节点p
            final Node p = node.predecessor();
          	// 如果p是头节点,说明当前线程是第二个真正的节点,可以去获取锁
            if (p == head) {
              	// 再次尝试获取锁,因为这时锁可能已经被释放了,所以再次尝试
                int r = tryAcquireShared(arg);
              	// 返回r>=0,说明获取共享锁成功
                if (r >= 0) {
                    setHeadAndPropagate(node, r);  // 设置当前节点为头节点,并可能传播共享锁
                    p.next = null; // help GC
                    return;
                }
            }
          	// 如果p不是头节点,判断是否要park线程,如果需要,修改interrupted的值
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    } finally {
        if (interrupted)
            selfInterrupt();
    }
}

下面看一下当获取共享锁成功,调用的setHeadAndPropagate方法:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
  	// 把当前节点设为头节点
    setHead(node);
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
      	// 获取当前节点的下个节点
        Node s = node.next;
      	// 如果下个节点为null,或者下个节点是共享(是获取读锁的线程)的状态
        if (s == null || s.isShared())
            doReleaseShared();  // 传播共享锁,会把后面的读锁线程都唤醒,这也是读写锁在读读方面可以并发的原因
    }
}

上面代码看到,如果下个节点是共享的状态,还会执行doReleaseShared方法:

private void doReleaseShared() {
  	// 无限循环,直到完成共享锁的传播操作或者头节点发生变化
    for (;;) {
        Node h = head;
      	// 判断头节点不为空且不是尾节点。这是为了确保存在至少一个共享模式的等待节点
        if (h != null && h != tail) {
          	// 获取头节点的等待状态
            int ws = h.waitStatus;
          	// 如果等待状态是Node.SIGNAL,表示需要唤醒后继节点
            if (ws == Node.SIGNAL) {
              	// 将头节点的等待状态从Node.SIGNAL设置为0,即标记为无等待状态。
               // 如果设置成功,表示当前线程负责唤醒后继节点;如果设置失败,继续循环
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
              	// 唤醒后继节点
                unparkSuccessor(h);
            }
          	// 如果等待状态为0,表示需要进行传播
            else if (ws == 0 &&
                     // 将头节点的等待状态从0设置为Node.PROPAGATE,即标记为传播状态。
                     // 如果设置成功,表示当前线程负责进行共享锁的传播;如果设置失败,继续循环。
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
      	// 如果头节点没有改变,表示当前线程负责完成共享锁的传播操作
        if (h == head)                   // loop if head changed
            break;
    }
}
  1. 读锁解锁

下面看下读锁的解锁代码:

public void unlock() {
    sync.releaseShared(1);
}

接着releaseShared:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

然后是tryReleaseShared:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null ||
            rh.tid != LockSupport.getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
      	// 这里会死主要代码,先把状态减去高位的值
        int nextc = c - SHARED_UNIT;
      	// 然后用cas设置状态
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
          	// 最后看计数书否为0.不是0就会返回fasle,注意即使返回fasle,当前线程的锁也已经释放
            return nextc == 0;
    }
}

当tryReleaseShared返回真,即所有的锁都释放了,就会执行doReleaseShared方法。

流程

假设现在有两个线程t1和t2,分别来上写锁和读锁

  1. 写锁上锁流程

1) t1 线程执行lock加锁,执行tryAcquire方法,此时没有其他线程,则会成功上锁,把state的写锁状态改为1,把t1设为owner线程。注意写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位。

java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第1张图片

2)t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。因为有t1占据写锁即独占锁,所以t2不能再加读锁了,tryAcquireShared 返回 -1 表示获取锁失败。

java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第2张图片

3)这时t2会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态。
java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第3张图片

4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁。

5)如果没有成功,在 doAcquireShared 内 for (; 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (; 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park。

java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第4张图片

6)这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第5张图片

接着上面开始解锁流程

7)首先t1进行解锁unlock,这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子,state状态变为0,线程owner设为null

java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第6张图片

8)接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行,这回再来一次 for (; 执行 tryAcquireShared 成功则让读锁计数加一。
java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第7张图片

这时 t3 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点。

java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第8张图片

9)接着在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒第二个节点,这时 t3 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行。
java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第9张图片

下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点。

上面可以看到,一旦唤醒线程,就会把共享状态的线程全部唤醒,直到遇到独占锁线程,这就是为什么读读是可以并发的原因。

10)接着t2进行解锁unlock,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第10张图片

11)t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第11张图片

12)之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (; 这次自己是第二个节点,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
java并发编程 12:JUC之ReentrantReadWriteLock使用与原理_第12张图片

StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

加解读锁:

long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁:

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
 // 锁升级
}

示例:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.StampedLock;

public class StampedTest {
    public static void main(String[] args) throws InterruptedException {
        DataContainerStamped dataContainer = new DataContainerStamped(1);
        new Thread(() -> {
            dataContainer.read(1000);
        }, "t1").start();
        Thread.sleep(500);
        new Thread(() -> {
            dataContainer.read(0);
        }, "t2").start();
    }
}



@Slf4j
class DataContainerStamped {

    private int data;private final StampedLock lock = new StampedLock();

    public DataContainerStamped(int data) {
        this.data = data;
    }

    public int read(int readTime) {
        long stamp = lock.tryOptimisticRead();
        log.info("optimistic read locking...{}", stamp);
        try {
            Thread.sleep(readTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (lock.validate(stamp)) {
            log.info("read finish...{}, data:{}", stamp, data);
            return data;
        }
        // 锁升级 - 读锁
        log.info("updating to read lock... {}", stamp);
        try {
            stamp = lock.readLock();
            log.info("read lock {}", stamp);
            try {
                Thread.sleep(readTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("read finish...{}, data:{}", stamp, data);
            return data;
        } finally {
            log.info("read unlock {}", stamp);
            lock.unlockRead(stamp);
        }
    }

    public void write(int newData) {
        long stamp = lock.writeLock();
        log.info("write lock {}", stamp);
        try {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.data = newData;
        } finally {
            log.info("write unlock {}", stamp);
            lock.unlockWrite(stamp);
        }
    }
}

执行结果如下:

2023-07-23 17:48:30,537 - 0    INFO  [t1] up.cys.chapter12.DataContainerStamped:38  - optimistic read locking...256
2023-07-23 17:48:31,037 - 500  INFO  [t2] up.cys.chapter12.DataContainerStamped:38  - optimistic read locking...256
2023-07-23 17:48:31,038 - 501  INFO  [t2] up.cys.chapter12.DataContainerStamped:45  - read finish...256, data:1
2023-07-23 17:48:31,549 - 1012 INFO  [t1] up.cys.chapter12.DataContainerStamped:45  - read finish...256, data:1

可以看到,两个线程都是打印的乐观读,都没有上锁。

当我们改下,第二个线程改成读操作,如下:

2023-07-23 17:52:31,980 - 0    INFO  [t1] up.cys.chapter12.DataContainerStamped:38  - optimistic read locking...256
2023-07-23 17:52:32,483 - 503  INFO  [t2] up.cys.chapter12.DataContainerStamped:68  - write lock 384
2023-07-23 17:52:32,993 - 1013 INFO  [t1] up.cys.chapter12.DataContainerStamped:49  - updating to read lock... 256
2023-07-23 17:52:34,490 - 2510 INFO  [t2] up.cys.chapter12.DataContainerStamped:77  - write unlock 384
2023-07-23 17:52:34,501 - 2521 INFO  [t1] up.cys.chapter12.DataContainerStamped:52  - read lock 513
2023-07-23 17:52:35,507 - 3527 INFO  [t1] up.cys.chapter12.DataContainerStamped:58  - read finish...513, data:0
2023-07-23 17:52:35,507 - 3527 INFO  [t1] up.cys.chapter12.DataContainerStamped:61  - read unlock 513

可以看到乐观锁获取后,发现有写锁,就升级成为了读锁。

注意StampedLock不能取代ReentrantReadWriteLock

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入

你可能感兴趣的:(java并发编程JUC,java,并发编程)