【JUC】读写锁

news2024/10/6 0:33:11

读写锁

文章目录

  • 读写锁
    • 1. ReentrantReadWriteLock概述
    • 2. 编码演示
      • 2.1 ReentrantLock实现
      • 2.2 ReentrantReadWriteLock实现
    • 3. ReentrantReadWriteLock
      • 3.1 锁降级
      • 3.2 锁降级的必要性
      • 3.3 饥饿问题
    • 4. StampedLock(邮戳锁也叫票据锁)
      • 4.1 特点
      • 4.2 三种访问模式
      • 4.3 缺点
      • 4.4 编码示例

1. ReentrantReadWriteLock概述

读写锁定义为:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。

特点:

  • 它只允许读读共存,而读写和写写依然是互斥的
  • 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁(切菜还是拍蒜选一个)也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行

ReentrantReadWriteLock:读写互斥,读读共享,读没有完成时候其它线程无法获得写锁

2. 编码演示

有一个缓存类Cache,需要进行读和写操作,如何使其性能达到最高(StampedLock请看StampedLock章节)?

2.1 ReentrantLock实现

public class Main {

    static class Cache {
        private final ReentrantLock lock = new ReentrantLock();

        Map<String, String> map = new HashMap<>();

        public void write(String key, String value){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "正在写入");
                map.put(key, value);
                TimeUnit.MILLISECONDS.sleep(500);
                System.out.println(Thread.currentThread().getName() + "完成写入");
            } catch (InterruptedException e) {
	            throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }

        public String read(String key) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "正在读取");
                TimeUnit.MILLISECONDS.sleep(200);
                System.out.println(Thread.currentThread().getName() + "完成读取");
                return map.get(key);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        long startTime = System.currentTimeMillis();
        Cache cache = new Cache();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(String.valueOf(finalI), String.valueOf(finalI));
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        for (int i = 5; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.read(String.valueOf(finalI));
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println("总消耗时间:" + (endTime - startTime) + "ms");
    }
}

输出

0正在写入
0完成写入
1正在写入
1完成写入
2正在写入
2完成写入
4正在写入
4完成写入
3正在写入
3完成写入
5正在读取
5完成读取
6正在读取
6完成读取
7正在读取
7完成读取
8正在读取
8完成读取
9正在读取
9完成读取
总消耗时间:3632ms

分析:读写,写写之间确实需要锁进行互斥控制,但是读读之间不需要锁控制,而是可以同时执行的,如何优化读读?引出ReentrantReadWriteLock

2.2 ReentrantReadWriteLock实现

public class Main {

    static class Cache {

//        private final ReentrantLock lock = new ReentrantLock();
        private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        Map<String, String> map = new HashMap<>();

        public void write(String key, String value){
//            lock.lock();
            readWriteLock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "正在写入");
                map.put(key, value);
                TimeUnit.MILLISECONDS.sleep(500);
                System.out.println(Thread.currentThread().getName() + "完成写入");
            } catch (InterruptedException e) {
	            throw new RuntimeException(e);
            } finally {
//                lock.unlock();
                readWriteLock.writeLock().unlock();
            }
        }

        public String read(String key) {
//            lock.lock();
            readWriteLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "正在读取");
                TimeUnit.MILLISECONDS.sleep(200);
                System.out.println(Thread.currentThread().getName() + "完成读取");
                return map.get(key);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
//                lock.unlock();
                readWriteLock.readLock().unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        long startTime = System.currentTimeMillis();
        Cache cache = new Cache();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(String.valueOf(finalI), String.valueOf(finalI));
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        for (int i = 5; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.read(String.valueOf(finalI));
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println("总消耗时间:" + (endTime - startTime) + "ms");
    }
}

输出

0正在写入
0完成写入
2正在写入
2完成写入
1正在写入
1完成写入
4正在写入
4完成写入
3正在写入
3完成写入
5正在读取
6正在读取
8正在读取
9正在读取
7正在读取
6完成读取
8完成读取
9完成读取
5完成读取
7完成读取
总消耗时间:2798ms

3. ReentrantReadWriteLock

允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持读并发,读读可以共享

3.1 锁降级

目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性

ReentrantReadWriteLock锁降级: 将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),程度变强叫做升级,反之叫做降级

写锁的降级,降级成为了读锁

  1. 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的 次序
  3. 如果释放了写锁,那么就完全转换为读锁。

在这里插入图片描述

public class Main {

    static int resource = 0;

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

        writeLock.lock();
        readLock.lock();
        resource++;
        writeLock.unlock();
        System.out.println("先写锁后读锁且交替:" + resource);
        readLock.unlock();

