CAS解析和 synchronized 优化过程

ops/2024/9/23 19:18:13/

目录

正文:

1.synchronized的优化过程

1.1锁粗化与锁细化

1.2自旋锁

1.3锁消除

1.4 偏向锁

1.5. 轻量级锁

1.6 重量级锁

CAS-toc" style="margin-left:80px;"> 2.CAS

2.1概述

2.2java中的cas操作

 2.3ABA问题

总结:


正文:

1.synchronized的优化过程

synchronized 是 Java 中用于实现同步和线程安全的关键字。在 Java 早期版本中,synchronized 的实现较为简单,但它在面对高并发场景时存在一些性能问题。随着 Java 虚拟机(JVM)的发展,synchronized 经历了一系列的优化,以提高其在并发环境下的性能。

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

1.1锁粗化与锁细化

为了减少频繁的锁申请和释放操作,JVM 引入了锁粗化和锁细化技术。

  • 锁粗化:将多个连续的锁操作合并为一个更大范围的锁,减少锁的申请和释放次数。
  • 锁细化:将一个大范围的锁分解为多个小范围的锁,以减少锁的争用。
1.2自旋锁

为了减少线程在等待锁时的阻塞和上下文切换开销,JVM 引入了自旋锁。当一个线程尝试获取一个已被其他线程持有的锁时,它不是立即阻塞,而是在当前线程上执行一个循环(自旋),等待锁被释放。如果锁在短时间内被释放,自旋锁可以显著提高性能。同时JVM 会根据历史锁的获取情况来调整自旋的次数,这种自旋称为适应性自旋。如果一个锁通常在自旋后能够很快被获取,那么 JVM 会增加自旋的次数;反之,如果自旋很少成功,JVM 会减少自旋次数,避免浪费处理器资源。

1.3锁消除

JVM 通过逃逸分析,识别出一些不可能被其他线程访问的共享资源,从而在编译时消除对这些资源的锁操作。锁消除减少了不必要的同步开销,提高了程序的运行效率。

1.4 偏向锁

为了减少在没有锁竞争的情况下的同步开销,JVM 引入了偏向锁。偏向锁会偏向于第一个获取它的线程,这个线程在后续获取锁时不需要进行任何同步操作。只有当另一个线程尝试获取这个偏向锁时,偏向模式才会被撤销,并根据情况可能升级为轻量级锁或重量级锁。偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程. 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销) 如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前 申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态。

1.5. 轻量级锁

当偏向锁被打破,即有其他线程尝试获取这个锁时,JVM 会尝试使用轻量级锁。轻量级锁依赖于 CAS 操作来实现线程间的同步。如果 CAS 操作成功,线程获得锁;如果失败,线程可以自旋等待或者阻塞等待。自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源。因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了,也就是所谓的 "自适应"。

1.6 重量级锁

如果竞争进一步激烈,自旋不能快速获取到锁状态, 就会膨胀为重量级锁 此处的重量级锁就是指用到内核提供的 mutex 。执行加锁操作,会先进入内核态, 在内核态判定当前锁是否已经被占用 ,如果该锁没有占用,则加锁成功,并切换回用户态。 如果该锁被占用,则加锁失败。 此时线程进入锁的等待队列,挂起,等待被操作系统唤醒.。经过一段时间后,这个锁被其他线程释放了,操作系统也想起了这个挂起的线程,于是唤醒 这个线程,尝试重新获取锁。

简单来说,会有以下几个过程:

CAS" style="background-color:transparent;"> 2.CAS

2.1概述

CAS(Compare-And-Swap)是一种乐观锁机制,用于实现对共享变量的原子操作。CAS 通过原子性地比较内存中的值和预期的值,当两者相等时,更新内存中的值为新值。CAS 操作包括三个步骤:读取变量当前值、比较变量当前值和期望值、更新变量值。如果当前值等于期望值,则更新变量值,否则重试。CAS 操作是在硬件层面上提供的原子操作,可以保证并发访问下的数据一致性。

CAS 操作包含三个参数:内存位置(V),预期原值(A)和新值(B)。CAS 的基本步骤如下:

  1. 读取内存位置 V 的当前值。
  2. 检查当前值是否与预期原值 A 相等。
  3. 如果相等,将内存位置 V 更新为新值 B。
  4. 如果不相等,操作失败,可以选择重试或放弃。

