读写锁ReentrantReadWriteLock

news2024/10/5 16:22:59

        读写锁ReentrantReadWriteLock是JDK1.5提供的一个工具锁,适用于读多写少的场景,将读写分离,从而提高并发性。读写锁允许的情况:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

        ReentrantReadWriteLock可用于提高某些集合的并发性。仅当集合预计很大时,读线程比写线程多,并且需要用超过同步开销的开销时,使用ReentrantReadWriteLocks通常是值得的。

        ReentrantReadWriteLock实现了ReadWriteLock接口。

          ReadWriteLock接口

        ReadWriteLock接口暴露了两个Lock对象,一个用来读,另一个用来写。读取ReadWriteLock锁守护的数据,必须先获得读取的锁;当需要修改ReadWriteLock锁守护的数据时,必须先获得写入的锁。读写锁加锁策略允许多个同时存在的读锁,但只允许一个写者。也就是说,读锁是共享锁,写锁是排他锁,读锁和写锁不能同时存在。

  • 读锁 - 如果没有线程锁定ReadWriteLock进行写入,则多线程可以访问读锁。
  • 写锁 - 如果没有线程正在读或写,那么一个线程可以访问写锁。

1. 可重入

        顾名思义,ReentrantReadWriteLock是可重入锁,它的读锁、写锁都是可重入的。

public class ReentrantLockTest {

    private static final ReentrantReadWriteLock reentrantReadWriteLock
            = new ReentrantReadWriteLock(true);

    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock
            .readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock
            .writeLock();

    public void reentrantRead() {
        readLock.lock();
        read();
        readLock.unlock();
    }

    public void reentrantWrite() {
        writeLock.lock();
        write();
        writeLock.unlock();
    }

    public static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    public static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockTest test = new ReentrantLockTest();
        test.reentrantRead();
        test.reentrantWrite();
    }
}

        运行结果:

2. 公平锁

       ReentrantReadWriteLock可以事公平锁,只需在构造函数的参数中传入 true:

ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true); 

       在获取读锁之前,线程会检查 readerShouldBlock() 方法,同样,在获取写锁之前,线程会检查 writerShouldBlock() 方法,来决定是否需要插队或者是去排队:

final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}

      在公平锁的情况下,只要等待队列中有线程在等待,也就是 hasQueuedPredecessors() 返回 true 的时候,那么 writer 和 reader 都会阻塞,也就是一律不允许插队。因此,对于公平锁而言,在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁。下面的测试代码做了一个简单的验证:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class FairLocking {

    public static final boolean FAIR = true;
    private static final int NUM_THREADS = 3;

    private static volatile int expectedIndex = 0;

    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLock.WriteLock lock = new ReentrantReadWriteLock(FAIR).writeLock();

        // we grab the lock to start to make sure the threads don't start until we're ready
        lock.lock();

        for (int i = 0; i < NUM_THREADS; i++) {
            new Thread(new ExampleRunnable(i, lock)).start();

            // a cheap way to make sure that runnable 0 requests the first lock 
            // before runnable 1
            // 这里休眠,可以保证每个线程在第一次循环的时候,都没有获得锁,从第二次
            // 循环开始后,每个线程轮流去尝试获得锁
            Thread.sleep(10);
        }

        // let the threads go
        lock.unlock();
    }

    private static class ExampleRunnable implements Runnable {
        private final int index;
        private final ReentrantReadWriteLock.WriteLock writeLock;

        public ExampleRunnable(int index, ReentrantReadWriteLock.WriteLock writeLock) {
            this.index = index;
            this.writeLock = writeLock;
        }

        public void run() {
            while(true) {
                writeLock.lock();
                try {
                    // this sleep is a cheap way to make sure the previous thread loops
                    // around before another thread grabs the lock, does its work,
                    // loops around and requests the lock again ahead of it.
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    //ignored
                }

                if (index != expectedIndex) {
                    System.out.printf("Unexpected thread obtained lock! " +
                            "Expected: %d Actual: %d%n", expectedIndex, index);
                    System.exit(0);
                }

                expectedIndex = (expectedIndex+1) % NUM_THREADS;
                writeLock.unlock();
            }
        }
    }
}

       这段测试代码给每个线程绑定了一个下标,每个线程在各自的循环中轮流去获取写锁。如果不是遵循先请求先获取的方式,那么期望的下标值跟获得锁的线程下标必然会不一致。运行这段代码,会发现一直在循环中,证明了公平性的阻塞策略。

 3. 非公平锁的插队策略

        在构造函数的参数中传入 false,就是非公平锁。ReentrantReadWriteLock默认是非公平锁。非公平锁对读写锁排队的实现如下:

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

        可以看到, 写锁的线程返回值是 false,所以写锁是随时可以插队的;而对于读锁线程来说,就没那么简单了,它需要判断队列中第一个等待的结点是否是写线程,如果是,则读线程不允许插队,否则读线程可以闯入。也就是说,读锁只有在头结点不是写线程的情况是可以插队!