        readLock.lock();
        writeLock.lock();
        System.out.println("先读锁后写锁且交替:" + resource);
        resource++;
        readLock.unlock();
        writeLock.unlock();
    }
}

输出且程序锁死,不会终止程序

先写锁后读锁且交替:1

使用写锁的过程中可以加入读锁,反之则不行;用读锁的过程中,必须读完才能再使用写锁,否则会导致程序卡死

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

综上,

  • ReentrantReadWriteLock读的过程不允许写,只有等待读线程都释放了读锁后才能获取写锁,也就是写入必须等待,这是一种悲观的读锁

3.2 锁降级的必要性

CacheData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock(); // R1
        if (!cacheValid) {
            rwl.readLock().unlock(); // R1
            rwl.writeLock().lock(); // W1
            try {
                data = new Object();
                cacheValid = true;
                rwl.readLock(); // R2 在释放写锁前立刻抢夺读锁
            } finally {
                rwl.writeLock().unlock(); // W1
            }
        }
        try {
            // use(data);
        } finally {
            rwl.readLock().unlock(); // R2
        }
    }
}
  1. 代码中声明了一个volatile类型的cacheValid变量,保证其可见性

  2. 首先获取读锁,如果cache不可用,则释放读锁。获取写锁,在更改数据之前,再检查一次cachevalid的值,然后修改数据,将achevalid置为true,然后在释放写锁前立刻抢夺获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性

总结:一句话,同一个线程自己持有写锁时再去拿读锁,其本质相当于重入。

  • 如果违背锁降级的步骤
    • 如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误
  • 如果遵循锁降级的步骤
    • 线租C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线理C完成数据处理过程。释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的

3.3 饥饿问题

由上诉可知,当持有读锁时,将无法获取写锁,那么当大量读锁请求竞争资源时,写操作必须等待读锁全部释放才能获取到写锁,导致一直无法写入,这便是写饥饿问题,该问题将由JDK8中的StampedLock解决

4. StampedLock(邮戳锁也叫票据锁)

ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验

  • stamp,戳记,long类型,代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且当释放锁或转换锁的时候,都要传入最初获取的stamp值
  • 对短的只读代码段,使用乐观模式通常可以减少争用并提高吞吐量
  • 该锁目前生产上还较少使用

