并发编程学习案例-ReentrantReadWriteLock非公平的情况下读锁插队和写锁插队场景复现

本文通过代码示例展示了JavaReentrantReadWriteLock在非公平模式下,写锁可以随时插队,而读锁仅在等待队列头节点不是写线程时可以插队的现象。作者通过模拟多线程场景,证实了写线程和特定条件下读线程的插队行为。
摘要由CSDN通过智能技术生成

一、前言

Java ReentrantReadWriteLockReadWriteLock 的实现类,可以分出2把锁,ReentrantReadWriteLock.ReadLock 读锁和 ReentrantReadWriteLock.WriteLock写锁。我们知道读读之间可以共享,读写、写写是互斥的,这样并发度比 ReentrantLock 这种互斥锁更高。ReentrantReadWriteLock在Java 中锁分公平和非公平:

  • 在公平的场景下,多个线程会排队按顺进行加锁和释放锁;
  • 在非公平的场景下如果排队中的线程在唤醒期间还未唤醒时,此时如果有其它写线程加入则无需进入对列等待可插队获取到锁;如果在唤醒期间还未唤醒时,排队的队列里头节点是读线程,此时如果有其它读线程加入则无需进入对列等待可插队获取到读锁;。这里复现 ReentrantReadWriteLock 读锁在特定的场景下插队的场景复现

总结:

  • 写锁可以随时插队
  • 读锁仅在等待对列头节点不是想获取写锁的线程的时候可以进行插队

二、源码

ReentrantReadWriteLock 的源码里非公平的实现,writeShouldBlock()写总能插队,readerShouldBlock()读在当前对列第一个线程是排它时,可以进行插队,下面给出代码演示案例

在这里插入图片描述

三、 代码案例

(一)复现写的时候插队场景

依次启动线程一(写线程),线程二线程三(读线程),线程四(写线程),线程五线程六(读线程),然后再启动一个子线程来创建1K个写线程进行尝试写。
线程一先获取写锁,其它线程则进入对列阻塞等待,当线程一处理完毕释放锁的瞬间,如果有非中文的写线程在线程二三四五六前先获取到写线程,则说明,这些写线程进行了插队。

package com.lvzb.lock;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 非公平的读写锁,写锁插队 和 读锁插队案例
 *
 * @author: lvzb31988
 * @date: 2023/02/03 17:16
 */
public class NofaireBargeDemoTest {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    public void read() {
        System.out.println(Thread.currentThread().getName() + "尝试获取读锁");
        readLock.lock();
        try {
            TimeUnit.MILLISECONDS.sleep(20);
            System.out.println(Thread.currentThread().getName() + " 获取到了读锁...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
    }

    public void write() {
        System.out.println(Thread.currentThread().getName() + "尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 获取到了写锁...");
            TimeUnit.MILLISECONDS.sleep(45);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + " 释放到了写锁...");
            writeLock.unlock();
        }
    }

    /**
     * 模拟出来 写锁插队的场景
     *
     * @throws InterruptedException
     */
    @Test
    void nofairTest1() throws InterruptedException {
        NofaireBargeDemoTest bargeDemo = new NofaireBargeDemoTest();

        List<Thread> arr = new ArrayList<>();
        new Thread(bargeDemo::write, "线程一").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程二").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程三").start();
        Thread.sleep(5);
        new Thread(bargeDemo::write, "线程四").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程五").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程六").start();
        new Thread(() -> {
            for (int i = 0; i < 800; i++) {
                new Thread(bargeDemo::write, "线程" + i).start();
            }
        }).start();


        TimeUnit.SECONDS.sleep(50);
    }
    
}

参考执行结果

日志打印结果 第一个写线程释放瞬间,被其376号刚创建的写线程给获取执行,而没有直接执行队列里早已等待的读线程二和三


线程一尝试获取写锁
线程一 获取到了写锁...
线程二尝试获取读锁
线程三尝试获取读锁
线程四尝试获取写锁
线程五尝试获取读锁
线程六尝试获取读锁
线程0尝试获取写锁
线程1尝试获取写锁
线程2尝试获取写锁
线程3尝试获取写锁
线程4尝试获取写锁
线程5尝试获取写锁
线程6尝试获取写锁
线程7尝试获取写锁
线程8尝试获取写锁
..........
线程371尝试获取写锁
线程289尝试获取写锁
线程373尝试获取写锁
线程374尝试获取写锁
线程375尝试获取写锁
线程一 释放到了写锁...
线程376尝试获取写锁
线程377尝试获取写锁
线程376 获取到了写锁...
线程161尝试获取写锁
线程474尝试获取写锁
线程473尝试获取写锁
线程472尝试获取写锁
线程475尝试获取写锁
..........
线程768尝试获取写锁
线程792尝试获取写锁
线程790尝试获取写锁
线程793尝试获取写锁
线程794尝试获取写锁
线程758尝试获取写锁
线程789尝试获取写锁
线程784尝试获取写锁
线程776尝试获取写锁
线程786尝试获取写锁
线程779尝试获取写锁
线程760尝试获取写锁
线程781尝试获取写锁
线程778尝试获取写锁
线程783尝试获取写锁
线程788尝试获取写锁
线程785尝试获取写锁
线程787尝试获取写锁
线程780尝试获取写锁
线程376 释放到了写锁...
线程二 获取到了读锁...
线程三 获取到了读锁...
线程四 获取到了写锁...
线程四 释放到了写锁...
线程五 获取到了读锁...
线程六 获取到了读锁...
线程0 获取到了写锁...
线程0 释放到了写锁...
线程1 获取到了写锁...
.......

