news 2026/2/7 5:07:51

线程安全揭秘:如何让多线程程序不再打架?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线程安全揭秘:如何让多线程程序不再打架?

文章目录

    • 一、什么是线程安全?从餐厅厨房说起
    • 二、线程安全问题的根源:计算机底层视角
      • 1. 内存可见性问题:不只是"看不见"那么简单
      • 2. 竞态条件:像"抢购限量商品"
    • 三、Java中的线程安全解决方案
      • 1. 内置锁(synchronized):厨房的"专用令牌"
      • 2. volatile关键字:餐厅的"中央公告板"
      • 3. 原子类:无锁的"智能计数器"
      • 4. 并发集合:线程安全的"共享储物柜"
    • 四、实战场景:如何选择正确的线程安全策略
      • 场景1:计数器(高频更新)
      • 场景2:缓存(读多写少)
      • 场景3:状态标志(简单状态控制)
    • 五、线程安全的级别:从"不可变"到"线程对立"
    • 六、线程安全的最佳实践
    • 七、总结:线程安全的"终极秘诀"
      • 参考文章:

大家好,我是你们的后端技术老友科威舟,今天给大家分享一下线程安全的原理。

多个线程同时访问时,如果不需要额外的同步就能正确工作,那就是线程安全的——这就像一家和谐的餐厅,多位厨师共享厨房却不会互相干扰。

作为后端开发者,我们常遇到这种情况:单线程测试完美的系统,在高并发下突然崩溃。这不是系统的缺陷,而是线程安全在作祟。今天,让我们一起深入探讨线程安全的奥秘。

一、什么是线程安全?从餐厅厨房说起