4.1 特点

  • 所有获取锁的方法,都返回一个邮戳 (Stamp),Stamp为零表示获取失败,其余都表示成功
  • 所有释放锁的方法,都需要一个邮戳 (Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
  • StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

4.2 三种访问模式

  • Reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似
  • Writing(写模式):功能和ReentrantRerdWriteLock的写锁类似
  • Optimistic reading (乐观读模式): 无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

4.3 缺点

  • StampedLock不支持重入
  • StampedLock的悲观读和写锁都不支持条件变量(Condition)
  • 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

4.4 编码示例

public class Main {

    static class Cache {

        private final StampedLock stampedLock = new StampedLock();
        Map<String, String> map = new HashMap<>();

        public void write(String key, String value){
            long lockStamped = stampedLock.writeLock();
            try {
                System.out.println(Thread.currentThread().getName() + "正在写入");
                map.put(key, value);
                TimeUnit.MILLISECONDS.sleep(500);
                System.out.println(Thread.currentThread().getName() + "完成写入");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                stampedLock.unlockWrite(lockStamped);
            }
        }

        public String read(String key) {
            long optimisticLockStamped;
            do {
                optimisticLockStamped = stampedLock.tryOptimisticRead();
                System.out.println(Thread.currentThread().getName() + "正在读取");
                try {
                    TimeUnit.MILLISECONDS.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if (stampedLock.validate(optimisticLockStamped)) {
                    System.out.println(Thread.currentThread().getName() + "完成读取");
                    return map.get(key);
                }
                System.out.println(Thread.currentThread().getName() + "读取失败,自旋");
            } while (true);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(15);
        long startTime = System.currentTimeMillis();
        Cache cache = new Cache();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(String.valueOf(finalI), String.valueOf(finalI));
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        for (int i = 5; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.read(String.valueOf(finalI));
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        for (int i = 10; i < 15; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(String.valueOf(finalI), String.valueOf(finalI));
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println("总消耗时间:" + (endTime - startTime) + "ms");
    }
}

输出:

0正在写入
5正在读取
7正在读取
8正在读取
6正在读取
9正在读取
0完成写入
4正在写入
4完成写入
3正在写入
3完成写入
2正在写入
2完成写入
7读取失败,自旋
7正在读取
5读取失败,自旋
6读取失败,自旋
6正在读取
8读取失败,自旋
8正在读取
9读取失败,自旋
9正在读取
5正在读取
1正在写入
1完成写入
10正在写入
10完成写入
11正在写入
11完成写入
12正在写入
8读取失败,自旋
8正在读取
7读取失败,自旋
7正在读取
9读取失败,自旋
9正在读取
5读取失败,自旋
5正在读取
6读取失败,自旋
6正在读取
12完成写入
13正在写入
13完成写入
14正在写入
14完成写入
9读取失败,自旋
9正在读取
5读取失败,自旋
7读取失败,自旋
6读取失败,自旋
6正在读取
8读取失败,自旋
8正在读取
7正在读取
5正在读取
5完成读取
7完成读取
8完成读取
6完成读取
9完成读取
总消耗时间:8073ms

读取过程中也可以写入,只要读取过程中存在写入,则该次读取失败,进行自旋,再次读取

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

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

相关文章

微信如何自动添加好友?多账号如何统一自动加好友?

有朋友因为工作需要&#xff0c;要将一批名单加到微信好友中&#xff08;已知手机号&#xff09;。目前的方法是通过QQ同步助手&#xff0c;把电脑上的名单同步到手机上&#xff0c;然后再由微信自带的通讯录好友推荐来发起好友申请。 但是&#xff0c;这个做法的问题是&#…

《视觉 SLAM 十四讲》V2 第 12 讲 建图

文章目录 12.2 单目稠密 重建12.2.2 极线搜索 && 块匹配12.2.3 高斯分布的深度滤波器 12.3 单目稠密重建 【Code】待改进12.3.4 图像间的变换 12.4 RGB-D 稠密建图12.4.1 点云地图 【Code】查询OpenCV版本 opencv_version 12.4.2 从点云 重建 网格 【Code】查看PCL 版本…

three-tile: 三维瓦片地图框架应用示例源码-GitHub

上篇文章&#xff1a;three-tile: 从头编写一套三维瓦片地图框架&#xff0c;大家是否有兴趣&#xff1f; three-tile&#xff0c;一个基于threejs的三维瓦片地图框架。最近花了点时间写了一些应用示例&#xff0c;放在GitHub上了&#xff0c;其中包含three-tile框架打包后的代…

第二证券:A股三季报披露全面启动 多领域上市公司业绩表现亮点纷呈

A股上市公司三季报宣告全面发动。Wind数据闪现&#xff0c;到10月17日记者发稿&#xff0c;来自沪深北三大交易所近80家上市公司首要晒出了最新运营效果体现的“效果单”。本周&#xff0c;相关财报宣告家数也将增至270家左右。与此同时&#xff0c;10月以来&#xff0c;亦有不…

移远通信携手MIKROE推出搭载LC29H系列模组的Click boards开发板,为物联网应用带来高精定位服务

近日&#xff0c;移远通信与MikroElektronika&#xff08;以下简称“MIKROE”&#xff09;展开合作&#xff0c;基于移远LC29H系列模组推出了多款支持实时动态载波相位差分技术&#xff08;RTK&#xff09;和惯性导航&#xff08;DR&#xff09;技术的Click Boards™ 开发板&am…

sql中的group by 举例子数据库日期带汉字转换2023年10月18天

sql中的group by 举例子 sql中 group by多个字段&#xff0c;对所有字段做group by_group by 多个字段_Foools的博客-CSDN博客 【精选】玩转SQL语句之group by 多字段分组查询与having子句&#xff0c;一篇解决你的疑惑&#xff01;_sql多个分组查询-CSDN博客 select to_char…

什么是MTC认证?MTC证书要求?主要测试项目是?需要提供哪些资料

金属类产品MTC证书-欧盟与英国清关新要求&#xff01; 英国禁止进口俄钢材的通告内容 从2023年 9 月 30 日起&#xff0c;欧盟和英国将对在第三国加工的特定钢铁产品实施新的制裁&#xff0c;这些产品包含俄罗斯原产的钢铁原料。进口商需要在进口时申报进口货物是否合规。 什…

儿童珠宝首饰上亚马逊美国站合规标准是什么?如何办理?

儿童珠宝首饰 儿童珠宝首饰指原则上由 12 岁及以下儿童作为装饰品移除或穿戴的商品。本政策涵盖的儿童珠宝首饰&#xff0c;包括但不限于脚链、手链、耳环、项链、戒指、珠宝首饰制作或维修套装以及钟表。 亚马逊儿童珠宝首饰政策 亚马逊要求所有儿童珠宝首饰均经过检测并符合…

vcruntime140.dll如何修复,五种修复vcruntime140.dll方法分享

当我们在运行某些程序时&#xff0c;可能会遇到“找不到VCRuntime140.dll”的错误。这个错误通常表示我们的系统中缺少某个重要的动态链接库文件&#xff0c;而VCRuntime140.dll正是其中之一。本文将详细介绍如何修复这个问题。 一、问题出现的原因 首先&#xff0c;我们需要了…

五大场景告诉你,如何把iPaaS运用到实处

iPaaS&#xff08;Integration Platform as a Service&#xff09;是一种云计算服务模式&#xff0c;提供了企业系统和数据之间集成的平台。它具有以下应用价值&#xff1a; 1、整合系统服务&#xff1a;iPaaS可以将不同的系统和服务进行集成&#xff0c;促进数据的流动和共享…

SpringCloud对服务内某个client进行单独配置

文章目录 问题解决过程问题解决 问题 我们的微服务项目用的是springCloud&#xff0c;某个微服务接口因为数据处理量大&#xff0c;出现了接口超时的情况&#xff0c;我们需要单独修改这一个feignClient的超时时间。 解决过程 一开始项目只是在application文件里面进行了全局…

企业如何自定义人力资源和财务报表?

企业自定义人力资源和财务报表是一种重要的能力&#xff0c;它允许企业根据其特定需求和目标创建和定制报表。以下是一些详细的步骤和说明&#xff0c;帮助企业实现人力资源和财务报表的自定义。 1. 确定报表需求&#xff1a; 首先&#xff0c;企业需要明确自己对人力资源和财…

Camera BSP之GPIO/I2C/PMIC简介

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、GPIO介绍二、IC 总线概括三、PMIC 概括四、思考 一、GPIO介绍 GPIO&#xff1a;General Purpose Input Output &#xff08;通用输入/输出&#xf…

修炼k8s+flink+hdfs+dlink(五:安装dockers,cri-docker,harbor仓库,k8s)

一&#xff1a;安装docker。&#xff08;所有服务器都要安装&#xff09; 安装必要的一些系统工具 sudo yum install -y yum-utils device-mapper-persistent-data lvm2添加软件源信息 sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/cent…

RemObjects Elements 12.0 Crack

Elements 是一个现代多功能软件开发工具链。 它支持六种流行的编程语言&#xff1a;Oxygene (Object Pascal)、C#、Java、Mercury (Visual Basic.NET™)、Go 和 Swift&#xff0c;适用于所有现代平台。 使用 Elements&#xff0c;您可以为您喜欢的任何平台进行编程- 无论是单…

VR全景广告:让消费者体验沉浸式交互,让营销更有趣

好的产品都是需要广告宣传的&#xff0c;随着科技的不断发展&#xff0c;市面上的广告也和多年前的传统广告不同&#xff0c;通过VR技术&#xff0c;可以让广告的观赏性以及科技感更加强烈&#xff0c;并且相比于视频广告&#xff0c;成本也更低。 在广告营销中&#xff0c;关键…

Python中Numpy的应用技巧

目录 1. 什么是 NumPy?2. NumPy 中的数组2.1. 创建数组2.2. 用Numpy的数据2.2.1. OpenCV2.2.2. Pandas 3. 数学计算3.1. 四则计算3.1.1. 矩阵乘法3.1.2. 点乘 3.2. 求逆矩阵与伪逆矩阵3.3. 判断矩阵相等3.4. np.eye()函数生成对角阵 4. 统计4.1. 最大值、最小值、均值条件4.2.…

混凝土基础的智能设计:VisualFoundation 12.0 Crack

实现混凝土基础的智能设计:工程师依靠 VisualFoundation:使用这个专注的工具可以更轻松、更强大地对基础进行建模。通用 FEA 工具&#xff08;如VisualAnalysis&#xff09;可以做很多事情&#xff0c;但对于特定于基础的工程来说&#xff0c;这更快、更智能。 草图边界 快速绘…

2023系统架构师---信息系统基础知识

目录 信息系统基础知识 信息系统概述 信息系统开发方法 1.结构化方法 2&#xff0c;原型法 3.面向对象方法 4.面向服务的方法 信息系统基础知识 信息系统是一个由人、计算机等组成的能进行信息的收集、传递、存储、加工、维护和使用的系统&#xff0c;它是一门综合了经济…

混凝土和砖石设计:IES Quick Suite Crack

工IES Quick Suite程师凭借快速产品取得成功&#xff1a;用于混凝土和砖石设计的四个独立工具。这些实用程序体积小&#xff0c;功能强大且速度极快。它们在检查现有结构或设计新结构方面具有许多共同的优点。 为了方便起见&#xff0c;这些产品捆绑在一个安装程序中。单独使用…