Java 并发(三) —— 锁

embedded/2024/10/15 22:27:58/

一、悲观锁

并发访问共享资源时,冲突概率可能非常高,所以先加锁,再访问修改共享资源前。

缺点:

  1. 高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。
  2. 可能会存在死锁问题(线程获得锁的顺序不当时),影响代码的正常运行。

1.互斥锁和自旋锁

当加锁失败时:

  • 互斥锁用「线程切换」来应对:两次线程上下文切换,性能损耗比较大
  • 自旋锁用「忙等待」来应对:需要一直占有CPU,性能开销小

自旋锁适用于明确知道被锁住的代码的执行时间很短,等待时间不长的情况。

2.读写锁

适用于明确 区分读操作和写操作的场景。读操作并发性很强

  • 读优先锁:写线程会被饿死
  • 写优先锁:读线程会被饿死
  • 公平读写锁:用队列根据先入先出原则对读、写线程进行排队,读写线程都不会饿死

二、乐观锁

并发访问共享资源时,冲突概率可能非常低,所以先访问修改共享资源,再检查,如果发生冲突则放弃本次修改。

优点:

  1. 高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上往往会更胜一筹。

缺点:

  1. 如果冲突频繁发生(写占比非常多的情况),会频繁失败并重试,这样同样会非常影响性能,导致 CPU 飙升。

1.版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

2.CAS算法

CAS(Compare-And-Swap, 比较并交换) 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

缺点:

  1. "ABA"问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。ABA 问题的解决思路是在变量前面追加上版本号或者时间戳
  2. CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。(基于 CAS 实现的自旋锁是悲观锁
  3. CAS 操作仅能对单个共享变量有效。

2.1. CAS 在 Java 中的实现:Unsafe 类

Unsafe类中的 CAS 方法是native方法。native关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。

java">Unsafe#getAndAddInt源码// 原子地获取并增加整数值
public final int getAndAddInt(Object o, long offset, int delta) {int v;do {// 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v + delta));// 返回旧值return v;
}

可以看到,getAndAddInt 使用了 do-while 循环:在compareAndSwapInt操作失败时,会不断重试直到成功。也就是说,getAndAddInt方法会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败(当前值在此期间被其他线程修改),它会重新获取当前值并再次尝试更新,直到操作成功。

由于 CAS 操作可能会因为并发冲突而失败,因此通常会与while循环搭配使用,在失败后不断重试,直到操作成功。这就是 自旋锁机制

2.2. Unsafe 类的应用:Atomic 类

java">AtomicInteger核心源码// 获取 Unsafe 实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;static {try {// 获取“value”字段在AtomicInteger类中的内存偏移量valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }
}
// 确保“value”字段的可见性
private volatile int value;// 如果当前值等于预期值,则原子地将值设置为newValue
// 使用 Unsafe#compareAndSwapInt 方法进行CAS操作
public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}// 原子地将当前值加 delta 并返回旧值
public final int getAndAdd(int delta) {return unsafe.getAndAddInt(this, valueOffset, delta);
}// 原子地将当前值加 1 并返回加之前的值(旧值)
// 使用 Unsafe#getAndAddInt 方法进行CAS操作。
public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
}// 原子地将当前值减 1 并返回减之前的值(旧值)
public final int getAndDecrement() {return unsafe.getAndAddInt(this, valueOffset, -1);
}

AtomicInteger是 Java 的原子类之一,主要用于对 int 类型的变量进行原子操作,它利用Unsafe类提供的低级别原子操作方法实现无锁的线程安全性。

总结

Java 中 Unsafe 类提供原子操作(CAS),这些原子操作通过调用 native 方法来执行。

Atomic 原子类 通过调用 Unsafe 类中的方法保证了硬件级别的原子操作,实现了原子性,再通过 volatile 关键字保证多线程之间变量的可见性,二者的结合才有了 多线程下的 CAS 乐观锁


三、其它锁

1.公平锁与非公平锁

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

2.可中断锁与不可中断锁

  • 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。
  • 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。

四、参考

【多线程与高并发】- synchronized锁的认知_双机运行 synchronized能否生效-CSDN博客

源码级别的讲解JAVA 中的CAS_cas源码-CSDN博客

乐观锁和悲观锁详解 | JavaGuide

Java并发编程面试题 | 小林coding (xiaolincoding.com)


http://www.ppmy.cn/embedded/94111.html

相关文章

虚拟机centos9搭建wordpress

目录 安装环境和搭建简介 1. 更换yum源更新系统软件包: 1.1备份yum源 1.1.1创建备份目录: 1.1.2移动现有仓库配置文件到备份目录: 1.1.3验证备份: 1.2更换yum源 1.2.1添加yum源 1.2.2删除和建立yum缓存 1.3更新系统软件…

C++之 bind 绑定器深入学习:从入门到精通!

简介 本文详细阐述了 C 中关于 bind 绑定器技术的基本概念和常用技巧。 引入动机 在标准算法库中&#xff0c;有一个算法叫 remove_if&#xff0c;其基本使用如下&#xff1a; #include <iostream> #include <string> #include <algorithm> #include &l…

【ARM】v8架构programmer guide(5)_ARMv8指令集介绍

目录 5.1 ARMv8 指令集 5.1.1 32bit和64bit A64指令的重大区别 5.1.2 地址 5.1.3 寄存器 5.2 不同指令集直接的切换 ARMv8架构中最显著的变化之一是引入了64位指令集。这个指令集补充了现有的32位指令集架构。这一增加使得处理器可以访问64位宽的整数寄存器和数据操作&…

ubuntu将软件放到任务栏

右键点击这个 pycharm 方法1&#xff1a; 方法2&#xff1a; sudo nano /usr/share/applications/PyCharm.desktop 编辑这个 [Desktop Entry] NamePyCharm CommentPyCharm Integrated Development Environment Exec/path/to/PyCharm.sh Icon/path/to/PyCharm.svg Terminalf…

如何使用Jmeter对HTTP接口进行压力测试?

我们不应该仅仅局限于某一种工具&#xff0c;性能测试能使用的工具非常多&#xff0c;选择适合的就是最好的。笔者已经使用Loadrunner进行多年的项目性能测试实战经验&#xff0c;也算略有小成&#xff0c;任何性能测试&#xff08;如压力测试、负载测试、疲劳强度测试等&#…

AJAX基础知识

AJAX&#xff08;Asynchronous JavaScript and XML&#xff09;是一种技术&#xff0c;用于在网页加载时从服务器异步获取数据&#xff0c;而无需重新加载整个页面。它允许在后台与服务器进行通信&#xff0c;提升网页的交互性和用户体验。AJAX 通常结合 JavaScript 和 XML&…

MBTI 性格测试小程序实战

MBTI 性格测试应用介绍 参考项目&#xff1a;16Personalities&#xff08;https://www.16personalities.com/ch&#xff09; MBTI 实现方案介绍 核心组成&#xff1a; 题目用户答案评分规则 题目结构 暂时使用JSON&#xff0c;便于理解&#xff0c;result代表题目对应的…

http跨域网络请求中的CORS(跨源资源共享) 那些事 -- HTTP跨域请求, chrome插件跨域请求使用详解, origin格式,origin通配符等

在我们进行网络应用开发的时候&#xff0c;如果用到了跨域网络请求&#xff0c;则不可避免的就会遇到http跨域网络请求 CORS的问题&#xff0c;今天就和大家来聊聊跨域网络请求中的CORS的那些事。 跨源资源共享&#xff08;CORS&#xff09; CORS 是一种基于 HTTP 头的机制&a…