锁策略详解:互斥锁、读写锁、乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁、偏向锁、可重入锁与不可重入锁、公平锁与非公平锁

本文主要是介绍锁策略详解:互斥锁、读写锁、乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁、偏向锁、可重入锁与不可重入锁、公平锁与非公平锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


一.锁策略

锁策略指的是在多线程编程中用于管理共享资源访问的规则和技术。它们确保在任何给定时间只有一个线程可以访问共享资源,以防止竞态条件和数据不一致性问题。常见的锁策略包括:

  1. 互斥锁(Mutex):最常见的锁类型,用于确保同一时刻只有一个线程可以访问共享资源。其他线程必须等待当前线程释放锁。

  2. 读写锁(ReadWriteLock):允许多个线程同时读取共享资源,但只有一个线程可以写入共享资源。这有助于提高并发性能,因为读取操作不会阻塞其他读取操作。

  3. 悲观锁(Pessimistic Locking):假设会发生并发访问,因此在访问共享资源之前先获取锁。适用于写入操作频繁的场景。

  4. 乐观锁(Optimistic Locking):假设不会发生并发访问,因此在提交更改时检查是否有其他线程已经修改了资源。适用于读取操作频繁,且冲突不经常发生的场景。

  5. 自旋锁(Spin Lock):在获取锁时不会阻塞线程,而是使用循环等待直到锁可用。适用于短期占用情况,避免了线程切换的开销。

  6. ... ...

选择合适的锁策略需要根据具体的应用场景和性能要求来决定。不同的锁策略有不同的开销和适用场景,需要根据实际情况进行权衡和选择。

二.互斥锁

互斥锁(Mutex,Mutual Exclusion)是一种用于多线程编程的同步机制,用于确保在任何给定时间只有一个线程可以访问共享资源。互斥锁提供了两个主要操作:锁定(lock)和解锁(unlock)。在访问共享资源之前,线程需要先获取锁;在访问完成后,需要释放锁,以允许其他线程访问该资源。

互斥锁的特点包括:

  1. 互斥性:同一时间只有一个线程可以持有互斥锁,其他线程必须等待当前线程释放锁后才能获取锁。
  2. 阻塞:当一个线程尝试获取一个已经被其他线程持有的互斥锁时,该线程会被阻塞,直到获取到锁为止。
  3. 锁定状态:互斥锁可以有两种状态:锁定和未锁定。只有持有锁的线程才能访问共享资源。
  4. 安全性:互斥锁可以确保在多线程环境下对共享资源的安全访问,避免了竞态条件和数据不一致性问题。

在C/C++中,互斥锁通常由pthread_mutex_t表示,可以使用pthread_mutex_lock来获取锁,pthread_mutex_unlock来释放锁。在Java中,可以使用synchronized关键字来实现互斥锁的功能,也可以使用ReentrantLock类来实现互斥锁。

三.读写锁

读写锁(ReadWriteLock)是一种并发控制机制,允许多个线程同时读取共享资源,但在写入时需要互斥,即只能有一个线程进行写操作,且写操作时禁止读取操作。读写锁的主要目的是在读操作频繁、写操作较少的情况下提高并发性能。

读写锁有两种状态:读取锁和写入锁。在读取锁状态下,多个线程可以同时获取读取锁,从而允许并发读取操作。在写入锁状态下,只有一个线程可以获取写入锁,其他线程无法获取读取或写入锁,直到写入操作完成。

读写锁的特点包括:

  1. 读写分离:读取操作不会阻塞其他读取操作,但会阻塞写入操作。这样可以提高读取操作的并发性能。

  2. 写入互斥:写入操作会阻塞其他读取和写入操作,确保在写入操作时不会有其他线程对共享资源进行读取或写入。

  3. 公平性:读写锁可以是公平的,即按照请求锁的顺序分配锁;也可以是非公平的,即允许插队获取锁。

Java中的读写锁由ReentrantReadWriteLock实现,读写锁特别适合于 "频繁读, 不频繁写" 的场景中。

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁,这个对象提供了 lock / unlock 方法进行加锁解锁
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个对象也提供了 lock / unlock 方法进行加锁解锁

