第17章 读写锁分离设计模式(Java高并发编程详解:多线程与系统设计)

1.场景描述

对资源的访问一般包括两种类型的动作——读和写(更新、删除、增加等资源会发生变化的动作),如果多个线程在某个时刻都在进行资源的读操作,虽然有资源的竞争,但是这种竞争不足以引起数据不一致的情况发生,那么这个时候直接采用排他的方式加锁,就显得有些简单粗暴了。表1将两个线程对资源的访问动作进行了枚举,除了多线程在同一时间都进行读操作时不会引起冲突之外,其余的情况都会导致访问的冲突,需要对资源进行同步处理。

2.读写分离程序设计

2.1 接口定义

读写锁的类图

1.Lock接口定义
public interface Lock {
    // 获取显示锁, 没有获得锁的线程将被堵塞
    void lock() throws InterruptedException;

    // 释放获取的锁
    void unlock();
}

Lock接口定义了锁的基本操作, 加锁和解锁, 显式锁的操作强烈建议与try finally语句块一起使用,加锁和解锁说明如下。

  • lock() :当前线程尝试获得锁的拥有权, 在此期间有可能进入阻塞。
  • unlock() :释放锁, 其主要目的就是为了减少reader或者writer的数量。
2.ReadWriteLock接口定义

ReadWrite Lock虽然名字中有lock, 但是它并不是lock, 它主要是用于创建read lock和write lock的, 并且提供了查询功能用于查询当前有多少个reader和writer以及waiting中的writer, 根据我们在前文中的分析, 如果reader的个数大于0, 那就意味着writer的个数等于0, 反之writer的个数大于0(事实上writer最多只能为1) , 则reader的个数等于0,由于读和写,写和写之间都存在着冲突,因此这样的数字关系也就不奇怪了。

readLock() :该方法主要用来获得一个Read Lock。
writeLock() :同read Lock类似, 该方法用来获得Write Lock。
getWriting Writers) :获取当前有多少个线程正在进行写的操作, 最多是1个。
getWaiting Writers() :获取当前有多少个线程由于获得写锁而导致阻塞。
getReading Readers() :获取当前有多少个线程正在进行读的操作。

2.2程序实现

1.ReadWriteLockImpl

相对于Lock, ReadWrite Lock Impl更像是一个工厂类, 可以通过它创建不同类型的锁,我们将ReadWrite Lock Impl设计为包可见的类, 其主要目的是不想对外暴露更多的细节,在ReadWrite Lock Impl中还定义了非常多的包可见方法, 代码所示

public class ReadWriteLockImpl implements ReadWriteLock{

    // 定义对象锁
    private final Object MUTEX = new Object();

    // 当前有多少个线程正在写入
    private int writingWriters = 0;

    // 当前有多少个线程正在等待写入
    private int waitingWriters = 0;

    // 当前有多少个线程正在read
    private int readingReaders = 0;

    // read 和 write 的偏好设置
    private boolean preferWriter;

    // 默认情况下preferWrite为true
    public ReadWriteLockImpl() {
        this(true);
    }

    // 构造ReadWriteLockImpl并且传入preferWriter
    public ReadWriteLockImpl(boolean preferWriter) {
        this.preferWriter = preferWriter;
    }


    public Object getMUTEX() {
        return MUTEX;
    }

    public int getWaitingWriters() {
        return waitingWriters;
    }

    public boolean getPreferWriter() {
        return preferWriter;
    }

    // 创建读锁
    @Override
    public Lock readLock() {
        return new ReadLock(this);
    }

    // 创建写锁
    @Override
    public Lock writeLock() {
        return new WriteLock(this);
    }

    // 使写线程的数量增加
    void incrementWritingWriters() {
        this.writingWriters++;
    }

    // 使等待写入的线程数量增加
    void incrementWaitingWriters() {
        this.waitingWriters++;
    }


    // 使读线程的数量增加
    void incrementReadingReaders() {
        this.readingReaders++;
    }


    // 使写线程的数量减少
    void decrementWritingWriters() {
        this.writingWriters--;
    }


