【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!

载自:https://my.oschina.net/meandme/blog/1839265

好长一段时间前,某些场景需要JUC的读写锁,但在某个时刻内读写线程都报超时预警(长时间无响应),看起来像是锁竞争过程中出现死锁(我猜)。经过排查项目并没有能造成死锁的可疑之处,因为业务代码并不复杂(仅仅是一个计算过程),经几番折腾,把注意力转移到JDK源码,正文详细说下ReentrantReadWriteLock的隐藏坑点。



过程大致如下:

  • 若干个读写线程抢占读写锁
  • 读线程手脚快,优先抢占到读锁(其中少数线程任务较重,执行时间较长)
  • 写线程随即尝试获取写锁,未成功,进入双列表进行等待
  • 随后读线程也进来了,要去拿读锁

问题:优先得到锁的读线程执行时间长达73秒,该时段写线程等待是理所当然的,那读线程也应该能够得到读锁才对,因为是共享锁,是吧?但预警结果并不是如此,超时任务线程中大部分为读。究竟是什么让读线程无法抢占到读锁,而导致响应超时呢?

把场景简化为如下的测试代码:读——写——读 线程依次尝试获取ReadWriteLock,用空转替换执行时间过长。

执行结果:控制台仅打印出Thread[读线程 -- 1,5,main],既是说读线程 -- 2并没有抢占到读锁,跟上诉的表现似乎一样。

public class ReadWriteLockTest {
  public static void main(String[] args) throws InterruptedException {
    TestLock testLock = new TestLock();
    Thread read1 = new Thread(new ReadThread(testLock), "读线程 -- 1");
    read1.start();
    Thread.sleep(100);
    Thread write = new Thread(new WriteThread(testLock), "写线程 -- 1");
    write.start();
    Thread.sleep(100);
    Thread read2 = new Thread(new ReadThread(testLock), "读线程 -- 2");
    read2.start();
  }

  private class TestLock {
    private String string = null;
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock readLock = readWriteLock.readLock();
    private Lock writeLock = readWriteLock.writeLock();

    public void set(String s) {
      writeLock.lock();
      try {
//      writeLock.tryLock(10, TimeUnit.SECONDS);
        string = s;
      } finally {
        writeLock.unlock();
      }
    }

    public String getString() {
      readLock.lock();
      System.out.println(Thread.currentThread());
      try {
        while (true) {
        }
      } finally {
        readLock.unlock();
      }
    }
  }

  class WriteThread implements Runnable {
    private TestLock testLock;
    public WriteThread(TestLock testLock) {
      this.testLock = testLock;
    }

    @Override
    public void run() {
      testLock.set("射不进去,怎么办?");
    }
  }

  class ReadThread implements Runnable {
    private TestLock testLock;
    public ReadThread(TestLock testLock) {
      this.testLock = testLock;
    }

    @Override
    public void run() {
      testLock.getString();
    }
  }
}

我们用jstack查看一下线程,看到读线程2和写线程1确实处于WAITING的状态。

排查项目后,业务代码并没有问题,转而看下ReentrantReadWriteLock或AQS是否有什么问题被我忽略的。

第一时间关注共享锁,因为独占锁的实现逻辑我确定很清晰了,很快我似乎看到自己想要的方法。

public static class ReadLock implements Lock, java.io.Serializable {
  public void lock() {
    //if(tryAcquireShared(arg) < 0) doAcquireShared(arg);
    sync.acquireShared(1);
  }
}
abstract static class Sync extends AbstractQueuedSynchronizer {
  protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    //计算stata,若独占锁被占,且持有锁非本线程,返回-1等待挂起
    if (exclusiveCount(c) != 0 &&
      getExclusiveOwnerThread() != current)
      return -1;
    //计算获取共享锁的线程数
    int r = sharedCount(c);
    //readerShouldBlock检查读线程是否要阻塞
    if (!readerShouldBlock() &&
      //线程数必须少于65535
      r < MAX_COUNT &&
      //符合上诉两个条件,CAS(r, r+1)
      compareAndSetState(c, c + SHARED_UNIT)) {
      //下面的逻辑就不说了,很简单
      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;
    }
    return fullTryAcquireShared(current);
  }
}

嗯,没错,方法readerShouldBlock()十分瞩目,几乎不用看上下文就定位到该方法。因为默认非公平锁,所以直接关注NonfairSync。

static final class NonfairSync extends Sync {
  final boolean writerShouldBlock() {
      return false;
  }
  final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
  }
}
//下面方法在ASQ中
final boolean apparentlyFirstQueuedIsExclusive() {
  Node h, s;
  return (h = head) != null && //head非空
      (s = h.next)  != null && //后续节点非空
      !s.isShared()         && //后续节点是否为写线程
      s.thread != null;        //后续节点线程非空
}

