Java 入门指南:Java 并发编程 —— ReentrantLock 实现悲观锁(Pessimistic Locking)

news2024/11/13 20:34:59

悲观锁

悲观锁(Pessimistic Locking)是一种悲观的并发控制机制,它基于悲观的假设,即并发冲突会时常发生,因此在访问共享资源(如数据库记录或共享变量)之前,会先获取独占性的锁,以防止其他线程对资源的并发读写。

悲观锁适用于写操作频繁、读操作较少的场景,能够确保数据一致性,但会引入较大的性能开销和线程切换的开销。

实现方式

在 Java 中,可以使用以下方式实现悲观锁:

  1. synchronized 关键字:使用 synchronized 关键字可以实现对共享资源的悲观锁。通过在方法或代码块中加上 synchronized 关键字,只允许一个线程进入同步区域,并对共享资源进行操作。其他线程需要等待当前线程释放锁才能进入同步区域。
synchronized (sharedObject) {
    // 进入同步区域,操作共享资源
}
  1. ReentrantLock 类:ReentrantLockJava.util.concurrent 包提供的可重入锁实现。相较于 Synchronized,ReentrantLock 提供了更精细的锁控制,包括手动获取锁、手动释放锁、可重入性等特性。

注意事项

  • 悲观锁的使用需要考虑锁的粒度,过大的锁粒度可能会影响并发性能,过小的锁粒度可能会导致频繁的锁竞争。

  • 使用悲观锁时,应确保获取锁和释放锁的操作是成对出现的,否则可能会导致死锁或资源泄漏等问题。

  • 需要谨慎处理异常情况,确保在异常发生时能够正确释放锁,避免其他线程被阻塞。

ReentrantLock

在Java并发编程中,ReentrantLock 是一个非常强大且灵活的锁机制,ReentrantLock(重入锁)是实现了 Lock接口 的一个,也是在实际编程中使用频率很高的一个锁,相比于 synchronized 关键字提供了更多的功能和灵活性。

如尝试非阻塞地获取锁、可中断地获取锁、以及锁的超时获取等。支持重入性,能够对共享资源重复加锁,即当前线程获取该锁后再次获取不会被阻塞。这些特性使得 ReentrantLock 成为实现悲观锁(Pessimistic Locking)的理想选择。

ReentrantLock 继承了 AQSAbstractQueuedSynchronizer 的缩写,即 抽象队列同步器,是 Java.util.concurrent 中的一个基础工具类),内部有一个抽象类 Sync,实现了一个同步器。

使用 ReentrantLock 需要进行显式地加锁和释放锁操作,如下所示:

ReentrantLock lock = new ReentrantLock();

lock.lock(); // 加锁
try {
    // 临界区代码
} finally {
    lock.unlock(); // 释放锁
}

特性

ReentrantLock 提供了与 synchronized 类似的互斥访问资源的能力,但它还提供了一些额外的特性:

  1. 可重入性:同一个线程可以多次获取同一个 ReentrantLock 锁,而不会产生死锁。这使得在一个方法中调用另一个需要同步访问的方法成为可能。

    在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;

    由于锁会被获取 n 次,那么只有锁在被释放同样的 n 次之后,该锁才算是完全释放成功。

  2. 可中断性:ReentrantLock 提供了可中断的锁获取方式,即可以在等待锁时响应中断。

  3. 公平性:ReentrantLock 可以选择公平性非公平性的获取锁方式。

    • 在公平模式下,等待时间较长的线程更容易获得锁

    • 在非公平模式下,则不保证线程获取锁的顺序。

  4. 条件变量支持:ReentrantLock 通常与 Condition 配合使用,提供了与条件变量相关联的 Condition 对象,能够更灵活地实现精确的线程等待和唤醒操作。可以方便地实现多路选择通知,更加精确的线程等待和通知机制。