    // 使等待获取写入锁的数量减一
    void decrementWaitingWriters() {
        this.waitingWriters--;
    }

    // 使读取线程的数量减少
    void descementReadingReaders() {
        this.readingReaders--;
    }

    @Override
    public int getWritingWriters() {
        return this.writingWriters;
    }


    @Override
    public int getReadingReaders() {
        return this.readingReaders;
    }

    void changePrefer(boolean preferWriter) {
        this.preferWriter = preferWriter;
    }
}

虽然我们在开发一个读写锁,但是在实现的内部也需要一个锁进行数据同步以及线程之间的通信, 其中MUTEX的作用就在于此, 而prefer Writer的作用在于控制倾向性, 一般来说读写锁非常适用于读多写少的场景, 如果prefer Writer为false, 很多读线程都在读数据,那么写线程将会很难得到写的机会。

2.ReadLock

读锁是Lock的实现, 同样将其设计成包可见以透明其实现细节, 让使用者只用专注于对接口的调用,代码如所示

public class ReadLock implements Lock{

    private final ReadWriteLockImpl readWriteLock;

    ReadLock(ReadWriteLockImpl readWriteLock) {
        this.readWriteLock = readWriteLock;
    }


    @Override
    public void lock() throws InterruptedException {
        // 使用Mutex 作为 锁
        synchronized (readWriteLock.getMUTEX()) {
            // 若此时有线程在进行写操作,或者有写线程在等待并且偏向写锁的标识为
            // true时,就会无法获得读锁,只能被挂起
            while(readWriteLock.getWritingWriters() > 0
            || (readWriteLock.getPreferWriter() && readWriteLock.getWritingWriters() > 0 )) {
                readWriteLock.getMUTEX().wait();
            }
            readWriteLock.incrementReadingReaders();
        }
    }

    @Override
    public void unlock() {
        // 使用Mutex作为锁,并且进行同步
        synchronized (readWriteLock.getMUTEX()) {
            // 释放锁的过程就是使得当前reading的数量减一
            // 将perferWriter设置为true,可以使得writer线程获得更多的机会
            // 通知唤醒与Mutex关联monitor waitset中的线程
            readWriteLock.descementReadingReaders();
            readWriteLock.changePrefer(true);
            readWriteLock.getMUTEX().notifyAll();
        }

    }
}
  • 当没有任何线程对数据进行写操作的时候,读线程才有可能获得锁的拥有权,当然除此之外,为了公平起见,如果当前有很多线程正在等待获得写锁的拥有权,同样读线程将会进入Mutex的wait set中, reading Reader的数量将增加。
  • 读线程释放锁, 这意味着reader的数量将减少一个, 同时唤醒wait中的线程, reader唤醒的基本上都是由于获取写锁而进入阻塞的线程,为了提高写锁获得锁的机会,需要将prefer Writer修改为true
3.WriteLock

写锁是Lock的实现, 同样将其设计成包可见以透明其实现细节, 让使用者只用专注于对接口的调用,由于写-写冲突的存在,同一时间只能由一个线程获得锁的拥有权,代码所示。

public class WriteLock implements Lock{

    private final ReadWriteLockImpl readWriteLock;

    public WriteLock(ReadWriteLockImpl readWriteLock) {
        this.readWriteLock = readWriteLock;
    }


    @Override
    public void lock() throws InterruptedException {
        synchronized (readWriteLock.getMUTEX()) {
            try {
                // 首先使等待获取写入锁的数字加一
                readWriteLock.incrementWritingWriters();
                // 如果此时有其他线程正在进行读操作,或者写操作,那么当前线程将被挂起
                while(readWriteLock.getReadingReaders() > 0
                || readWriteLock.getWritingWriters() > 0) {
                    readWriteLock.getMUTEX().wait();
                }
            } finally {
                // 成功获取到了写入锁,使得等待获取写入锁的计数减一
                this.readWriteLock.decrementWaitingWriters();
            }
            // 将正在写入的线程数量加一
            readWriteLock.incrementWritingWriters();
        }
    }