CAS 操作是原子的,这意味着在整个比较和交换的过程中,内存位置 V 不会被其他线程修改。

CAS伪代码(下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的)

boolean CAS(address, expectValue, swapValue) {
if (*address == expectedValue) {*address = swapValue; return true;}
return false;
}
2.2java中的cas操作

Java 中的 CAS 操作是通过 java.util.concurrent.atomic 包中的原子类实现的。这些原子类利用了 CPU 指令集(如 CMPXCHG)中的原子操作来保证在多线程环境下变量的更新是“看起来”无锁的(lock-free)。Java 提供了一系列原子类,位于 java.util.concurrent.atomic 包中,如 AtomicIntegerAtomicLongAtomicReference 等。这些原子类提供了一些常见的原子操作方法,如 getAndIncrement()compareAndSet()getAndSet() 等。

下面以 AtomicInteger 类为例,介绍一些常用的原子操作方法:

  1. get():获取当前值。
  2. getAndIncrement():获取当前值并自增。
  3. compareAndSet(int expect, int update):比较当前值与期望值,如果相等则更新为新值。
  4. getAndSet(int newValue):设置新值并返回旧值。

下面是一个简单的示例代码,演示了如何使用 AtomicInteger 类进行原子操作:

import java.util.concurrent.atomic.AtomicInteger;public class casExample {//原子变量private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {//创建两个线程Thread t1 = new Thread(() ->{for(int i = 0; i < 5000; i++){//类似与count++;count.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

使用 AtomicInteger 类实现原子操作,在两个线程中分别对共享的 AtomicInteger 类型变量 count 进行自增操作,最终输出结果10000。

 2.3ABA问题

定义:

在多线程环境中,当线程 A 读取一个变量的值(假设为 A),在这个过程中线程 B 也读取了该值,并将变量修改为其他值(假设为 B),随后线程 B 又将变量改回原来的值 A。此时,线程 A 进行 CAS 操作,由于读取的值没有变化(仍然是 A),CAS 操作可能会成功,尽管实际上该值已经被其他线程修改过。

ABA 问题可能导致以下影响:

  • 数据不一致:由于 CAS 操作在值未变化的假象下成功,可能会导致基于该值的逻辑产生不一致的结果。
  • 逻辑错误:在某些情况下,ABA 问题可能导致程序逻辑错误,因为程序可能期望在值发生变化时执行某些操作,但由于 ABA 问题,这些操作可能被遗漏。

示例代码

import java.util.concurrent.atomic.AtomicReference;public class test {public static void main(String[] args) throws InterruptedException {AtomicReference<Integer> ref = new AtomicReference<>(1);Thread t1 = new Thread(() -> {int original = ref.get();System.out.println("原始值为 " + original); // 输出原始值// 模拟值被其他线程修改并恢复ref.set(2);ref.set(original);// 即使 ref 的值恢复为 original,CAS 操作仍然会成功boolean success = ref.compareAndSet(original, 3);System.out.println("cas操作是否成功 " + success); // 输出 CAS 操作结果});t1.start();t1.join();System.out.println("cas操作后的值:" + ref);}
}

输出结果为

 

在这个例子中,主线程首先读取了 ref 的值为 1,然后模拟了其他线程将 ref 的值修改为 2 后又改回 1 的过程。即使 ref 的值被恢复了,由于 CAS 操作是基于值比较的,它仍然会认为操作成功,在特定的情况下就会造成问题。

解决方法:

有几种策略可以解决或缓解 ABA 问题:

  • 版本号:在每个对象中加入一个版本号,每次修改对象时,版本号递增。CAS 操作不仅比较对象的值,还要比较版本号,从而确保即使值相同,版本号不同也能检测到变化。
  • AtomicStampedReference:Java 提供了 AtomicStampedReference 类,它通过引入一个“stamp”(类似于版本号)来解决 ABA 问题。每次更新对象时,stamp 也会更新,从而避免了 ABA 问题。
  • 锁机制:虽然不是解决 ABA 问题的直接方法,但在某些情况下,使用锁(如 synchronized 或 ReentrantLock)来保证操作的原子性和可见性,也是一种可行的替代方案。

示例代码

import java.util.concurrent.atomic.AtomicStampedReference;public class ABASolutionExample {public static void main(String[] args) {AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(1, 1);int[] stamp = new int[1];stamp[0] = asr.getStamp();Integer current = asr.getReference();current = new Integer(2); // 模拟值被其他线程修改asr.compareAndSet(1, current, stamp[0], stamp[0] + 1); // 模拟恢复原始值stamp[0]++;boolean success = asr.compareAndSet(1, 3, stamp[0] - 1, stamp[0]);System.out.println("CAS operation success: " + success); // 输出 CAS 操作结果}
}

在这个例子中,即使 asr 的引用值被恢复为 1,但由于 stamp 值已经增加,CAS 操作将会失败。

总结:

锁策略在多线程编程中起着至关重要的作用,合理选择和优化锁策略可以提高程序的性能和可靠性。对于一些需要高性能的场景,想要保证线程安全如果单单靠加锁无法满足要求,可以考虑使用cas来进行优化。


http://www.ppmy.cn/ops/8564.html

相关文章

使用go_concurrent_map 管理 并发更新缓存

在后台服务中&#xff0c;为了提速&#xff0c;我在内存还做了一个告诉缓存来管理用户信息&#xff0c;根据更新通知&#xff0c;或者定时去redis中同步信息&#xff0c;那么在加载或者更新某个用户元素时&#xff0c;要防止并发&#xff0c; 当&#xff1a; 1&#xff09;如…

构建未来跨境电商平台:系统架构与关键技术

随着全球市场的日益融合和电子商务的快速发展&#xff0c;跨境电商平台成为了连接全球买家和卖家的重要桥梁&#xff0c;为消费者提供了更广阔的购物选择&#xff0c;为企业拓展国际市场提供了更广阔的机会。而要构建一个高效、稳定的跨境电商平台&#xff0c;除了吸引人们的注…

OpenHarmony实战开发-减少卡顿丢帧、合理使用renderGroup

概述 在大型业务场景开发过程中&#xff0c;为了提升产品的视觉效果&#xff0c;经常大量使用属性动画和转场动画&#xff0c;当业务场景复杂度达到一定程度之后&#xff0c;就有可能出现卡顿的情况。本文推荐在单一页面上存在大量应用动效的组件时&#xff0c;使用renderGrou…

C#表对象model添加一个表中没有的字段

C# 对象model,想添加一个表中没有的字段&#xff0c;如何标记&#xff0c;使用属性 [NotMapped] using System; using System.ComponentModel.DataAnnotations.Schema; using ALP.Application.Code; namespace ALP.Application.Entity.PlanManage {/// <summary>/// PL_…

Vmware ---快捷键

Vi 文件名.c xrandr 查看分辨率 xrandr -s 分辨率 调你自己想要的分辨率 ctr shift 放大字体 ctr - 缩小字体 ctr alt t 打开控制台 cd caoshupei 进入曹树培文件夹 cd .. 退回上层文件夹 ls 列出生成的文件 ls -a 显示所有文件&#xff0c;包含隐藏的文件和文件…

Cesium简单案例

一、Cesium组件 1、HTML <template><div id"cesiumContainer"><!-- 地图工具栏 --><ul class"mapTools"><li v-for"item in toolsData" :key"item.id" click"toolsClick(item)"><!-- 显…

笔记 | 编译原理L2:词法分析(lexical analysis)

1 概述 词法分析(lexical analysis)&#xff0c;也称scanning, 编译程序的第一阶段,其作用是识别单词(程序意义上)并找出词法错误. 读入源程序的输入字符、将它们拆分成词素&#xff0c;生成并输出一个词法单元序列&#xff0c;每个词法单元对应于一个词素 回顾词法分析在整个…

备战面试K8S

备战面试&&K8S Kubernetes关于DockerDocker的优缺点分析 WebAssemblyWebAssembly与Container比较 CtrCrictlCtr和CriCtl的区别 Pod生命周期PodConditions容器状态Pod容器组成生命周期的流程 Kubelet EFK日志采集工具的优缺点 Kubernetes 容器运行接口 Container Runti…