synchronized 只能通过 waitnotify/notifyAll 方法唤醒一个线程或者唤醒全部线程(单路通知

公平锁和非公平锁

公平锁(Fair Lock)和非公平锁(Unfair Lock)是针对锁的获取顺序而言的。

ReentrantLock 内部有两个非抽象类 NonfairSyncFairSync,即 非公平同步器 和 公平同步器,它们都继承了 Sync 类,都调用了 AOS(AbstractOwnableSynchronizer,这个类于 JDK 1.6 引入。用于表示锁与持有者之间的关系(独占模式)) 的 setExclusiveOwnerThread 方法,即 公平锁和非公平锁都是独占锁

公平锁

公平锁(Fair Lock):当锁处于可用状态时,锁会先分配给等待时间最长的线程,也就是先排队的线程。

这样能够保证线程获取锁的顺序与线程启动的顺序一致,避免了等待时间过长的情况,确保较低优先级的线程也有机会获取到锁。在公平锁的情况下,锁的获取顺序是按照线程请求锁的顺序(FIFO)来进行排序的

公平锁比非公平锁的性能更差一些,因为需要维护队列,而队列的操作是会对性能产生影响的。此外,使用公平锁时还可能出现 活锁 现象,即一个线程不断尝试获取锁,但总是失败的情况。

如果希望保证响应时间足够短且资源利用率不低,可以使用公平锁。

非公平锁

非公平锁(Unfair Lock):当锁处于可用状态时,锁会立即分配给一个准备好的线程,而不考虑其他等待获取锁的线程。在非公平锁的情况下,获取锁的线程是随机选择的,不具有先来先服务的特点。

在一般情况下,使用非公平锁的性能会更好,因为非公平锁减少了线程上下文的切换,从而提高了并发性。

创建 ReentrantLock

  1. ReentrantLock 的无参构造方法是构造非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
  1. ReentrantLock 的有参构造方法可传入一个 boolean 值,true 时为公平锁,false 时为非公平锁:
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平锁的实现方式与非公平锁的实现方式基本一致,只是在获取锁时增加了判断当前节点是否有前驱节点的逻辑判断

使用 ReentrantLock 时,锁必须在 try 代码块开始之前获取,并且加锁之前不能有异常抛出,否则在 finally 块中就无法释放锁( ReentrantLock 的锁必须在 finally 中手动释放)

ReentrantLock lock = new ReentrantLock();
// ...
lock.lock();
try {
    doSomething();
    doOthers();
} finally {
    lock.unlock();
}

ReentrantLock 使用示例

下面是一个使用 ReentrantLock 实现悲观锁的简单示例:

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class Counter {  
    // 使用 ReentrantLock 作为悲观锁  
    private final Lock lock = new ReentrantLock();  
    private int count = 0;  
  
    // 线程安全地增加计数  
    public void increment() {  
        lock.lock(); // 尝试获取锁  
        try {  
            count++; // 访问共享资源  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    // 获取当前计数  
    public int getCount() {  
        lock.lock(); // 尝试获取锁  
        try {  
            return count; // 访问共享资源  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        Counter counter = new Counter();  
  
        // 假设有多个线程同时调用 increment() 方法  
        // 这里为了演示,我们只用一个线程来模拟  
        for (int i = 0; i < 1000; i++) {  
            counter.increment();  
        }  
  
        System.out.println("Final count: " + counter.getCount());  
    }  
}

在这个示例中,Counter 类中的 increment()getCount() 方法都使用了 ReentrantLock 来确保线程安全。尽管在这个简单的例子中我们只使用了单个线程来调用 increment() 方法,但在多线程环境下,ReentrantLock 会确保 count 变量的增加操作是线程安全的。

ReentrantReadWriteLock

在并发场景中,为了解决线程安全问题,我们通常会使用关键字 sychronized 或者 JUC 包(Java Util Concurrent Java 并发工具包)中实现了 Lock 接口的 ReentrantLockopen。但它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。

而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性,而如果在这种业务场景下,依然使用独占锁的话,很显然会出现性能瓶颈。针对这种读多写少的情况,Java 提供了另外一个实现 Lock 接口的 ReentrantReadWriteLock

ReentrantReadWriteLock 读写锁,它是对传统的互斥锁(如 ReentrantLock)的扩展,可以允许多个线程同时读取共享资源,而对写操作进行互斥,提供了读写分离的机制。

特性

ReentrantReadWriteLock 具有以下特点:

  1. 读锁共享性:多个线程可以同时获取读锁,读取共享资源,而不会互斥。这使得多个线程可以同时读取数据,提高了并发性能。

  2. 写锁独占性:一旦线程获取了写锁,其他线程无法获取读锁或写锁。这样可以确保只有一个线程进行写操作,保持数据的一致性。

  3. 可重入性:和 ReentrantLock 一样,ReentrantReadWriteLock 支持重入,同一个线程可以多次获取读锁或写锁。

  4. 锁降级:读写锁支持锁降级,即写锁降级,是一种允许写锁转换为读锁的过程。不支持锁升级

由于读锁是共享的,所以当存在读锁时,写操作会被阻塞。这使得写操作的优先级较高,可以防止写操作长时间被读操作阻塞。

ReentrantReadWriteLock 管理读锁和写锁的机制使得读写操作可以并发进行,读锁和写锁是分离的,实现了读写、写读、写写的过程互斥,从而提高了并发性能。

适用于读操作远远多于写操作的场景,允许多线程同时读取共享资源,避免了读-读之间的互斥。

但在读操作和写操作的频率相差不大,或者读操作频率较高的情况下,仍然可能导致写操作长时间被延迟,影响系统的响应性能。

写锁降级

ReentrantReadWriteLock 的内部实现使用了 写锁降级 的机制,即一个线程在持有写锁的同时可以获取读锁,并逐步释放写锁,从而实现了锁的降级。

写锁降级是一种允许写锁转换为读锁的过程。通常的顺序是:

  1. 获取写锁:线程首先获取写锁,确保在修改数据时排它访问。

  2. 获取读锁:在写锁保持的同时,线程可以再次获取读锁。

  3. 释放写锁:线程保持读锁的同时释放写锁。

  4. 释放读锁:最后线程释放读锁。

这样,写锁就降级为读锁,允许其他线程进行并发读取操作,但仍然排除其他线程的写操作。

![[processCachedData().png]]

  1. 获取读锁:首先尝试获取读锁来检查某个缓存是否有效。

  2. 检查缓存:如果缓存无效,则需要释放读锁,因为在获取写锁之前必须释放读锁。

  3. 获取写锁:获取写锁以便更新缓存。此时,可能还需要重新检查缓存状态,因为在释放读锁和获取写锁之间可能有其他线程修改了状态。

  4. 更新缓存:如果确认缓存无效,更新缓存并将其标记为有效。

  5. 写锁降级为读锁:在释放写锁之前,获取读锁,从而实现写锁到读锁的降级。这样,在释放写锁后,其他线程可以并发读取,但不能写入。

  6. 使用数据:现在可以安全地使用缓存数据了。

  7. 释放读锁:完成操作后释放读锁。

读写状态的记录

AQS 内部的 state 字段(int 类型,32 位),用于描述有多少线程持有锁。

  • 同步状态的低 16 位用来表示写锁的获取次数

  • 同步状态的高 16 位用来表示读锁被获取的次数

如果是重入锁的话 state 值就是重入的次数

![[record of sharedCount and exclusiveCount.png]]

读锁和写锁

ReentrantReadWriteLock 内部维护了两把锁,分别为 读锁 ReadLock写锁 WriteLock

![[ReentrantReadWriteLock Variable.png]]

ReadLockWriteLock 是靠 AQSAbstractQueuedSynchronizer 的缩写,即 抽象队列同步器,是 Java.util.concurrent 中的一个基础工具类)的子类 Sync 实现的锁

写锁
写锁的获取

ReentrantReadWriteLock 的写锁是排他锁(独享锁),而实现写锁的同步语义是通过重写 AQS 中的 tryAcquire 方法实现的:

首先获取写锁当前的同步状态,当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取写锁成功并支持重入,增加写状态 exclusiveCount

tryAcquire() 除了重入条件(当前线程为获取写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取

原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

因此只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

写锁的释放

写锁释放通过重写 AQStryRelease 方法实现,与 ReentrantLock 的释放基本一致:

![[Release WirteLock Source Code.png]]

  1. 判断当前线程是否持有写锁,若未持有则抛出 IllegalMonitorStateException 异常。

  2. 将写状态变量减去对应的计数值 releases
    int nextc = getState() - releases; 因为写状态是由同步状态的低 16 位表示的,只需要用当前同步状态直接减去写状态

  3. 如果计数器为 0,则唤醒一个等待写锁的线程

  4. 如果计数器不为 0,则需要唤醒所有等待的线程

读锁
读锁的获取

读锁是一种共享式锁,同一时刻该锁可以被多个读线程获取。实现共享式同步组件的同步语义需要通过重写 AQSAbstractQueuedSynchronizer 的缩写,即 抽象队列同步器,是 Java.util.concurrent 中的一个基础工具类) 的 tryAcquireShared 方法和 tryReleaseShared 方法

首先获取写锁的同步状态,如果写锁已经其他被获取,获取读锁失败,进入等待状态,如果当前线程获取了写锁或者写锁未被获取,当前线程增加读状态,获取读锁成功,利用 CAS 更新同步状态。

如果 CAS 失败或者已经获取读锁的线程再次获取读锁时,是通过 fullTryAcquireShared 方法实现的

读锁的释放

读锁释放的实现主要通过重写 AQS 的方法 tryReleaseShared 实现

  1. 判断当前线程是否持有读锁,若未持有则抛出 IllegalMonitorStateException 异常。

  2. 将当前线程的读锁计数器减 1。

  3. 如果当前线程的读锁计数器为 0,从读锁的等待队列中唤醒一个线程。

  4. 如果当前线程的读锁计数器还不为 0,则说明当前线程还持有至少一个读锁,不需要释放锁。

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

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

相关文章

内存管理篇-17解开页表的神秘面纱-下

1.页表初探遗留问题-页表的创建过程 使用MMU之前&#xff0c;页表要准备好&#xff0c;怎么准备的&#xff1f;如何把物理内存通过section映射构建页表页表的创建过程分析&#xff1a;__create_page_tables--创建临时页表&#xff0c;然后在开启MMU 页表的大小和用途页表在内存…

您应该使用哪个矢量数据库? 选择最适合您需求的数据库

导言 矢量数据库已成为存储非结构化和结构化数据表示并编制索引的首选。 这些表征被称为向量嵌入&#xff0c;由嵌入模型生成。 向量存储在利用深度学习模型&#xff08;尤其是大型语言模型&#xff09;的应用开发中发挥着至关重要的作用。 什么是矢量数据库&#xff1f; 在…

中国文化艺术孙溟展浅析《绛帖》

孙溟展浅析《绛帖》 《绛帖》是汇集摹刻众多名帖。北宋时潘师旦共摹刻二十卷&#xff0c;以《淳化阁帖》为基础有增加和删减&#xff0c;因为是在降州《今山西新降》摹刻的故此而得名。 《绛帖》 《绛帖》 《绛帖》 《绛帖》 据传潘师旦去世后&#xff0c;他的两个儿子各…

双硬盘双系统怎么装?双硬盘双系统安装教程(win7+win10双系统)

最近有很网友想双硬盘装双系统&#xff0c;考虑到如果哪块硬盘坏了我另一块硬盘还可以启动&#xff0c;以防万一硬盘坏了一块无法使用问题&#xff0c;今天小编给大家带来的双硬盘双系统方法教程。 相关推荐&#xff1a; 如何安装双系统?安装双系统详细步骤 双系统怎么设置启动…

个人旅游网(2.1)——使用阿里云在springboot项目中发送短信

文章目录 一、背景介绍二、详细步骤2.1、申请资质2.2、申请签名2.3、申请模板2.4、申请accessKey秘钥对2.5、SDK的使用[!]2.5.1、项目中导入依赖2.5.2、发短信的工具类 一、背景介绍 验证码发送背后的功能原理图&#xff1a; 想要在项目中实现上述发送验证码的功能&#xff0c…

操作系统面试真题总结(三)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 什么是进程&#xff1f; 在操作系统中&#xff0c;进程是指正在执…

8月31复盘日记

8月31复盘日记 前言今日感恩今日记录今日感悟今日名言 前言 昨天因为回到宿舍比较晚&#xff0c;所以来不及进行记录。   昨天早上五点多就醒了&#xff0c;可能是睡眠质量特别好&#xff0c;基本一倒头就是深度睡眠。所以我直接就起床去健身房运动&#xff0c;练了臀和核心&…

FastAPI+Vue3零基础开发ERP系统项目实战课 20240831上课笔记 路径参数

遗留的问题 3、FastAPI4、MySQL85、SQLModel&#xff0c;和FastAPI框架是同一个作者&#xff0c;两者结合比较好用&#xff0c;但是学习成本较高&#xff0c;使用难度稍大6、Vue37、Tailwindcss8、Ant Design Vue&#xff0c;UI组件库9、数据同步&#xff1a;有一个API&#x…

基于卷积神经网络的磨削平板类零件擦伤检测

基于卷积神经网络的磨削平板类零件擦伤检测 前言正文 前言 还记得读研那会儿刚学习完了卷积神经网络&#xff0c;初步学会了最基础的分类问题&#xff0c;当时也有点python基础&#xff0c;同时对TensorFlow也有点入门了。正好我的课题中有一类缺陷比较难以用传统方法识别判断&…

Vue60 插槽

插槽 作用&#xff1a;让父组件可以向子组件指定位置插入html结构&#xff0c;也是一种组件间通信的方式&#xff0c;适用于 父组件 > 子组件 。 分类&#xff1a;默认插槽、具名插槽、作用域插槽 使用方式&#xff1a; 默认插槽&#xff1a; 父组件中&#xff1a;<Cat…

聊聊 OceanBase 资源管理

OceanBase 是分布式多租户架构数据库&#xff0c;其分布式集群从资源角度看可以分成集群、Zone、OB Server、资源池和租户等几个层次。 今天我们从集群和资源两个层次梳理 OceanBase 资源管理相关的概念。 OceanBase 集群管理 OceanBase 集群包含若干个Zone&#xff0c;一个Z…

CRE6511KL 原边SOP7电源芯片

CRE6511KL 是一款内置高压 BJT 功率开关管,5W多模式原边控制的开关电源芯片。CRE6511KL较少的外围元器件、较低的系统成本设计出高性能的交直流转换开关电源。CRE6511KL 提供了极为全面和性能优异的智能化保护功能&#xff0c;包括逐周期过流保护、软启动、芯片过温保护、输出过…

Hot Chips 2024:博通(Broadcom)展示AI计算ASIC的光学连接

引言 在2024年的Hot Chips会议上&#xff0c;博通展示了其最新的AI计算专用集成电路&#xff08;ASIC&#xff09;&#xff0c;这款ASIC集成了光学连接技术。这一展示不仅体现了博通在定制AI加速器领域的领先地位&#xff0c;也预示着未来数据中心网络技术的一个重要发展方向。…

Ubuntu上qt使用SSH样式表

SSH样式表 如果学习过web的同学都知道&#xff0c;我们在学习HTML的时候会用到样式表&#xff0c;我们使用它来更改我们的颜色、大小、背景等等。上到后面&#xff0c;老师会说&#xff1a;我们如果在HTML文件中编辑太多的样式&#xff0c;就会让代码看起来非常的繁琐&#xf…

Obsidian个人知识库搭建流程

Obsidian的安装 Obsidian官网 个人博客搭建 xlog【使用xlog搭建个人博客Step by Step】 知乎【使用xlog搭建个人博客Step by Step】

实习的一点回顾Gradle

1 项目目录下执行 ./gradlew build&#xff0c;用.gradlew自动构建 首先有一个gradlew&#xff08;linux&#xff09;或者gradlew.bat&#xff08;win&#xff09;是用来检查java配置&#xff0c;环境配置之类的东西 然后去启动gradle/wrapper/gradle-wrapper.jar去检测本地gra…

【Google Play版】bilibili 3.19.2最新国际版(如何鉴别是否官方?)

相信玩 B 站的小伙伴也不少&#xff0c;国内版确实太臃肿了&#xff0c;已经“变质”了。 这是主界面&#xff0c;我没有做任何设置&#xff0c;刚装好就是这样&#xff1a; 摘自 Play 最新的评论&#xff1a; “3.19非常好&#xff0c;解决了我之前两个痛点。一是首页的自动…

基于PCA-SVM的人脸识别系统(MATLAB GUI)

matlab人脸识别系统(GUI),PCASVM方法 源码详细注释以及适配于本系统的5k字的理论资料&#xff08;方便学习&#xff09; GUI里的人脸识别算法&#xff1a;PCA-SVM 功能&#xff1a;利用ORL数据库&#xff0c;进行训练&#xff0c;测试&#xff0c;并可对单张图片进行识别 两个界…

ArrayList的详细使用教程

ArrayList实现了List接口&#xff0c;是顺序容器&#xff0c;即元素存放的数据与放进去的顺序相同&#xff0c;允许放入null元素&#xff0c;底层通过数组实现。除该类未实现同步外&#xff0c;其余跟Vector大致相同。每个ArrayList都有一个容量(capacity)&#xff0c;表示底层…

vscode写markdown(引入html及css语法)

vscode写markdown 下载插件插入代码markdown中引入html和css语法导出pdf 下载插件 Markdown All in OneMarkdown Preview EnhancedPaste Image 插入代码 afdfafamarkdown中引入html和css语法 呵呵 用ctrlshiftp 搜索"Markdown Preview Enhanced:Customize CSS"在…