    @Override
    public void unlock() {
        synchronized (readWriteLock.getMUTEX() ) {
            // 减少正在写入锁的线程计数器
            readWriteLock.decrementWritingWriters();
            // 将偏好状态修改为false, 可以使得读锁被最快速的获得
            readWriteLock.changePrefer(false);
            // 通知唤醒其他在Mutex monitor waitset中的线程
            readWriteLock.getMUTEX().notifyAll();
        }
    }
}

  • 当有线程在进行读操作或者写操作的时候,若当前线程试图获得锁,则其将会进入MUTEX的wait set中而阻塞, 同时增加waiting Writer和writing Writer的数量, 但是当线程从wait set中被激活的时候waiting Writer将很快被减少。
  • 写释放锁, 意味着writer的数量减少, 事实上变成了0, 同时唤醒wait中的线程,并将prefer Writer修改为false, 以提高读线程获得锁的机会。

3.读写锁的使用

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

public class ShareData {
    // 定义共享数据(资源)
    private final List<Character> container = new ArrayList<>();

    //构造ReadWriteLock
    private final ReadWriteLock readWriteLock = ReadWriteLock.readWriteLock();

    // 创建读取锁
    private final Lock readLock = readWriteLock.readLock();

    // 创建写入锁
    private final Lock writeLock = readWriteLock.writeLock();
    private final int length;

    public ShareData(int length) {
        this.length = length;
        for(int i = 0; i < length; i++) {
            container.add(i, 'c');
        }
    }

    public char[] read() throws InterruptedException {
        try {
           // 首先使用读锁进行lock
           readLock.lock();
           char[] newBuffer = new char[length];
           for(int i = 0; i < length; i++) {
               newBuffer[i] = container.get(i);
           }
           slowly();
           return newBuffer;
        } finally {
            // 当操作结束之后,将锁释放
            readLock.unlock();
        }
    }

    public void write(char c) throws InterruptedException {
        try {
            //使用写锁进行lock
            writeLock.lock();
            for(int i = 0; i < length; i++ ) {
                this.container.add(i, c);
            }

            slowly();

        }finally {
            // 当所有的操作都完成之后,对写锁进行释放
            writeLock.unlock();
        }
    }

    // 简单模拟操作的耗时
    private void slowly() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ShareData中涉及了对数据的读写操作, 因此它是需要进行线程同步控制的。首先, 创建一个ReadWrite Lock工厂类, 然后用该工厂分别创建ReadLock和WriteLock的实例, 在read方法中使用Read Lock对其进行加锁, 而在write方法中则使用WriteLock, 的程序则是关于对ShareData的使用。

public class ReadWriteLockTest {
     this is the example for read write lock
    private final static String text = "this";

