JUC高并发编程8:读写锁

news2024/12/27 12:30:28

1 概述

1.1 悲观锁和乐观锁

在并发编程中,锁机制是用来控制多个线程对共享资源的访问。悲观锁和乐观锁是两种不同的并发控制策略。

1.1.1 悲观锁(Pessimistic Locking)

悲观锁假设在最坏的情况下,多个线程会同时访问和修改共享资源,因此在访问共享资源之前,会先对资源进行加锁,以确保同一时间只有一个线程可以访问该资源。常见的悲观锁实现包括数据库中的表锁和行锁。

  • 优点:可以有效避免数据冲突,适用于写操作较多的场景。
  • 缺点:加锁和解锁操作会增加系统开销,可能导致性能下降。

1.1.2 乐观锁(Optimistic Locking)

乐观锁假设在大多数情况下,多个线程不会同时访问和修改共享资源,因此在访问共享资源时不会立即加锁,而是在提交修改时检查资源是否被其他线程修改过。如果资源被修改过,则回滚操作或重试。常见的乐观锁实现包括版本号机制和时间戳机制。

  • 优点:减少了加锁和解锁的开销,适用于读操作较多的场景。
  • 缺点:在并发冲突较多的情况下,可能会导致频繁的重试操作,影响性能。

1.2 表锁和行锁

在数据库中,锁机制用于控制对表或行的访问。

1.2.1 表锁(Table Lock)

表锁是对整个表进行加锁,当一个事务对表进行加锁后,其他事务无法对该表进行写操作,但可以进行读操作(取决于锁的类型)。

  • 优点:实现简单,不会发生死锁。
  • 缺点:锁的粒度较大,可能会影响并发性能。

1.2.2 行锁(Row Lock)

行锁是对表中的某一行进行加锁,当一个事务对某一行进行加锁后,其他事务无法对该行进行写操作,但可以对其他行进行操作。

  • 优点:锁的粒度较小,可以提高并发性能。
  • 缺点:实现复杂,可能会发生死锁。

1.3 读锁和写锁

在数据库中,锁可以分为读锁和写锁。

1.3.1 读锁(Read Lock)

读锁是一种共享锁(Shared Lock),多个事务可以同时持有读锁,但持有读锁的事务不能进行写操作。读锁不会阻塞其他读锁,但会阻塞写锁。

  • 优点:允许多个事务同时读取数据,提高了并发性能。
  • 缺点:可能会发生死锁,特别是在多个事务同时持有读锁并等待写锁时。

1.3.2 写锁(Write Lock)

写锁是一种独占锁(Exclusive Lock),当一个事务持有写锁时,其他事务不能持有任何锁(包括读锁和写锁)。写锁会阻塞其他所有锁。

  • 优点:可以确保数据的一致性,避免数据冲突。
  • 缺点:可能会发生死锁,特别是在多个事务同时持有写锁并等待其他锁时。

1.4 死锁

死锁是指两个或多个事务在等待对方释放锁的情况下,都无法继续执行的状态。死锁通常发生在以下情况:

  • 读锁和写锁的相互等待:例如,事务1持有读锁并等待事务2释放写锁,而事务2持有写锁并等待事务1释放读锁。
  • 行锁的相互等待:例如,事务1锁定了某一行并等待事务2释放另一行的锁,而事务2锁定了另一行并等待事务1释放第一行的锁。

1.5 小结

  • 悲观锁:适用于写操作较多的场景,通过加锁避免数据冲突,但会增加系统开销。
  • 乐观锁:适用于读操作较多的场景,通过版本号或时间戳机制避免数据冲突,减少加锁开销,但在并发冲突较多时可能导致频繁重试。
  • 表锁:对整个表加锁,实现简单,不会发生死锁,但锁粒度大,影响并发性能。
  • 行锁:对表中的某一行加锁,锁粒度小,提高并发性能,但可能发生死锁。
  • 读锁:允许多个事务同时读取数据,提高并发性能,但可能发生死锁。
  • 写锁:确保数据一致性,避免数据冲突,但可能发生死锁。

2 案例实现

场景: 使用 ReentrantReadWriteLock 对一个 hashmap 进行读和写操作

  • 代码
//资源类
class MyCache{

    // 创建map集合
    private volatile Map<String,Object> map = new HashMap<>();

    // 创建读写锁对象
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 放数据
    public void put(String key,Object value){
        // 添加写锁
        rwLock.writeLock().lock();




        try {
            System.out.println(Thread.currentThread().getName() +" 正在写操作"+key);
            // 暂停一会儿
            TimeUnit.MICROSECONDS.sleep(300);

            // 放数据
            map.put(key,value);

            System.out.println(Thread.currentThread().getName()+" 写完了" + key);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放写锁
            rwLock.writeLock().unlock();
        }


    }

