JAVA乐观锁小结

本文介绍了Java乐观锁的概念,重点讲述了CAS技术的工作原理,包括AtomicInteger类中的应用。同时,分析了CAS可能遇到的ABA问题和CPU资源浪费问题,并提出了相应的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说到Java锁机制,我们可以暂时将Java的锁分为乐观锁和悲观锁。它们是整个并发编程中很重要的基石,也是面试当中经常会被问到的知识点。今天我们就来总结一下乐观锁的那些事。

什么是乐观锁

既然说到了乐观锁,那么就需要先解释一个问题,即什么是乐观锁?

顾名思义,乐观锁就是一种理想化的状态,非常乐观。每次去取数据的时候都认为其他线程不会对该数据进行修改,因此不会考虑上锁(毕竟一般上锁都是为了主动从防止一些“坏人”,而我们的乐观锁天真的认为没什么坏人)。

基于乐观锁的思想有很多种实现方式,而谈到这里就不得不说一下今天的主角,就是CAS("Compare and Swap)

CAS技术

CAS是乐观锁的一种经典实现方式,当多个线程尝试更新一个变量的时候,只有一个线程能够更新成功,其它线程都会被告知更新失败,但是失败的线程并不会挂起而是会自旋等待并重试。再整个CAS实现当中,有几个参数需要给大家解释一下:V(内存的新值),A(期待值),B(要写入的值)。而CAS的工作过程即:先去读取一次内存当中的旧值作为期待值,再准备向数据B之前,再次取内存当中读取该值是否有被修改。如果被修改(A != V),则放弃被次操作并自旋重试。如果没有被修改,则写入该值。由于其简单的轻量级的实现方式,JUC当中也有大量的工具类选择基于CAS进行实现:Atomic相关类,ReentrantLock中的FairSync类等。

接下来我们通过源码的方式,具体看一下CAS在AtomicInteger类中的应用。

AtomicInteger

对于AtomicInteger,我们可以通过其getAndIncrement()方法来一窥CAS的工作原理和CAS的一些特性:

整个getAndIncrement的方法调用链:

graph LR
AtomicInteger.getAndIncrement-->unsafe.getAndAddInt
unsafe.getAndAddInt -->this.compareAndSwapInt

而compareAndSwapInt就是整个getAndIncrement实现的关键。通过源码我们可以知道compareAndSwapInt是一个native修饰符修饰的本地方法。查找资料后,我对这个方法有了进一步的了解。即compareAndSwapInt在Unsafe类中会调用Atomic::cmpxchg(该方法再winodws_x86 和 linux_x86中均有实现)。具体的:

1.会先通过os:is_mp()方法判断是否为多处理系统。(如果为单处理器系统则没必要加上lock指令,减少性能开销)

2.Lock_If_MP(mp)会根据mp的值来决定是否为cmpxchg添加lock前缀。(【注】此处的cmpxchg为汇编指令,其作用是比较与交换)

并且Intel手册对lock指令有如下说明:

  1. lock指令能够确保对内存的读-改-写操作的原子性
  2. lock指令能够禁止该指令前后的读写指令重排序
  3. lock指令会立即将缓存中的数据写回主存
小结

通过上面的描述我们可以看到,虽然在代码层面CAS并没有加锁,但最终依然只能有一个线程可以对目标变量进行操作。除此之外CAS可以保证单个共享变量操作的原子操作并且具有volatile的读写语义。

CAS可能会出现什么问题?

上面我们了解了CAS的原理和应用,那么我们需要思考一个问题。在我们使用CAS进行并发操作的时候,可能会出现什么问题?这里,我们可以主要总结三点问题:

  • ABA问题
  • 自旋所带来的CPU开销
  • 只能保证单个共享变量的原子操作

ABA问题

ABA问题是使用CAS时最常出现的一个问题。它的整个过程如下图所示:

sequenceDiagram
A(线程2修改一次值,线程3改回原值)->>线程1: 读
A(线程2修改一次值,线程3改回原值)->>线程2: 读
线程2->>A(线程2修改一次值,线程3改回原值):写
A(线程2修改一次值,线程3改回原值)->>线程3: 读
线程3->>A(线程2修改一次值,线程3改回原值):写
线程1->>A(线程2修改一次值,线程3改回原值):写

通过上图我们可以看到虽然线程1仍然可以CAS写成功,但是它并没有感受到该A值在整个过程当中发生的问题。有可能它的值没有发生变化,而其含义却已经发生了变化。其实这种场景也非常常见,比如在我们的业务当中,我们需要在修改记录数据之前验证数据是否发生过变化,如果没有发生变化则进行写入,如果发生变化则放弃。这样可以在一定程度上提高并发度。

那么如何解决ABA问题?  

对于ABA问题的常见解决思路即生成一个唯一可表示记录信息的标记值。例如我们可以新增一个自增字段,每次操作这个字段后该值加1,写写入数据之前比较该值是否与进入该方法时读取到的值相同。初次之外还可以记录版本号和时间戳(思路大同小异)。

对于自旋带来的CPU资源浪费问题

根据上面的分析我们知道,在CAS写入的过程当中,如果写入失败并不会挂起线程,而是会自旋并继续重试。在某些极端场景下,这可能会死循环或者造成CPU资源的白白浪费。在我们平时的编码过程当中,我们其实也可以考虑jdk1.6之后对synchronized进行锁升级的思路。自旋到一定次数还无法或者资源时,我们可以考虑放弃该任务返回null值或主动升级成重量级锁。

以上,便是我在学习轻量级锁时的一些总结和认识,希望能够帮助到有需要的人。也希望各位大家能够不吝指出错误,留言进一步的讨论。

### Spring Boot 集成百度 DeepSeek 实现流式输出 为了在 Spring Boot集成百度 DeepSeek 并实现流式输出功能,可以遵循如下方法: 创建 Maven 或 Gradle 项目并引入必要的依赖项。对于 Maven 用户,在 `pom.xml` 文件中加入以下内容来添加对百度 AI SDK 的支持[^2]。 ```xml <dependency> <groupId>com.baidu.aip</groupId> <artifactId>easydl-java-sdk</artifactId> <version>LATEST_VERSION</version> </dependency> ``` 配置文件 `application.properties` 中设置 API 密钥和其他参数以便连接到 DeepSeek 服务[^3]. ```properties baidu.apiKey=YOUR_API_KEY baidu.secretKey=YOUR_SECRET_KEY ``` 编写 Java 类用于初始化 AipSpeech 客户端实例以及定义处理音频数据的方法。这里展示了一个简单的例子,其中包含了获取 token 和发送语音请求的功能[^4]: ```java import com.baidu.aip.speech.AipSpeech; @Service public class SpeechService { @Value("${baidu.apiKey}") private String apiKey; @Value("${baidu.secretKey}") private String secretKey; private final AipSpeech client; public SpeechService() { this.client = new AipSpeech(apiKey, secretKey); } /** * 发送语音合成请求. */ public byte[] synthesize(String text) throws Exception { JSONObject response = client.synthesis(text, "zh", 1, null); int statusCode = ((Number)response.get("err_no")).intValue(); if (statusCode != 0){ throw new RuntimeException("Error code:" + statusCode); } return Base64.decodeBase64(response.getString("result")); } } ``` 构建 RESTful Web Service 来接收客户端传入的数据并通过上述 service 层调用相应逻辑完成实际操作。下面是一个控制器类的例子,它允许通过 POST 请求传递待转换的文字串,并返回经过编码后的 PCM 数据作为响应体的一部分[^5]。 ```java @RestController @RequestMapping("/api/deepseek") public class StreamingController { @Autowired private SpeechService speechService; @PostMapping(value="/synthesize", produces="audio/l16;rate=16000") public ResponseEntity<Resource> handlePost(@RequestBody String body) throws IOException{ try { byte[] pcmData = speechService.synthesize(body); InputStream inputStream = new ByteArrayInputStream(pcmData); Resource resource = new InputStreamResource(inputStream); HttpHeaders headers = new HttpHeaders(); headers.setContentLength(pcmData.length); headers.setContentType(MediaType.parseMediaType("audio/l16")); return new ResponseEntity<>(resource, headers , HttpStatus.OK); } catch(Exception e){ log.error(e.getMessage(),e); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } } ``` 此代码片段展示了如何利用 Spring Boot 构建应用程序并与百度 DeepSeek 进行交互以执行文本转语音的任务。请注意替换占位符(如 YOUR_API_KEY)为真实的值,并根据具体需求调整其他细节部分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值