想象一家繁忙的餐厅厨房,多位厨师(线程)共享使用有限的厨具(共享资源)和食材(数据)。如果没有合理规则,可能会发生:

  • 两位厨师同时争抢同一把刀(资源竞争
  • 一位厨师刚判断汤里需要加盐,另一位却把盐用光了(竞态条件
  • 一位厨师更新了菜单,但其他厨师仍按旧菜单准备(内存可见性问题)

在Java世界中,一个简单的示例可以说明问题:

publicclassUnsafeCounter{privateintcount=0;publicvoidincrement(){count++;// 这不是原子操作!}}

这个简单的count++操作实际上包含三个步骤:读取当前值、增加1、写回新值。当多线程同时执行时,可能会发生数据丢失现象。

二、线程安全问题的根源:计算机底层视角

1. 内存可见性问题:不只是"看不见"那么简单

现代计算机架构中,每个CPU都有自己的缓存。当一个线程修改了共享变量,该修改可能暂时只存在于当前CPU的缓存中,不会立即写回主内存,其他线程也就无法立即看到这个变化。

publicclassVisibilityProblem{privatestaticbooleanflag=false;// 缺少volatile关键字publicstaticvoidmain(String[]args){Threadwriter=newThread(()->{try{Thread.sleep(1000);}catch(InterruptedExceptione){}flag=true;// 修改可能不会立即对其他线程可见});Threadreader=newThread(()->{while(!flag){// 可能永远循环,看不到flag的变化}});writer.start();reader.start();}}

2. 竞态条件:像"抢购限量商品"

竞态条件就像多人同时抢购最后一件商品:A看到有库存,B也看到有库存,但只有一人能成功购买。

publicclassRaceCondition{privateintbalance=100;// 不安全的取款方法publicvoidwithdraw(intamount){if(balance>=amount){// 如果在这里线程被切换,可能导致超额取款balance-=amount;}}}

三、Java中的线程安全解决方案

1. 内置锁(synchronized):厨房的"专用令牌"

synchronized关键字就像厨房的专用令牌,只有拿到令牌的厨师才能使用特定厨具。

publicclassSafeCounter{privateintcount=0;publicsynchronizedvoidincrement(){count++;// 现在安全了!}}

底层原理:synchronized基于**监视器锁(Monitor)**实现,每个Java对象都有一个内置锁。线程进入同步代码前自动获取锁,退出时自动释放锁。

2. volatile关键字:餐厅的"中央公告板"

volatile确保变量的修改立即对其他线程可见,就像餐厅的中央公告板,任何更新都会立即被所有人看到。

publicclassVisibleFlag{privatevolatilebooleanstopRequested=false;publicvoidstop(){stopRequested=true;// 修改立即对所有线程可见}}

但注意:volatile不保证复合操作的原子性,它只解决可见性问题。

3. 原子类:无锁的"智能计数器"

Java的java.util.concurrent.atomic包提供了一系列原子类,如AtomicInteger,它们使用**CAS(Compare-And-Swap)**指令实现,无需锁也能保证原子性。

publicclassAtomicCounter{privateAtomicIntegercount=newAtomicInteger(0);publicvoidincrement(){count.incrementAndGet();// 原子操作,性能比synchronized更高}}

4. 并发集合:线程安全的"共享储物柜"

Java提供了多种线程安全的并发集合类:

  • ConcurrentHashMap:支持高并发的HashMap实现
  • CopyOnWriteArrayList:读多写少场景的理想选择
  • BlockingQueue:优秀的生产者-消费者实现工具

四、实战场景:如何选择正确的线程安全策略

场景1:计数器(高频更新)

// 推荐:AtomicLong(性能最佳)privateAtomicLongrequestCount=newAtomicLong();// 次选:synchronized(保证安全但性能较低)privatelongrequestCount=0;publicsynchronizedvoidincrement(){requestCount++;}

场景2:缓存(读多写少)

// 推荐:ConcurrentHashMap(并发读写性能均衡)privateConcurrentHashMap<String,Object>cache=newConcurrentHashMap<>();// 特殊情况:CopyOnWriteArrayList(读极多,写极少)privateCopyOnWriteArrayList<String>configList=newCopyOnWriteArrayList<>();

场景3:状态标志(简单状态控制)

// 推荐:volatile(简单可见性保证)privatevolatilebooleanshutdownRequested=false;// 不推荐:AtomicBoolean(过度复杂,volatile已足够)

五、线程安全的级别:从"不可变"到"线程对立"

根据线程安全程度,我们可以将类分为几个级别:

  1. 不可变(Immutable):像String、Long这样的类,状态创建后就不能改变,天生线程安全

  2. 无条件的线程安全:如ConcurrentHashMap,有足够的内部同步,无需外部同步。

  3. 有条件的线程安全:如Collections.synchronizedList返回的集合,迭代时需要外部同步。

  4. 非线程安全:如ArrayList、HashMap,需要客户端自己实现同步。

  5. 线程对立:即使外部同步,也无法保证线程安全(应避免)。

六、线程安全的最佳实践

  1. 优先使用不可变对象:不可变对象天生线程安全,是解决并发问题的最佳选择。

  2. 文档化线程安全保证:在代码文档中明确说明类的线程安全级别。

  3. 避免过度同步:同步范围过大可能导致性能问题甚至死锁。

  4. 谨慎使用公共锁对象:考虑使用私有锁对象防止拒绝服务攻击。

publicclassPrivateLock{privatefinalObjectlock=newObject();// 私有锁对象publicvoidsafeMethod(){synchronized(lock){// 外部无法干扰// 安全操作}}}

七、总结:线程安全的"终极秘诀"

线程安全不是魔法,而是建立在三个基石上:

  1. 原子性:操作要么完全执行,要么完全不执行
  2. 可见性:一个线程的修改对其他线程立即可见
  3. 有序性:程序按代码顺序执行(允许必要的重排序优化)

回到餐厅厨房的比喻,确保线程安全就像制定良好的厨房工作规则:为关键区域设立专用令牌(synchronized),设置中央公告板及时通知变化(volatile),以及建立明确的工作流程(原子操作)。

最重要的是,在编写并发代码时,不要依赖猜测,而要基于可靠的并发工具和明确的约定。多线程编程虽然复杂,但掌握了正确的方法和工具,我们就能编写出既安全又高效的程序。


参考文章:

  1. https://www.51cto.com/article/627460.html
  2. https://blog.csdn.net/u013773608/article/details/99752973
  3. https://blog.csdn.net/Coloured_Glaze/article/details/100635585
  4. https://blog.csdn.net/weixin_33893473/article/details/92415650
  5. https://blog.csdn.net/2301_78064339/article/details/131021135
  6. https://my.oschina.net/emacs_8710921/blog/17077058
  7. https://my.oschina.net/emacs_9455642/blog/18592766
  8. [深入讲解线程安全在值对象模式中的不可变性](https://blog.csdn.net/zhxup606/article/details/151683489

更多技术干货欢迎关注微信公众号科威舟的AI笔记~

【转载须知】:转载请注明原文出处及作者信息

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/6 2:12:07

SPFA算法

在图论的世界里&#xff0c;“最短路径” 是个高频需求 —— 比如从家到公司的最优路线、网络中数据传输的最短延迟。我们知道 Dijkstra 算法很经典&#xff0c;但它怕负权边&#xff1b;Bellman-Ford 算法能处理负权边&#xff0c;却慢得让人着急。今天要讲的 SPFA 算法&#…

作者头像 李华
网站建设 2026/1/29 12:12:21

高频Jmeter软件测试面试题

近期&#xff0c;有很多粉丝在催更关于Jmeter的面试题&#xff0c;索性抽空整理了一波&#xff0c;以下是一些高频Jmeter面试题&#xff0c;拿走不谢~ 一、JMeter的工作原理 JMeter就像一群将请求发送到目标服务器的用户一样&#xff0c;它收集来自目标服务器的响应以及其他统…

作者头像 李华
网站建设 2026/2/6 18:01:40

aliexpress 逆向分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01;部分python代码n231 cp.call(get231, …

作者头像 李华
网站建设 2026/2/7 5:31:54

腾讯滑块 collect分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01;部分python代码pow_cfg data["dat…

作者头像 李华
网站建设 2026/2/2 3:31:13

4、基础设施资源管理:提升云、虚拟和存储网络效率的关键

基础设施资源管理:提升云、虚拟和存储网络效率的关键 1. 数据基础设施管理 在当今数字化时代,信息服务的高效、灵活、可靠且经济的交付至关重要。支持信息服务交付的资源涵盖多个方面: - 硬件 :包括服务器、存储设备、输入/输出与网络连接设备以及桌面设备。 - 软件 …

作者头像 李华
网站建设 2026/2/1 12:22:20

从 Spring Boot 2.x 到 3.5.x + JDK21:一次完整的生产环境迁移实战

升级背景 在私有化部署过程中&#xff0c;客户使用安全扫描工具检测到大量安全漏洞&#xff0c;主要集中在&#xff1a; 框架版本过低&#xff1a;Spring Boot 2.1.6.RELEASE&#xff08;发布于 2019 年&#xff09;JDK 版本过旧&#xff1a;JDK 8&#xff08;缺乏最新安全补…

作者头像 李华