apparentlyFirstQueuedIsExclusive什么作用,检查持锁线程head后续节点s是否为写锁,若真则返回true。结合tryAcquireShared的逻辑,如果true意味着读线程会被挂起无法共享锁。

这好像就说得通了,当持锁的是读线程时,跟随其后的是一个写线程,那么再后面来的读线程是无法获取读锁的,只有等待写线程执行完后,才能竞争。

这是jdk为了避免写线程过分饥渴,而做出的策略。但有坑点就是,如果某一读线程执行时间过长,甚至陷入死循环,后续线程会无限期挂起,严重程度堪比死锁。为避免这种情况,除了确保读线程不会有问题外,尽量用tryLock,超时我们可以做出响应。

当然也可以自己实现ReentrantReadWriteLock的读写锁竞争策略(排队任务无视先后顺序,读线程直接获取读锁放行;等待所有读线程unlock读锁,写线程才可以进行lock),但还是算了吧,遇到读远多于写的场景时,写线程饥渴带来的麻烦更大,表示踩过坑,别介。

载自:https://my.oschina.net/meandme/blog/1839265

原文地址:https://www.cnblogs.com/septemberFrost/p/12155847.html

时间: 2024-11-08 23:59:13

【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!的相关文章

Java并发编程ReentrantReadWriteLock

基于AQS的前世今生,来学习并发工具类ReentrantReadWriteLock.本文将从ReentrantReadWriteLock的产生背景.源码原理解析和应用来学习这个并发工具类. 1. 产生背景 前面我们学习的重入锁ReentrantLock本质上还是互斥锁,每次最多只能有一个线程持有ReentrantLock.对于维护数据完整性来说,互斥通常是一种过于强硬的规则,因此也就不必要的限制了并发性.互斥是一种保守的加锁策略,虽然可以避免"写/写"冲突和"写/读"

java并发锁ReentrantReadWriteLock读写锁源码分析

1.ReentrantReadWriterLock基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为 如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁(锁降级):如果一个线程对资源加了读锁,其他线程可以继续加读锁. java.util.concurrent.locks中关于多写锁的接口:ReadWriteLock public interface ReadWriteLock { /** * Returns the lock used for r

java 并发(五)---AbstractQueuedSynchronizer(4)

读写锁 ReentrantReadWriteLock 首先我们来了解一下 ReentrantReadWriteLock 的作用是什么?和 ReentranLock 有什么区别?Reentrancy 英文的意思是可重入性.ReentrantReadWriteLock下文简称(rrwl)         下面总结来自   Java并发编程-ReentrantReadWriteLock ,你也可以从JDK 中阅读到这段. ReentrantReadWriteLock是Lock的另一种实现方式,我们已经

【Java并发编程实战】—–“J.U.C”:ReentrantReadWriteLock

ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/读"."读/写"."写/写"操作都不能同时发生.然而在实际的场景中我们就会遇到这种情况:有些资源并发的访问中,它大部分时间都是执行读操作,写操作比较少,但是读操作并不影响数据的一致性,如果在进行读操作时采用独占的锁机制,这样势必会大大降低吞吐量.所以如果能够做

【死磕Java并发】-----J.U.C之读写锁:ReentrantReadWriteLock

此篇博客所有源码均来自JDK 1.8 重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少.然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低.所以就提供了读写锁. 读写锁维护着一对锁,一个读锁和一个写锁.通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞. 读写锁的主要特

Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock

本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步组建的基础框架.该类主要包括: 1.模式,分为共享和独占. 2.volatile int state,用来表示锁的状态. 3.FIFO双向队列,用来维护等待获取锁的线程. AQS部分代码及说明如下: public abstract class AbstractQueuedSynchronizer extend

Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock(转)

本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步组建的基础框架.该类主要包括: 1.模式,分为共享和独占. 2.volatile int state,用来表示锁的状态. 3.FIFO双向队列,用来维护等待获取锁的线程. AQS部分代码及说明如下: public abstract class AbstractQueuedSynchronizer extend

玩转Java并发工具,精通JUC,成为并发多面手

玩转Java并发工具,精通JUC,成为并发多面手 深度解密JUC库,提升五位一体的并发综合实力 使用场景+作用+底层原理逐一解读,吃透JUC并发包 掌握丰富的并发工具,解决实际问题,面试.工作轻松搞定 链接:https://pan.baidu.com/s/1ehvQUq9LUmmrZHYH7_DuGw 提取码:jyfs 全网程序学习资料,包含Java后端.前端.人工智能大数据.Python.数据结构和算法.运维.测试.面试相关等课程,所有视频资料均无加密,普通播放器就可播放,加客服微信coder

Java并发编程深入学习——Lock锁

Lock锁介绍 ??在Java 5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile.Java 5.0 增加了一种新的机制:ReentrantLock.它并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能. Lock接口 Lock接口位于java.util.concurrent.locks包中,它定义了一组抽象的加锁操作. public interface Lock { //获取锁 void lock(); // 如果当