当前位置: 首页 > news >正文

并发控制方案详解

上一篇博客我们拆解了一个经典并发坑——双线程各执行10次count++,初始值为0,并发执行后却加不到20。核心原因很简单:count++看似一行代码,底层被拆分为“读取-自增-写入”3个非原子指令,线程切换时会导致计数丢失。

知道了问题根源,解决起来就有了明确方向:保证count++操作的原子性,让这3个指令成为不可中断的整体,同一时刻只有一个线程能执行。今天我们就单独聚焦解决方案,用3种常用、易上手的并发控制方案,手把手教你让count稳定加到20,同时拆解每种方案的原理、代码实现、适用场景和避坑点,新手也能直接套用。

先回顾下问题场景(方便对照实操):两个线程t1、t2,各自执行10次count++,count初始值0,未做并发控制时,结果大概率小于20;我们的目标是通过合理的并发控制,让最终结果稳定等于20,同时兼顾代码简洁性和性能。

一、方案1:使用synchronized锁(悲观锁,最简单直接)

synchronized是Java中最基础、最易上手的并发控制手段,属于悲观锁——它默认认为并发访问一定会出现冲突,因此提前加锁,阻止多个线程同时执行临界区代码,从根本上避免指令被打断。

1.1 代码实现(直接修改原测试类)

只需给执行count++的方法添加synchronized修饰,即可保证方法内的代码原子执行:

public class CountTest { // 共享变量count,初始值0 private static int count = 0; // 用synchronized修饰,保证方法原子执行(核心修改) public static synchronized void increment() { for (int i = 0; i < 10; i++) { count++; } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(CountTest::increment); Thread t2 = new Thread(CountTest::increment); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最终count值:" + count); // 稳定输出20 } }

1.2 底层原理

synchronized会给方法(或代码块)添加一把“互斥锁”,当t1线程进入increment()方法时,会自动获取锁,此时t2线程尝试进入该方法时,会被阻塞在锁外,直到t1执行完所有代码、释放锁后,t2才能获取锁并执行。

也就是说,原本并发执行的t1和t2,在synchronized的作用下,变成了串行执行:t1先完整执行10次count++(count从0到10),t2再完整执行10次(count从10到20),自然不会出现计数丢失,结果稳定为20。

从JVM层面来说,synchronized通过“监视器锁(Monitor)”实现,底层依赖CPU的锁指令,能保证操作的原子性、可见性和有序性,彻底解决并发冲突。

1.3 适用场景与避坑点

  • 适用场景:并发量不高、业务逻辑简单的场景(比如简单计数、单线程操作共享资源),尤其是新手入门,优先用synchronized,无需关注复杂的锁操作,代码简洁且不易出错。