四.乐观锁与悲观锁

乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是两种不同的并发控制机制,用于处理多线程环境下的数据访问冲突。

  1. 乐观锁(Optimistic Locking)

    • 乐观锁的核心思想是假设在大多数情况下,数据并发访问冲突的概率很低,因此不需要加锁。在读取数据时,不会加锁,而是在尝试更新数据时才会进行检查。
    • 在使用乐观锁时,通常会在读取数据时记录下数据的版本号或者时间戳,在尝试更新数据时检查这个版本号或者时间戳是否发生变化。如果没有变化,则说明在读取到写入之间数据没有被其他线程修改,可以进行更新操作;如果发生变化,则表示数据被修改过,需要进行冲突处理,如回滚或者重试等。
    • 乐观锁适用于读操作频繁、写操作相对较少的场景,可以减少锁竞争,提高并发性能。
  2. 悲观锁(Pessimistic Locking)

    • 悲观锁的核心思想是假设在数据访问时会发生并发冲突,因此在访问数据之前先加锁,确保在任何时候只有一个线程可以访问数据,其他线程需要等待。
    • 悲观锁适用于写操作频繁的场景,可以有效避免数据被并发修改而导致的问题。常见的悲观锁实现包括互斥锁(Mutex)和读写锁(ReadWriteLock)等。

总的来说,乐观锁适合于并发写入比较少、并发读取比较多的场景,可以提高系统的并发性能;而悲观锁适合于并发写入比较频繁的场景,可以保证数据的一致性和完整性。选择合适的锁策略取决于具体的应用场景和需求。

举个例子来说

同学 A 认为 "老师是比较忙的, 我来问问题, 老师不一定有空解答"。因此同学 A 会先给老师发消息: "老师你忙嘛? 我下午两点能来找你问个问题嘛?" (相当于加锁操作) 得到肯定的答复之后,才会真的来问问题。 如果得到了否定的答复,那就等一段时间,下次再来和老师确定时间。这个是悲观锁。

同学 B 认为 "老师是比较闲的, 我来问问题, 老师大概率是有空解答的"。因此同学 B 直接就来找老师(没加锁, 直接访问资源) 如果老师确实比较闲,那么直接问题就解决了。如果老师这会确实很忙,那么同学 B 也不会打扰老师,就下次再来(虽然没加锁,但是能识别出数据访问冲突)。这个是乐观锁。

这两种思路不能说谁优谁劣,而是看当前的场景是否合适。如果当前老师确实比较忙,那么使用悲观锁的策略更合适,使用乐观锁会导致 "白跑很多趟", 耗费额外的资源。如果当前老师确实比较闲, 那么使用乐观锁的策略更合适,使用悲观锁会让效率比较低。

五.重量级锁与轻量级锁

轻量级锁和重量级锁是 Java 虚拟机中用于实现锁的两种不同策略,主要针对 synchronized 关键字所提供的锁的不同实现方式。

  1. 轻量级锁(Lightweight Lock)

    • 轻量级锁是为了在多线程竞争情况下,提高性能而引入的一种锁优化技术。当一个线程尝试获取锁时,如果锁没有被其他线程占用,虚拟机会在当前线程的栈帧中使用 CAS 操作尝试将对象头部的 Mark Word 替换为指向当前线程的锁记录指针(Lock Record Pointer)。
    • 如果 CAS 操作成功,当前线程就获得了锁,并且锁的状态被标记为轻量级锁。此时其他线程访问同步块时会尝试自旋等待,而不是直接阻塞,以减少线程切换的开销。
    • 如果自旋等待一段时间后仍无法获取锁,或者其他线程争用激烈,CAS 操作失败,那么轻量级锁会膨胀为重量级锁。
  2. 重量级锁(Heavyweight Lock)

    • 当轻量级锁膨胀失败时,锁会升级为重量级锁。重量级锁会使其他线程阻塞,而不是进行自旋等待,防止CPU空转浪费资源。
    • 重量级锁的实现通常是通过操作系统的互斥量(Mutex)来实现的,当一个线程获取了锁,其他线程就无法获取锁,只能在等待队列中等待锁释放。