3.1 写者闯入

        写者是随时可以插队的,以下代码可以验证:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class WriteLockBargepQueue {

    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "尝试获得读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        System.out.println(Thread.currentThread().getName() + "尝试获得写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> write(),"线程一").start();
        new Thread(() -> read(),"线程二").start();
        new Thread(() -> read(),"线程三").start();
        new Thread(() -> write(),"线程四").start();
        new Thread(() -> read(),"线程五").start();
        new Thread(() -> read(),"线程六").start();

    }
}

        运行结果:

线程二尝试获得读锁
线程一尝试获得写锁
线程三尝试获得读锁
线程二得到读锁,正在读取
线程四尝试获得写锁
线程五尝试获得读锁
线程六尝试获得读锁
线程二释放读锁
线程一得到写锁,正在写入
线程一释放写锁
线程四得到写锁,正在写入
线程四释放写锁
线程三得到读锁,正在读取
线程五得到读锁,正在读取
线程六得到读锁,正在读取
线程三释放读锁
线程五释放读锁
线程六释放读锁

Process finished with exit code 0

         线程三原本排在线程四之前,但是当线程一释放写锁后,线程四优先拿到写锁。

3.2 读者闯入

        读者已经获得锁,写锁排在等待队列的首位,接着读者线程加入队列中。排在写锁后面,如果允许读者线程闯入,这样看似提高了效率,但如果想要读取的线程不停地增加,读线程不断闯入,那么降导致写锁线程长时间拿不到写锁,造成写者饥饿。下面用代码验证复原一下这个场景:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadLockBargepQueue {

    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "尝试获得读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        System.out.println(Thread.currentThread().getName() + "尝试获得写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read(),"Thread-2r").start();
        new Thread(() -> read(),"Thread-4r").start();
        new Thread(() -> write(),"Thread-3w").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                new Thread(() -> read(),"线程" + i).start();
            }
        }).start();
    }

}

        这段代码中,2号、4号线程是读线程,获得读锁;紧随其后是3号写线程,它在队首等待,其后是50个读线程,测试结果如下:

        3w线程在2r、4r线程之后,当2r、4r线程获得读锁后,3w线程自然就排在队首了。 

        如测试结果所示,2r、4r线程释放后,3w线程拿到写锁。可见,当写线程排在队首时,读者是无法闯入的。 

        如果调整一下顺序,使得排在队首的是读线程,那么读者就可以闯入了:

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read(),"Thread-2r").start();
        new Thread(() -> read(),"Thread-4r").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                new Thread(() -> read(),"读者线程" + i).start();
            }
        }).start();
        Thread.sleep(5);
        new Thread(() -> write(),"Thread-3w").start();
    }

       2线程本来排在3线程之前,但3线程却提前拿到读锁。 

        ReentrantReadWriteLock使用的注意事项 读锁不支持条件变量 重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待 重入时支持降级: 持有写锁的情况下可以去获取读锁

这个类的构造函数接受一个可选的公平参数。当设置为true时,在争用项下,锁倾向于授予对等待时间最长的线程的访问权限。 但是,请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的多个线程中的一个可以连续多次获得该锁,而其他活动线程则没有进展,目前也没有保存该锁。