  • 避坑点:不要过度使用synchronized——如果修饰的方法体过大,会导致线程阻塞时间过长,降低程序并发效率;另外,synchronized修饰静态方法时,锁的是整个类,而非实例对象,避免误判锁的作用范围。

二、方案2:使用AtomicInteger原子类(乐观锁,性能更优)

如果觉得synchronized的阻塞机制会影响性能,或者并发量稍高,推荐使用AtomicInteger原子类——它属于乐观锁,默认认为并发冲突不频繁,无需加锁,而是通过CAS操作(比较并交换)实现原子自增,避免线程阻塞,性能更优。

AtomicInteger是Java并发包(java.util.concurrent.atomic)中的核心类,专门用于解决简单的原子计数问题,底层基于CPU的CAS原语实现,能保证自增操作的原子性。

2.1 代码实现

用AtomicInteger替代普通int类型的count,使用其提供的incrementAndGet()方法替代count++,无需加锁,即可保证原子性:

import java.util.concurrent.atomic.AtomicInteger; public class CountTest { // 用AtomicInteger替代int,初始值0(核心修改) private static AtomicInteger count = new AtomicInteger(0); public static void increment() { for (int i = 0; i < 10; i++) { // 原子自增,替代count++,保证操作不可中断 count.incrementAndGet(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(CountTest::increment); Thread t2 = new Thread(CountTest::increment); t1.start(); t2.start(); t1.join(); t2.join(); // 用get()方法获取当前值 System.out.println("最终count值:" + count.get()); // 稳定输出20 } }

2.2 底层原理(CAS操作详解)

AtomicInteger的incrementAndGet()方法,底层依赖CAS操作(Compare And Swap,比较并交换),这是一条CPU级别的原子指令,能保证“比较-交换”的原子性,具体流程如下:

  1. 线程读取AtomicInteger中的当前值(记为V),并记录预期值(A = V);

  2. 线程对预期值A进行自增,得到新值B(B = A + 1);

  3. 通过CAS指令比较内存中的当前值V和预期值A:如果V == A,说明没有其他线程修改过,将新值B写入内存;如果V != A,说明其他线程已经修改过,放弃当前操作,重新读取最新值,重复上述步骤(自旋重试)。

简单来说,CAS操作相当于“自旋重试”——没有线程阻塞,只有当冲突发生时,才会重新尝试,相比synchronized的阻塞机制,在低冲突场景下性能更优。AtomicInteger的value字段被volatile修饰,能保证可见性,避免线程读取到旧值。

2.3 适用场景与避坑点

  • 适用场景:IO密集型场景、低并发冲突场景(比如接口计数、流量统计),尤其是需要兼顾性能和简洁性的场景,AtomicInteger是首选。除了AtomicInteger,并发包还提供了AtomicLong、AtomicBoolean等原子类,可根据需求选择,甚至还有AtomicIntegerArray用于原子操作数组元素。

  • 避坑点:CAS存在“ABA问题”——即一个值从A变成B,再变回A,CAS会认为值未被修改,可能导致逻辑错误(本案例中无影响,但复杂场景需注意);可使用AtomicStampedReference添加版本号解决ABA问题。另外,AtomicInteger只适用于简单的原子操作,无法实现复杂的复合逻辑(比如“if(count < 10) count++”)。

三、方案3:使用Lock锁(灵活可控,适合复杂场景)

如果需要更灵活的锁控制(比如公平锁、可中断锁、超时锁),推荐使用Lock锁(以ReentrantLock为例)。Lock是Java 1.5引入的并发控制接口,相比synchronized,它支持手动加锁、释放锁,控制更灵活,适合复杂的并发场景,底层同样通过CAS操作实现锁的获取与释放。

3.1 代码实现

创建ReentrantLock实例,在执行count++的代码块前后,手动加锁、释放锁(释放锁必须放在finally中,避免死锁):

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class CountTest { private static int count = 0; // 创建可重入锁实例(核心修改) private static Lock lock = new ReentrantLock(); public static void increment() { lock.lock(); // 手动加锁 try { // 临界区:count++操作,只有持有锁的线程能执行 for (int i = 0; i < 10; i++) { count++; } } finally { lock.unlock(); // 手动释放锁,必须在finally中,避免死锁 } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(CountTest::increment); Thread t2 = new Thread(CountTest::increment); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最终count值:" + count); // 稳定输出20 } }

3.2 底层原理

ReentrantLock(可重入锁)的核心原理和synchronized类似,都是通过“互斥锁”保证同一时刻只有一个线程执行临界区代码,但它提供了更灵活的锁机制:

  • 可重入性:线程持有锁后,可再次获取该锁(避免自己阻塞自己);

  • 公平锁/非公平锁:默认是非公平锁(和synchronized一致,随机抢占锁),也可通过构造函数创建公平锁(按线程等待顺序获取锁);

  • 可中断:支持线程在等待锁时被中断,避免无限阻塞;

  • 超时锁:可设置获取锁的超时时间,超时后放弃获取锁,避免死锁。

在本案例中,lock.lock()会让t1先获取锁,t2阻塞,直到t1执行完临界区代码、调用lock.unlock()释放锁后,t2才能获取锁执行,从而保证count++的原子性,结果稳定为20。ReentrantLock的性能在高并发场景下,通常优于synchronized(尤其JDK1.6之前),即使JDK1.6对synchronized做了优化,复杂场景下ReentrantLock依然更具优势。

3.3 适用场景与避坑点

  • 适用场景:高并发场景、复杂业务逻辑(比如需要中断锁等待、设置锁超时、使用公平锁),比如电商订单处理、分布式锁实现等。

  • 避坑点:必须手动释放锁,且要放在finally中——如果临界区代码抛出异常,未释放锁会导致其他线程永久阻塞(死锁);另外,避免过度使用公平锁,公平锁会降低并发效率,非特殊需求建议使用默认的非公平锁。

四、3种方案对比与选型建议

为了让大家快速选择适合自己的方案,整理了3种方案的核心对比,结合场景精准选型:

方案

锁类型

核心优势

核心劣势

适用场景

synchronized

悲观锁

代码简洁、无需手动管理锁、不易出错,JVM自动优化

灵活性差、阻塞式、高并发下性能一般

新手入门、并发量低、业务逻辑简单

AtomicInteger

乐观锁(CAS)

无阻塞、性能优、代码简洁,支持原子操作

仅适用于简单原子操作,存在ABA问题

IO密集型、低冲突、简单计数场景

ReentrantLock

悲观锁

灵活可控(公平锁、可中断、超时)、高并发性能优

需手动管理锁、易出现死锁(忘记释放)

高并发、复杂业务、需要灵活锁控制

http://www.jsqmd.com/news/516987/

相关文章:

  • 科研党必备:Latex转Word公式不乱的终极解决方案(附MathType配置技巧)
  • Conda环境下cuDNN与CUDA版本匹配的避坑指南
  • 永磁同步电机谐波抑制算法(14)——无模型预测控制与多同步坐标系谐波抑制的融合
  • 3月净水设备厂家分析出炉,这些品牌脱颖而出,净水设备/反渗透设备/混床设备/电渗析器/离子交换设备,净水设备厂商有哪些 - 品牌推荐师
  • Z-Image-Turbo新手必看:环境搭建与依赖安装,一步步带你跑通
  • 告别复杂代码:用Llama Factory可视化工具10分钟微调大模型
  • 探寻2026年口碑好的日精GTR减速机厂排名,凌圣机电在列 - 工业品牌热点
  • Pixel Dimension Fissioner实战教程:结合Notion API构建自动文案工作流
  • 遥感影像语义分割实战:从EvLab-SS benchmark数据集解析到高效训练样本生成
  • 2026年江苏FRPP管零售商家费用对比,哪家性价比更高 - 工业设备
  • CPU核心、Die和Package详解:从硬件角度理解你的处理器
  • GitOps实战:K8s配置版本管理全指南
  • 2026年日精GTR减速机优质服务厂家,天津地区哪家性价比高 - 工业推荐榜
  • 用YOLOv5s搞定网易易盾滑动验证码缺口识别:30张图训练保姆级教程(附Labelme转YOLO脚本)
  • [开源工具]2024最新免费临时邮箱(Temp Free Mail)终极指南
  • FRPP管大型厂家怎么选,永固工程塑料性价比高不? - 工业品网
  • YOLOv5的Focus模块:一个被误解的‘切片’操作,如何影响了你的检测精度与速度?
  • 2026年奔驰威霆、奔驰V300L、高顶塞纳成都选购权威盘点:五大维度解析四川本地可靠商家报价与配置 - 速递信息
  • LTE RLC层三种模式实战解析:TM/UM/AM到底怎么选?
  • Pixel Dimension Fissioner开源可部署:支持Kubernetes Helm Chart企业级编排
  • Docker小白必看:5分钟搞定Epic免费游戏自动领取(含常见问题解决)
  • 伯特兰悖论给产品经理的启示:如何避免定价策略中的概率陷阱
  • 域网络故障排查与修复指南
  • 实战指南:在UniApp中运用RenderJS突破H5限制,驱动OpenLayers移动GIS开发
  • OCCT 7.9.0 编译实战:从源码下载到VS项目生成的全流程解析
  • 2026年山东地区ELBE十字轴、ELBE驱动轴选购指南及费用说明 - 工业设备
  • 北京腕表保养价格全解析:从百达翡丽到浪琴,高端腕表养护成本与周期数据报告(2026年钟表行业协会最新统计) - 时光修表匠
  • FreeRTOS配置实战:手把手教你裁剪一个适合STM32F103的RTOS内核(附完整FreeRTOSConfig.h文件)
  • 从训练到上线:手把手教你用LLaMA-Factory WebUI完成模型微调、评估与导出完整流水线
  • Vue3模块化实战:如何用export批量导出工具函数提升代码复用率