    // 取数据
    public Object get(String key){
        // 添加读锁
        rwLock.readLock().lock();
        Object result = null;

        try {

            System.out.println(Thread.currentThread().getName() + " 正在读取操作" + key);
            // 暂停一会儿
            TimeUnit.MICROSECONDS.sleep(300);

            result = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 取完了" + key);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放读锁
            rwLock.readLock().unlock();
        }
        return result;

    }
}
public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();
        // 创建线程放数据
        for (int i = 0; i <= 5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.put(num+"",num+"");

            },String.valueOf(i)).start();
        }

        TimeUnit.MICROSECONDS.sleep(300);

        // 创建线程取数据
        for (int i = 0; i <= 5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.get(num+"");

            },String.valueOf(i)).start();
        }
    }
}

3 读写锁的演变

3.1 读写锁(Read-Write Lock)

读写锁是一种特殊的锁机制,它允许多个读线程同时访问共享资源,但只允许一个写线程访问共享资源。读写锁的主要目的是提高并发性能,特别是在读操作远多于写操作的场景中。

3.1.1 读写锁的特点

  • 读读共享:多个读线程可以同时持有读锁,读操作之间不会互斥。
  • 读写互斥:当一个写线程持有写锁时,其他读线程和写线程都不能访问共享资源。
  • 写写互斥:当一个写线程持有写锁时,其他写线程不能访问共享资源。

3.2 读写锁的演变

3.2.1 无锁

在无锁的情况下,多个线程可以同时访问共享资源,这会导致资源竞争和数据不一致的问题。

public class NoLockExample {
    private int resource = 0;

    public void read() {
        System.out.println("Read: " + resource);
    }

    public void write(int value) {
        resource = value;
        System.out.println("Write: " + resource);
    }
}

3.2.2 添加锁

使用 synchronizedReentrantLock 可以解决资源竞争的问题,但这些锁是独占的,每次只能有一个线程操作资源,即使是读操作也不能共享。

public class SynchronizedLockExample {
    private int resource = 0;

    public synchronized void read() {
        System.out.println("Read: " + resource);
    }

    public synchronized void write(int value) {
        resource = value;
        System.out.println("Write: " + resource);
    }
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private int resource = 0;
    private Lock lock = new ReentrantLock();

    public void read() {
        lock.lock();
        try {
            System.out.println("Read: " + resource);
        } finally {
            lock.unlock();
        }
    }

    public void write(int value) {
        lock.lock();
        try {
            resource = value;
            System.out.println("Write: " + resource);
        } finally {
            lock.unlock();
        }
    }
}

3.2.3 读写锁