总的来说,轻量级锁适用于线程竞争不激烈的情况,可以减少线程切换的开销,提高性能;而重量级锁适用于线程竞争激烈的情况,可以保证数据的一致性和完整性。 Java 虚拟机会根据锁竞争的情况自动选择合适的锁实现方式。

六.自旋锁

自旋锁是一种基于循环重试的锁机制,在多线程编程中用于实现对共享资源的互斥访问。当一个线程尝试获取自旋锁时,如果锁已被其他线程持有,该线程不会立即进入阻塞状态,而是在循环中不断尝试获取锁,直到成功获取为止,或者达到最大尝试次数后才会放弃。

自旋锁的主要特点包括:

  1. 循环等待:获取自旋锁的线程会在一个循环中不断尝试获取锁,直到成功或者达到一定的尝试次数。
  2. 忙等待:自旋锁的获取过程是忙等待的,即线程在等待锁的过程中会一直占用CPU资源。
  3. 无阻塞:自旋锁的获取过程不会使线程进入阻塞状态,适用于对锁的占用时间较短的情况。
  4. 性能:自旋锁适用于锁竞争不激烈、等待锁时间较短的情况,可以减少线程切换的开销,提高性能。
  5. 等待限时:为了避免自旋锁的线程一直占用CPU资源,通常会设置一个最大尝试次数或者超时时间,超过这个限制后线程会放弃自旋并进入阻塞状态等待锁的释放。

自旋锁在并发度高、锁竞争不激烈的情况下可以有效地提高性能,但在锁竞争激烈、持有锁时间较长的情况下,自旋锁可能会导致性能下降。因此,在选择锁机制时需要根据实际情况进行权衡和选择。在Java中,java.util.concurrent包提供了ReentrantLock的实现,其中包括了可重入锁和自旋锁的功能。

七.偏向锁

偏向锁是Java虚拟机为了减少无竞争情况下的同步原语的性能消耗而引入的一种优化手段。当一个线程访问同步块时,如果该同步块没有被锁定或者被其他线程锁定,那么这个线程会尝试获取偏向锁,并将对象头中的标记设置为指向自己的线程ID。之后,当这个线程再次进入同步块时,不需要再次获取锁,因为对象头中已经记录了这个线程已经获取了偏向锁。

偏向锁适用于大多数情况下都是单线程访问同步块的场景,因为在这种情况下,偏向锁可以显著减少获取锁的代价。但是,如果有其他线程竞争锁,那么偏向锁会失效,对象会膨胀为重量级锁,这样会增加获取锁的代价。

偏向锁的优点包括:

  1. 减少同步操作的耗时,提高程序性能;
  2. 对于无竞争的场景,减少不必要的线程切换。

需要注意的是,偏向锁只适用于无竞争的情况,如果存在竞争,会导致额外的性能损失。因此,在使用偏向锁时,需要根据实际场景进行评估和选择。

八.公平锁与非公平锁

公平锁和非公平锁是两种不同的锁获取策略,用于多线程环境下对共享资源的访问控制。

  1. 公平锁

    • 公平锁是指多个线程按照请求锁的顺序获取锁,即先到先得的原则。当一个线程尝试获取公平锁时,如果锁当前被其他线程持有,该线程会进入等待队列,按照先来后到的顺序等待锁的释放。
    • 公平锁保证了锁的获取是按照请求的顺序进行的,避免了某些线程长时间无法获取到锁的情况,确保了线程获取锁的公平性。
  2. 非公平锁

    • 非公平锁是指多个线程尝试获取锁时,并不考虑锁的等待队列中是否有等待线程,而是直接尝试获取锁。如果锁当前未被其他线程持有,则获取成功;如果锁已被其他线程持有,则当前线程可能会被阻塞,也可能会一直尝试获取锁。
    • 非公平锁相比于公平锁,可能会导致某些线程长时间无法获取到锁,存在一定的不公平性。但由于不需要维护等待队列的顺序,非公平锁的性能通常会比公平锁更好。

在Java中,ReentrantLockReentrantReadWriteLock可以指定为公平锁或非公平锁。在创建锁时,可以通过构造方法来指定锁的公平性。公平锁在一定程度上保证了线程获取锁的顺序,适用于对锁的公平性有要求的场景;而非公平锁则更适用于对性能要求较高,可以容忍一定程度的不公平性的场景。

