zoukankan      html  css  js  c++  java
  • 【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!

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


    过程大致如下:

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    public class ReadWriteLockTest {
    public static void main(String[] args) {
    ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
    }
     
    public ReadWriteLockTest() {
    try {
    init();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
     
    void init() 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的状态。

    jstackjstack

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    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。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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的读写锁竞争策略,但还是算了吧,遇到读远多于写的场景时,写线程饥渴带来的麻烦更大,表示踩过坑,别介。

    from: http://huangzehong.me/2018/07/02/20180702%20-%E3%80%90Java%E5%B9%B6%E5%8F%91%E3%80%91JUC%E2%80%94ReentrantReadWriteLock%E6%9C%89%E5%9D%91%EF%BC%8C%E5%B0%8F%E5%BF%83%E8%AF%BB%E9%94%81%EF%BC%81/

  • 相关阅读:
    JavaScript开发中几个常用知识点总结
    编写高质量代码改善C#程序的157个建议[勿选List<T>做基类、迭代器是只读的、慎用集合可写属性]
    编写高质量代码改善C#程序的157个建议[泛型集合、选择集合、集合的安全]
    C#基础知识系列十(集合)
    Json.Net6.0入门学习试水篇
    编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化]
    C#基础知识系列九(对IEnumerable和IEnumerator接口的糊涂认识)
    Asp.Net MVC3.0项目部署到Win7 64过程总结
    .sql文件l通过PLSQL导入到Oracle数据库
    PowerDesigner工具将表字段转成java实体
  • 原文地址:https://www.cnblogs.com/GarfieldEr007/p/10224179.html
Copyright © 2011-2022 走看看