4. 支持锁降级

       锁降级指的是写锁降级成为读锁。遵循获取写锁、获取读锁,再释放写锁次序,写锁能够降级成为读锁。

        锁降级主要是为了防止数据没有刷回到主内存,导致其他线程取到的值不一致!如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

        锁降级也可以有助于提高效率。如果一直使用写锁,最后释放写锁,虽然线程安全,但是有时候没这个必要,假设只有一处需要更新数据,后面的只是读,这个时候还用写锁就不能多个线程读了,浪费资源,这个时候就用锁的降级提高整体效率。

         以下代码是一个典型的锁降级例子:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CachedData {

    Object data;
    // 保证可见性
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();


    void processCachedData() {
        // 首先获取读锁
        rwl.readLock().lock();
        // 判断缓存是否有效,有效直接输出
        if (!cacheValid) {
            // 无效就把读锁释放,上写锁
            // 锁降级从写锁获取到开始,这个时候有可能有其他线程抢先获取了写锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // 因为可能有其他写线程抢到写锁并更新了缓存,所以需要再次判断如果缓存还是无效
                if (!cacheValid) {
                    // 更新data,缓存设为有效
                    data = new Object();
                    cacheValid = true;
                }
                // 因为我们想要使用数据(在后续打印出data),所以请求读锁
                rwl.readLock().lock();
            } finally {
                // 确保写锁释放,整个时候就是读锁了,然后打印data
                rwl.writeLock().unlock();
            }
        }
        try {
            System.out.println(data);
        } finally {
            // 确保读锁能释放
            rwl.readLock().unlock();
        }
    }
}

        锁降级的正确步骤是:持有写锁 -> 持有读锁 -> 释放写锁 -> 释放读锁,为什么要在写锁释放之前获取读锁呢?如果在释放写锁后再拿到读锁,当线程A写锁释放,线程B抢先拿到写锁,并修改数据,此时A再拿到读锁,读到将是被线程B破坏掉的数据,从而产生“脏读”。锁降级的本质也是锁的重入性,可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。

5. 不支持锁升级

        ReentrantReadWriteLock不支持锁的升级。

        在ReentrantReadWriteLock中,读锁和写锁之间是互斥的。当一个线程持有读锁时,其他线程可以继续获取读锁,但不能获取写锁。这是为了保证读操作的并发性,即多个线程可以同时读取共享资源。

        当一个线程持有读锁时,如果尝试获取写锁,由于写锁的独占性,写锁无法被其他线程获取。如果两个以上读线程都想要升级为写锁,并且不释放读锁,就会陷入永久等待的状态,造成死锁。

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

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

相关文章

IP地址会暴露我们的隐私吗?

IP地址在某种程度上可能会暴露个人或组织的隐私。以下是一些关于IP地址可能影响隐私的情况&#xff1a; 地理位置信息&#xff1a; IP地址的一部分信息是与地理位置相关的&#xff0c;因此可以用于确定用户或组织的大致地理位置。这可能泄露用户的身份或活动的地理背景。 互联…

Doris:Binlog Load导入数据

Binlog Load提供了一种使Doris增量同步用户在Mysql数据库的对数据更新操作的CDC(Change Data Capture)功能。Binlog Load需要依赖canal作为中间媒介&#xff0c;让canal伪造成一个从节点去获取Mysql主节点上的Binlog并解析&#xff0c;再由Doris去获取Canal上解析好的数据。 1…

centos k8s安装dapr

文章目录 安装helm更新helm库初始化dapr高可用方式安装 卸载dapr验证k8s的dapr安装rocketmq总结 安装helm 三个包放到一个目录下 chmod x get ./get helm version更新helm库 helm repo add dapr https://dapr.github.io/helm-charts/ helm repo update helm search repo dapr …

C/C++轻量级并发TCP服务器框架Zinx-游戏服务器开发004:游戏核心消息处理 - 玩家类的实现

文章目录 0 代码仓库1 需求2 AOI设计2.1 AOI算法简介2.2 AOI数据结构及实现2.2.1 玩家2.2.2 网格对象2.2.3 游戏世界矩形2.2.4 获取周围玩家的实现2.2.5 代码测试 2.3 GameRole结合AOI创建玩家2.3.1 创建游戏世界全局对象-GameRole继承AOIWorld的Player2.3.2 把玩家到游戏世界的…

Qt OpenMP使用

1、概念 OpenMP是一种用于共享内存并行系统的多线程程序设计方案&#xff0c;支持的编程语言包括C、C和Fortran。OpenMP提供了对并行算法的高层抽象描述&#xff0c;特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma指令&#xff0c;自动将程序并行处理&…

匿名类型与元组(ValueTuple)

匿名类型与元组&#xff08;ValueTuple&#xff09; 匿名类型 简介&#xff1a;匿名类型提供了一种方便的方法&#xff0c;可用来将一组相关的属性封装到单个对象中。匿名对象由编译器在编译时动态生成&#xff0c;它是只读的&#xff0c;且只在当前作用域中可见。它可以方便…

CDN加速技术:降低企业云服务成本的有效利用

在当今数字化时代&#xff0c;云服务已经成为企业运营的不可或缺的一部分。然而&#xff0c;与此同时&#xff0c;云服务的需求也在不断增长&#xff0c;使企业不得不应对更大的数据传输和负载。这就引出了一个关键问题&#xff1a;如何有效降低企业云服务成本&#xff0c;同时…