    public static void main(String[] args) {
        // 定义共享数据
        final ShareData shareData = new ShareData(50);
        // 创建两个线程进行数据写操作
        for(int i = 0; i < 2; i++) {
            new Thread(
                    () -> {
                        for(int index = 0; index < text.length(); index++ ) {
                            try {
                                char c= text.charAt(index);
                                shareData.write(c);
                                System.out.println(Thread.currentThread() + " write " + c);
                            }catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
            ).start();

            // 创建10个线程进行数据读操作
            for(int i1 = 0; i1 < 10; i1++ ) {
                new Thread( ()-> {
                    while(true) {
                        try {
                            System.out.println(Thread.currentThread() + " read " + new String(shareData.read()));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }

        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

强化学习 DAY1:什么是 RL、马尔科夫决策、贝尔曼方程

第一部分 RL基础&#xff1a;什么是RL与MRP、MDP 1.1 入门强化学习所需掌握的基本概念 1.1.1 什么是强化学习&#xff1a;依据策略执行动作-感知状态-得到奖励 强化学习里面的概念、公式&#xff0c;相比ML/DL特别多&#xff0c;初学者刚学RL时&#xff0c;很容易被接连不断…

【STM32系列】利用MATLAB配合ARM-DSP库设计FIR数字滤波器(保姆级教程)

ps.源码放在最后面 设计IIR数字滤波器可以看这里&#xff1a;利用MATLAB配合ARM-DSP库设计IIR数字滤波器&#xff08;保姆级教程&#xff09; 前言 本篇文章将介绍如何利用MATLAB与STM32的ARM-DSP库相结合&#xff0c;简明易懂地实现FIR低通滤波器的设计与应用。文章重点不在…

DeepSeek-R1 本地电脑部署 Windows系统 【轻松简易】

本文分享在自己的本地电脑部署 DeepSeek&#xff0c;而且轻松简易&#xff0c;快速上手。 这里借助Ollama工具&#xff0c;在Windows系统中进行大模型部署~ 1、安装Ollama 来到官网地址&#xff1a;Download Ollama on macOS 点击“Download for Windows”下载安装包&#x…

Llama最新开源大模型Llama3.1

Meta公司于2024年7月23日发布了最新的开源大模型Llama 3.1&#xff0c;这是其在大语言模型领域的重要进展。以下是关于Llama 3.1的详细介绍&#xff1a; 参数规模与训练数据 Llama 3.1拥有4050亿&#xff08;405B&#xff09;参数&#xff0c;是目前开源领域中参数规模最大的…

Linux之安装docker

一、检查版本和内核是否合格 Docker支持64位版本的CentOS 7和CentOS 8及更高版本&#xff0c;它要求Linux内核版本不低于3.10。 检查版本 cat /etc/redhat-release检查内核 uname -r二、Docker的安装 1、自动安装 Docker官方和国内daocloud都提供了一键安装的脚本&#x…

2022年全国职业院校技能大赛网络系统管理赛项模块A:网络构建(样题3)-网络部分解析-附详细代码

目录 附录1:拓扑图 附录2:地址规划表 1.SW1 2.SW2 3.SW3 4.SW4 5.SW5 6.SW6 7.SW7 8.R1 9.R2 10.R3 11.AC1 12.AC2 13.AP2 14.AP3 15.EG1 16.EG2 附录1:拓扑图 附录2:地址规划表 设备

Vim跳转文件及文件行结束符EOL

跳转文件 gf 从当前窗口打开那个文件的内容&#xff0c;操作方式&#xff1a;让光标停在文件名上&#xff0c;输入gf。 Ctrlo 从打开的文件返回之前的窗口 Ctrlwf 可以在分割的窗口打开跳转的文件&#xff0c;不过在我的实验不是次次都成功。 统一行尾格式 文本文件里存放的…

《Angular之image loading 404》

前言&#xff1a; 千锤万凿出深山&#xff0c;烈火焚烧若等闲。 正文&#xff1a; 一。问题描述 页面加载图片&#xff0c;报错404 二。问题定位 页面需要加载图片&#xff0c;本地开发写成硬编码的形式请求图片资源&#xff1a; 然而部署到服务器上报错404 三。解决方案 正确…

Windows Docker笔记-Docker容器操作

在文章《Windows Docker笔记-Docker拉取镜像》中&#xff0c;已经拉取成功了ubuntu镜像&#xff0c;本章来讲解如何通过镜像来创建容器并运行容器。 这里再类比一下&#xff0c;加深理解&#xff0c;比如&#xff0c;我们现在想开一个玩具厂&#xff0c;我们的最终目的肯定是想…

upload-labs安装与配置

前言 作者进行upload-labs靶场练习时&#xff0c;在环境上出了很多问题&#xff0c;吃了很多苦头&#xff0c;甚至改了很多配置也没有成功。 upload-labs很多操作都是旧时代的产物了&#xff0c;配置普遍都比较老&#xff0c;比如PHP版本用5.2.17&#xff08;还有中间件等&am…

(2025|ICLR,音频 LLM,蒸馏/ALLD,跨模态学习,语音质量评估,MOS)音频 LLM 可作为描述性语音质量评估器

Audio Large Language Models Can Be Descriptive Speech Quality Evaluators 目录 1. 概述 2. 研究背景与动机 3. 方法 3.1 语音质量评估数据集 3.2 ALLD 对齐策略 4. 实验结果分析 4.1 MOS 评分预测&#xff08;数值评估&#xff09; 4.2 迁移能力&#xff08;在不同…

深入理解linux中的文件(下)

目录 一、语言级缓冲区和内核级缓冲区 二、C语音中的FILE* fp fopen(“./file.txt”,"w"): 四、理解磁盘结构&#xff1a; 物理结构 逻辑结构 五、未被打开的文件&#xff1a; 六、更加深入理解inode编号怎么找到文件&#xff1a; 七、对路径结构进行…

零基础Vue入门6——Vue router

本节重点&#xff1a; 路由定义路由跳转 前面几节学习的都是单页面的功能&#xff08;都在专栏里面https://blog.csdn.net/zhanggongzichu/category_12883540.html&#xff09;&#xff0c;涉及到项目研发都是有很多页面的&#xff0c;这里就需要用到路由&#xff08;vue route…

京准:NTP卫星时钟服务器对于DeepSeek安全的重要性

京准&#xff1a;NTP卫星时钟服务器对于DeepSeek安全的重要性 京准&#xff1a;NTP卫星时钟服务器对于DeepSeek安全的重要性 在网络安全领域&#xff0c;分布式拒绝服务&#xff08;DDoS&#xff09;攻击一直是企业和网络服务商面临的重大威胁之一。随着攻击技术的不断演化…

网络计算机的五个组成部分

单个计算机是无法进行通信的。所以需要借助网络。 下面介绍一些在网络里常见的设备。 一、服务器 服务器是在网络环境中提供计算能力并运行软件应用程序的特定IT设备 它在网络中为其他客户机&#xff08;如个人计算机、智能手机、ATM机等终端设备&#xff09;提供计算或者应用…

MATLAB实现单层竞争神经网络数据分类

一.单层竞争神经网络介绍 单层竞争神经网络&#xff08;Single-Layer Competitive Neural Network&#xff09;是一种基于竞争学习的神经网络模型&#xff0c;主要用于数据分类和模式识别。其核心思想是通过神经元之间的竞争机制&#xff0c;使得网络能够自动学习输入数据的特…

【漫画机器学习】082.岭回归(或脊回归)中的α值(alpha in ridge regression)

岭回归&#xff08;Ridge Regression&#xff09;中的 α 值 岭回归&#xff08;Ridge Regression&#xff09;是一种 带有 L2​ 正则化 的线性回归方法&#xff0c;用于处理多重共线性&#xff08;Multicollinearity&#xff09;问题&#xff0c;提高模型的泛化能力。其中&am…

网络安全 | 零信任架构:重构安全防线的未来趋势

网络安全 | 零信任架构&#xff1a;重构安全防线的未来趋势 一、前言二、零信任架构的核心概念与原理2.1 核心概念2.2 原理 三、零信任架构的关键技术组件3.1 身份管理与认证系统3.2 授权与访问控制系统3.3 网络与安全监测系统3.4 加密与数据保护技术 四、零信任架构与传统安全…

网络爬虫学习:借助DeepSeek完善爬虫软件,增加停止任务功能

一、引言 我从24年11月份开始学习网络爬虫应用开发&#xff0c;经过2个来月的努力&#xff0c;终于完成了开发一款网络爬虫软件的学习目标。这几天对本次学习及应用开发进行一下回顾总结。前面已经发布了两篇日志&#xff1a; 网络爬虫学习&#xff1a;应用selenium从搜*狐搜…

JVM图文入门

往期推荐 【已解决】redisCache注解失效&#xff0c;没写cacheConfig_com.howbuy.cachemanagement.client.redisclient#incr-CSDN博客 【已解决】OSS配置问题_keyuewenhua.oss-cn-beijing.aliyuncs-CSDN博客 【排坑】云服务器docker部署前后端分离项目域名解析OSS-CSDN博客 微服…