使用 ReentrantReadWriteLock 可以实现读读共享,提高并发性能。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private int resource = 0;
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void read() {
        rwLock.readLock().lock();
        try {
            System.out.println("Read: " + resource);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void write(int value) {
        rwLock.writeLock().lock();
        try {
            resource = value;
            System.out.println("Write: " + resource);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}
3.2.3.1 读写锁的缺点
  • 锁饥饿:如果读操作非常频繁,写操作可能会长时间等待,导致写线程饥饿。例如,在地铁系统中,如果一直有人在读取信息(如查看列车时刻表),而没有人进行写操作(如更新列车状态),写操作可能会被长时间阻塞。
  • 读写互斥:读操作在进行时,写操作不能进行,只有读操作完成后,写操作才能进行。这可能会导致写操作的延迟。
  • 写读互斥:写操作在进行时,读操作不能进行,只有写操作完成后,读操作才能进行。这可能会导致读操作的延迟。

4 读写锁的降级

读写锁的降级是指在持有写锁的情况下,获取读锁,然后释放写锁,最后释放读锁的过程。这种操作通常用于在写操作完成后,需要进行读操作的场景。通过锁降级,可以在保证数据一致性的前提下,提高并发性能。

4.1 JDK 8 中的锁降级说明

在 JDK 8 中,读写锁的降级操作可以按照以下步骤进行:

  1. 获取写锁:首先获取写锁,确保当前线程对共享资源有独占访问权限。
  2. 获取读锁:在持有写锁的情况下,获取读锁。
  3. 释放写锁:释放写锁,此时其他线程可以获取写锁,但当前线程仍然持有读锁。
  4. 释放读锁:最后释放读锁,完成整个锁降级过程。

4.2 示例代码

以下是一个使用 ReentrantReadWriteLock 进行锁降级的示例代码:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDowngradeExample {
    private int resource = 0;
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void downgradeLock() {
        rwLock.writeLock().lock(); // 获取写锁
        try {
            // 写操作
            resource = 100;
            System.out.println("Write: " + resource);

            rwLock.readLock().lock(); // 获取读锁
        } finally {
            rwLock.writeLock().unlock(); // 释放写锁
        }

        try {
            // 读操作
            System.out.println("Read: " + resource);
        } finally {
            rwLock.readLock().unlock(); // 释放读锁
        }
    }

    public static void main(String[] args) {
        ReadWriteLockDowngradeExample example = new ReadWriteLockDowngradeExample();
        example.downgradeLock();
    }
}

4.3 读锁不能升级为写锁

需要注意的是,读锁不能直接升级为写锁。如果一个线程持有读锁,并尝试获取写锁,会导致死锁。因为读锁是共享的,多个线程可以同时持有读锁,而写锁是独占的,只能有一个线程持有写锁。如果一个线程持有读锁并尝试获取写锁,而其他线程也持有读锁,就会导致所有线程都在等待对方释放读锁,从而形成死锁。

4.4 小结

  • 锁降级:在持有写锁的情况下,获取读锁,然后释放写锁,最后释放读锁。这种操作可以提高并发性能,特别是在写操作完成后需要进行读操作的场景。
  • 读锁不能升级为写锁:读锁是共享的,写锁是独占的,读锁不能直接升级为写锁,否则会导致死锁。

5 思维导图

在这里插入图片描述

6 参考链接

【【尚硅谷】大厂必备技术之JUC并发编程】

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

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

相关文章

Qt-QDockWidget浮动窗口相关操作(49)

目录 描述 使用 描述 在 Qt 中&#xff0c;浮动窗⼝也称之为铆接部件。浮动窗⼝是通过 QDockWidget类 来实现浮动的功能。浮动窗口⼀般是位于核心部件的周围&#xff0c;可以有多个。 使用 创建我们可以参考下面的语法格式 使用起来也很简单&#xff0c;不过只能创建一个 Q…

【C语言】使用结构体实现位段

文章目录 一、什么是位段二、位段的内存分配1.位段内存分配规则练习1练习2 三、位段的跨平台问题四、位段的应用五、位段使用的注意事项 一、什么是位段 在上一节中我们讲解了结构体&#xff0c;而位段的声明和结构是类似的&#xff0c;它们有两个不同之处&#xff0c;如下&…

创建osd加入集群

故障原因&#xff1a;ceph节点一个磁盘损坏&#xff0c;其中osd69 down了&#xff0c;需要更换磁盘并重新创建osd加入ceph集群。 信息采集&#xff1a; 更换磁盘前&#xff0c;查询osd69对应的盘符&#xff1a; 将对应的故障磁盘更换后&#xff0c;并重做raid&#xff0c;然后查…

SDK4(note下)

以下代码涉及到了很多消息的处理&#xff0c;有些部分注释掉了&#xff0c;主要看代码 #include <windows.h> #include<tchar.h> #include <stdio.h> #include <strsafe.h> #include <string> #define IDM_OPEN 102 /*鼠标消息 * 键盘消息 * On…

76.【C语言】perror函数介绍

1.cplusplus的官网介绍 cplusplus的介绍 点我跳转 2.翻译 函数 perror void perror ( const char * str ); 打印错误信息 将errno(最后一个错误数字)的值解释为错误信息,之后把它打印到stderr中(标准错误输出流,通常是控制台)(备注有关"流"的概念在75.【C语言】文件…

k8s-pod的管理及优化设置

Pod是Kubernetes&#xff08;k8s&#xff09;中最小的资源管理组件&#xff0c;也是最小化运行容器化应用的资源对象。以下是对Pod的详细介绍&#xff1a; 一、Pod的基本概念 定义&#xff1a;Pod是Kubernetes中可以创建和管理的最小单元&#xff0c;是资源对象模型中由用户创…

网站排名,让网站快速有排名的几个方法

要让网站快速获得并提升排名&#xff0c;需要综合运用一系列专业策略和技术&#xff0c;这些策略涵盖了内容优化、技术调整、外链建设、用户体验提升等多个方面。以下是让网站快速有排名的几个方法&#xff1a; 1.内容为王&#xff1a;创造高质量、有价值的内容 -深入…

geolocator插件的用法

文章目录 1. 概念介绍2. 使用方法3. 示例代码4 体验分享我们在上一章回中介绍了如何实现滑动菜单相关的内容,本章回中将介绍如何获取位置信息.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在这里说的获取位置信息本质上是获取当前手机所在位置的gps坐标,就是我们…

【Chrome浏览器插件--资源嗅探猫抓】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、资源嗅探插件---猫抓二、使用步骤总结 一、资源嗅探插件—猫抓 猫抓是一个浏览器插件&#xff0c;可以检测当前网页中的一些资源文件&#xff0c;可设置嗅探的…

NVIDIA机密计算文档

NVIDIA 可信计算解决方案 文章目录 前言一、指南1. Intel TDX - 机密计算部署指南2. AMD SNP - 机密计算部署指南3. NVIDIA Hopper 机密计算证明文档4. nvtrust GitHub二、发行说明1. 550TRD3 - NVIDIA 可信计算解决方案发行说明2. 550TRD1 - NVIDIA 可信计算解决方案发行说明三…

The Android SDK location cannot be at the filesystem root

win11&#xff0c; 安装启动完Android Studio后&#xff0c;一直显示 The Android SDK location cannot be at the filesystem root因此需要下载SDK包&#xff0c;必须开启代理。 开启代理后&#xff0c;在System下开启自动检测代理&#xff0c;如图 重启Android Studio&a…

任务【浦语提示词工程实践】

0.1 环境配置 首先点击左上角图标&#xff0c;打开Terminal&#xff0c;运行如下脚本创建虚拟环境&#xff1a; # 创建虚拟环境 conda create -n langgpt python3.10 -y 运行下面的命令&#xff0c;激活虚拟环境&#xff1a; conda activate langgpt 之后的操作都要在这个环境…

基于LORA的一主多从监测系统_0.96OLED

关联&#xff1a;0.96OLED hal硬件I2C LORA 在本项目中每个节点都使用oled来显示采集到的数据以及节点状态&#xff0c;OLED使用I2C接口与STM32连接&#xff0c;这个屏幕内部驱动IC为SSD1306&#xff0c;SSD1306作为从机地址为0x78 发送数据&#xff1a;起始…

Windows环境安装CentOS7

【注意】安装CentOS需要先安装Vmware虚拟机 【下载前准备】 一、下载CentOS 7镜像文件阿里云镜像开源&#xff0c;点击跳转 二、安装VMware&#xff08;17&#xff09;&#xff1a; a. 官网&#xff0c;点击跳转 b. 许可证&#xff1a;JU090-6039P-08409-8J0QH-2YR7F 安装V…

Aegisub字幕自动化及函数篇(图文教程附有gif动图展示)(二)

目录 template行 template pre-line template line template syl template syl noblank template char template notext template pre-line notext template syl noblank notext template keeptags ​编辑 template loop number 内联变量 ​编辑 remeber函数 re…

提示工程、微调和 RAG

自众多大型语言模型&#xff08;LLM&#xff09;和高级对话模型发布以来&#xff0c;人们已经运用了各种技术来从这些 AI 系统中提取所需的输出。其中一些方法会改变模型的行为来更好地贴近我们的期望&#xff0c;而另一些方法则侧重于增强我们查询 LLM 的方式&#xff0c;以提…

【华为OD机试真题】95、最少面试官数

package mainimport ("fmt""sort" )type s struct {start intend intworkCount int }type duration struct {start intend int }// 查询时间段内是否有可用的面试官 func getFreeS(sList []*s, d *duration, workCountLimit int) (sIndex int)…

CanOpen转Profinet网关与钢成型机等机械集成时发挥的作用

在现代工业自动化领域&#xff0c;不同设备和系统之间的通信至关重要。CanOpen和Profinet是两种广泛应用于工业控制系统的通讯协议。CanOpen通常用于设备级别的通信&#xff0c;而Profinet则更常见于工业以太网&#xff0c;适用于更大范围的系统级控制。当型钢成型机等复杂机械…

@Service代替@Controller注解来标注到控制层的场景?

在SpringBoot开发中&#xff0c;Controller和Service基本上是日常开发中使用的最频繁的两个注解。但你有没考虑过Service代替Controller注解来标注到控制层的场景&#xff1f;换言之&#xff0c;经过Service标注的控制层能否实现将用户请求分发到服务层的功能&#xff1f; 前言…

视频智能分析/AI智能分析网关V4客流统计算法介绍及其在多领域多场景中的应用

随着人工智能技术的快速发展&#xff0c;AI智能分析网关V4作为一种集高性能、低功耗于一体的软硬一体AI边缘计算硬件设备&#xff0c;在工地、工厂、园区、消防、社区、校园等领域展现出强大的应用潜力。本文将详细介绍AI智能分析网关V4的客流统计算法原理及其在多个场景中的应…