kubernetes-调度

目录 一、k8s调度简介 二、影响kubernetes调度的因素 1、nodename 2、nodeselector 3、亲和与反亲和 &#xff08;1&#xff09;nodeaffinity &#xff08;2&#xff09;podaffinity&#xff08;亲和&#xff09; &#xff08;3&#xff09;podantiaffinity&#xff0…

易点易动固定资产管理系统:提升固定资产领用效率的解决方案

在企业运营中&#xff0c;固定资产的领用和管理是一个重要的环节。然而&#xff0c;对于许多企业来说&#xff0c;固定资产领用的过程往往存在效率低下、信息不透明等问题。为了帮助企业提升固定资产领用效率并实现用量控制管理&#xff0c;我们引入了易点易动固定资产管理系统…

小知识:无源无线测温传感器可以安装在哪些部位?

无源无线测温传感器采用超低功耗设计&#xff1a;主芯片采用美国TI公司&#xff0c;功耗低&#xff0c;低可至0.03mw&#xff0c;区别于传统的感应供电&#xff0c;不存在发热现象。测温元件采用耐高温、高精度热敏电阻&#xff0c;测温范围宽至-40℃&#xff5e;200℃&#xf…

WebGL主要接口功能

WebGL&#xff08;Web Graphics Library&#xff09;提供了一组用于在Web浏览器中呈现3D和2D图形的接口类型和功能。下面是一些主要的WebGL接口类型和它们的功能&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交…

复盘一个诡异的Bug

该Bug的诡异之处在于这是一个由多种因素综合碰撞之后形成的综合体。纵观整个排查过程&#xff0c;一度被错误的目标误导&#xff0c;花费大量功夫后才找到问题点所在&#xff0c;成熟的组件在没有确凿证据之前不能随意怀疑其稳定性。 前言 此前在接入两台粒径谱仪&#xff08;…

tingpng 批量压缩工具

无聊的时候写的 自用 还行 https://ttkeji.lanzoul.com/iPCfY1e5wwwh

虹科示波器 | 汽车免拆检测 | 2017款路虎发现车行驶中发动机抖动且加速无力

一、故障现象 一辆2017款路虎发现车&#xff0c;搭载3.0L发动机&#xff0c;累计行驶里程约为3.8万km。车主反映&#xff0c;车辆在行驶过程中突然出现发动机抖动且加速无力的现象&#xff0c;于是请求拖车救援。 二、故障诊断 拖车到店后首先试车&#xff0c;发动机怠速轻微抖…

盈科视控荣获创响中国大赛第四名

近日&#xff0c;随着2023“创响中国”安徽省创新创业大赛省内赛区复赛的举办完成&#xff0c;60个项目从6个专项组中脱颖而出。 盈科视控凭借其【IC 载板及先进 PCB 智慧工厂服务商】参赛项目&#xff0c;荣获大赛第四名。 本次大赛由安徽省发改委、安徽省财政厅、合肥市人民…

图形学及图形学进展

有段时间没有来CSDN了&#xff0c;最近一直忙于工作&#xff0c;最近图形学方面&#xff0c;特别是重建图形学方面有了比较大的进展&#xff0c;然后NeRF-SLAM向也有不少进展&#xff0c;但由于ChatGPT风光无限&#xff0c;光芒都没有发出来&#xff0c;后续还是继续创作&#…

【已解决】ChatGPT报错“Unable to load history Retry“等问题

解决ChatGPT历史加载错误&#xff1a;“Unable to load history Retry”问题指南 引言 在使用ChatGPT时&#xff0c;您可能遇到过一个常见的错误提示&#xff1a;“Unable to load history Retry”。这可能会阻止您查看以前的对话历史。本文将为您提供一个详细的教程&#xf…

nodejs express vue 酒店预订系统源码

开发环境及工具&#xff1a; nodejs&#xff0c;vscode&#xff08;webstorm&#xff09;&#xff0c;大于mysql5.5 技术说明&#xff1a; nodejs express vue elementui 功能介绍&#xff1a; 用户端&#xff1a; 用户登录注册 首页显示轮播图&#xff0c;客房分类&…

2010年5月27日Go生态洞察:I/O中Go的热门问答

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

【JS】判断字符串是否为 url 的方法

文章目录 用法解析 用法解析 当你传递一个字符串给 URL 构造函数时: 如果字符串是一个有效的 URL&#xff0c;它将返回一个新的 URL 对象。否则&#xff0c;它将返回一个错误。 const url new URL("https://www.baidu.com/"); console.log(url);函数封装&#xf…