九.可重入锁与不可重入锁

可重入锁(Reentrant Lock)和不可重入锁是两种锁的概念,主要区别在于同一个线程能否多次获取同一把锁。

  1. 可重入锁

    • 可重入锁是指同一个线程可以多次获取同一把锁,而不会发生死锁。线程每次获取锁时,锁的计数器会加一;每次释放锁时,计数器会减一。只有当计数器为零时,锁才会被完全释放。
    • 可重入锁的一个典型应用场景是方法递归调用。当一个同步方法内部调用同一个类的其他同步方法时,如果没有可重入锁,就会发生死锁。而可重入锁能够保证线程在递归调用时不会被自己阻塞。
  2. 不可重入锁

    • 不可重入锁是指同一个线程不能多次获取同一把锁。如果一个线程已经持有了某个锁,再次尝试获取该锁时会被阻塞,直到锁被释放。
    • 不可重入锁在一些情况下可能会导致死锁,因为线程在持有锁的情况下又试图获取同一把锁,会造成自己阻塞而无法释放已持有的锁。

Java中的synchronized关键字实现的锁是可重入锁,即同一个线程可以重复获取同一把锁。而ReentrantLock也是可重入锁的一种实现。可重入锁能够简化并发编程中对锁的使用,避免了一些潜在的死锁情况,提高了程序的可靠性和健壮性。




 本次的分享就到此为止了,希望我的分享能给您带来帮助,创作不易也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见

这篇关于锁策略详解:互斥锁、读写锁、乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁、偏向锁、可重入锁与不可重入锁、公平锁与非公平锁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/985350

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

10. 文件的读写

10.1 文本文件 操作文件三大类: ofstream:写操作ifstream:读操作fstream:读写操作 打开方式解释ios::in为了读文件而打开文件ios::out为了写文件而打开文件,如果当前文件存在则清空当前文件在写入ios::app追加方式写文件ios::trunc如果文件存在先删除,在创建ios::ate打开文件之后令读写位置移至文件尾端ios::binary二进制方式

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP

【STM32】SPI通信-软件与硬件读写SPI

SPI通信-软件与硬件读写SPI 软件SPI一、SPI通信协议1、SPI通信2、硬件电路3、移位示意图4、SPI时序基本单元(1)开始通信和结束通信(2)模式0---用的最多(3)模式1(4)模式2(5)模式3 5、SPI时序(1)写使能(2)指定地址写(3)指定地址读 二、W25Q64模块介绍1、W25Q64简介2、硬件电路3、W25Q64框图4、Flash操作注意事项软件SPI读写W2

嵌入式Openharmony系统构建与启动详解

大家好,今天主要给大家分享一下,如何构建Openharmony子系统以及系统的启动过程分解。 第一:OpenHarmony系统构建      首先熟悉一下,构建系统是一种自动化处理工具的集合,通过将源代码文件进行一系列处理,最终生成和用户可以使用的目标文件。这里的目标文件包括静态链接库文件、动态链接库文件、可执行文件、脚本文件、配置文件等。      我们在编写hellowor

LabVIEW FIFO详解

在LabVIEW的FPGA开发中,FIFO(先入先出队列)是常用的数据传输机制。通过配置FIFO的属性,工程师可以在FPGA和主机之间,或不同FPGA VIs之间进行高效的数据传输。根据具体需求,FIFO有多种类型与实现方式,包括目标范围内FIFO(Target-Scoped)、DMA FIFO以及点对点流(Peer-to-Peer)。 FIFO类型 **目标范围FIFO(Target-Sc

019、JOptionPane类的常用静态方法详解

目录 JOptionPane类的常用静态方法详解 1. showInputDialog()方法 1.1基本用法 1.2带有默认值的输入框 1.3带有选项的输入对话框 1.4自定义图标的输入对话框 2. showConfirmDialog()方法 2.1基本用法 2.2自定义按钮和图标 2.3带有自定义组件的确认对话框 3. showMessageDialog()方法 3.1