(二)复现读的时候插队

依次启动线程一(写线程),线程二线程三(读线程),线程四(写线程),线程五线程六(读线程),然后再启动一个子线程来创建1K个读线程进行尝试写。
线程一先获取写锁,其它线程则进入对列阻塞等待,当线程一处理完毕释放锁的瞬间,如果中文的读线程二获取到读锁进行执行此时等待对列的头部是线程三,在三还没开始去读期间如果有非中文的读线程进行了读操作,则说明,这些读线程进行了插队。

package com.lvzb.lock;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 非公平的读写锁,写锁插队 和 读锁插队案例
 *
 * @author: lvzb31988
 * @date: 2023/02/03 17:16
 */
public class NofaireBargeDemoTest {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    public void read() {
        System.out.println(Thread.currentThread().getName() + "尝试获取读锁");
        readLock.lock();
        try {
            TimeUnit.MILLISECONDS.sleep(20);
            System.out.println(Thread.currentThread().getName() + " 获取到了读锁...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
    }

    public void write() {
        System.out.println(Thread.currentThread().getName() + "尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 获取到了写锁...");
            TimeUnit.MILLISECONDS.sleep(45);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + " 释放到了写锁...");
            writeLock.unlock();
        }
    }

    /**
     * 模拟读线程在队列头部是读线程时,新的读线程可以进行插队场景,
     * 大量的读线程需要在子线程里创建,主线程内创建模拟不出来
     *
     * @throws InterruptedException
     */
    @Test
    void nofairTest() throws InterruptedException {
        NofaireBargeDemoTest bargeDemo = new NofaireBargeDemoTest();

        List<Thread> arr = new ArrayList<>();
        new Thread(bargeDemo::write, "线程一").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程二").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程三").start();
        Thread.sleep(5);
        new Thread(bargeDemo::write, "线程四").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程五").start();
        Thread.sleep(5);
        new Thread(bargeDemo::read, "线程六").start();
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                new Thread(bargeDemo::read, "线程" + i).start();
            }
        }).start();


        TimeUnit.SECONDS.sleep(50);
    }
}

参考执行结果

从结果可以看到很多数字类型的读线程在写线程一释放锁后,抢在了读线程二和线程三之前进行插队

线程一尝试获取写锁
线程一 获取到了写锁...
线程二尝试获取读锁
线程三尝试获取读锁
线程四尝试获取写锁
线程五尝试获取读锁
线程六尝试获取读锁
线程0尝试获取读锁
线程2尝试获取读锁
线程1尝试获取读锁
线程4尝试获取读锁
线程5尝试获取读锁
线程9尝试获取读锁
线程3尝试获取读锁
........
线程363尝试获取读锁
线程422尝试获取读锁
线程365尝试获取读锁
线程一 释放到了写锁...
线程428尝试获取读锁
线程426尝试获取读锁
线程320尝试获取读锁
线程369尝试获取读锁
线程432尝试获取读锁
线程427尝试获取读锁
线程421尝试获取读锁
线程366尝试获取读锁
线程412尝试获取读锁
线程431尝试获取读锁
线程413尝试获取读锁
线程398尝试获取读锁
线程430尝试获取读锁
线程395尝试获取读锁
线程396尝试获取读锁
线程367尝试获取读锁
线程424尝试获取读锁
线程335尝试获取读锁
线程392尝试获取读锁
线程364尝试获取读锁
线程370尝试获取读锁
线程424 获取到了读锁...
线程366 获取到了读锁...
线程431 获取到了读锁...
线程336尝试获取读锁
线程384尝试获取读锁
.......
线程433尝试获取读锁
线程421 获取到了读锁...
线程338尝试获取读锁
线程459尝试获取读锁
线程二 获取到了读锁...
线程369 获取到了读锁...
线程330尝试获取读锁
线程392 获取到了读锁...
线程427 获取到了读锁...
线程370 获取到了读锁...
线程372尝试获取读锁
线程320 获取到了读锁...
线程三 获取到了读锁...
线程364 获取到了读锁...
线程426 获取到了读锁...
线程432 获取到了读锁...
........

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值