浅谈Jmm的应用:并发安全-从volatile+粗/细锁到原子类

Java 内存模型(JMM)是多线程编程里绕不开的核心,决定了线程怎么跟共享数据打交道。今天咱们就从最基础的玩法聊起,一步步推演,找到问题,再优化到如今主流的高效方案。过程尽量轻松易懂,但逻辑一点不含糊,走着瞧!


JMM 是啥?内存咋分的

JMM 把内存分成两部分,简单粗暴:

  • 主内存:线程共享的地盘,放的是"官方"数据,像对象啊、静态变量啊。
  • 工作内存:每个线程的小天地,里面存的是从主内存抄过来的数据副本。

线程要干活,得先从主内存把数据拉到自己的工作内存,用完再写回去。打个比方,主内存是大账本,工作内存是每个线程的小抄本。

举个栗子 :有个变量 int num = 0 在主内存里。线程 A 抄一份到自己小抄本,改成 1,但没写回去;线程 B 也抄了一份,还是 0。这就暴露了 JMM 的关键挑战:线程间的数据咋保持一致?


最原始的办法:啥也不干

先看一段代码:

java 复制代码
public class NumberGame {
    public static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(() -> { for (int i = 0; i < 1000; i++) num++; });
        Thread b = new Thread(() -> { for (int i = 0; i < 1000; i++) num++; });
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(num);
    }
}

你可能觉得 num 最后会是 2000,毕竟两个线程各加了 1000 次。但跑几回,结果可能是 1975、1998,咋回事呢?原因出在 num++ 不是一气呵成的,它分三步:读 num、加 1、写回去。线程 A 和 B 可能同时读到 100,都加到 101,写回去还是 101,结果就丢了数据。

这办法的毛病

  1. 看不见 :A 改了 num,B 完全不知道,还是用老数据。
  2. 抢着干:两个线程一块儿改,互相覆盖,谁也不让谁。
  3. 纯靠天:没啥控制,随缘运行,效率和正确性都不靠谱。

第一步优化:加点料------volatile

咱们给 num 加个 volatile 试试:

java 复制代码
public static volatile int num = 0;

volatile 是干啥的?它能:

  • 保证看得到 :一个线程改了 num,立刻刷到主内存,其他线程读的时候也得从主内存拿最新值。
  • 别乱动顺序:编译器和 CPU 不会随便调整指令顺序。

咋做到的?JVM 在背后搞了点小动作,写数据时加个"写墙",确保改完就更新主内存;读时加个"读墙",逼着从主内存拿新鲜数据。

但再跑代码,num 还是不到 2000。为啥?volatile 只管让大家看到最新值,可 num++ 这三步还是分开走,线程抢着写照样会丢数据。

这步的短板

  • 抢夺没解决:数据还是会被覆盖。
  • 效果有限:性能开销不大,但问题没根治。

再升级:锁起来------synchronized

换个思路,把 num++ 塞进 synchronized 里:

java 复制代码
public class NumberGame {
    public static int num = 0;
    public static synchronized void addOne() { num++; }

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(() -> { for (int i = 0; i < 1000; i++) addOne(); });
        Thread b = new Thread(() -> { for (int i = 0; i < 1000; i++) addOne(); });
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(num); // 稳稳的 2000
    }
}

这回成了!synchronized 是咋搞定的?

  • 底层逻辑:靠的是监视器锁。JVM 在代码里加了锁的开关,线程进来先锁门,改完数据再开门。
  • 效果:既让改动立刻被别人看到,又保证一次只能一个线程干活。

新麻烦

  1. 有点慢:锁太重,线程得排队,2 个线程还行,20 个就拖后腿了。
  2. 锁得粗:整个方法都锁住,其实没必要锁那么多。

逼近高招:精细化与高效化

从啥也不管到加锁,咱们再往厉害的方向走几步。

1. 锁得更聪明

别锁整个方法,改用对象锁,只锁关键部分:

java 复制代码
public class NumberGame {
    private int num = 0;
    private final Object lock = new Object();

    public void addOne() {
        synchronized(lock) { num++; }
    }
}

好处:锁的范围小了,线程不用等太久,效率高了不少。现在很多框架都喜欢这么干。

2. 不锁也行------原子类

直接上 AtomicInteger,扔掉锁:

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class NumberGame {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(() -> { for (int i = 0; i < 1000; i++) num.incrementAndGet(); });
        Thread b = new Thread(() -> { for (int i = 0; i < 1000; i++) num.incrementAndGet(); });
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(num.get()); // 2000,没毛病
    }
}

咋实现的 ?靠 CAS(比较并交换),直接用 CPU 的原子指令,效率高还不堵车。 接轨主流:Java 的并发包里全是这种玩法,像线程池、并发容器都靠它。

3. 锁的进化

JVM 也没闲着,给 synchronized 加了优化:

  • 偏向锁:线程少时几乎没开销。
  • 轻量级锁:竞争稍微多点时用。
  • 重量级锁:真打起来才上。

更现代的路子 :用 ReentrantLock,比 synchronized 灵活,能搞公平锁、条件等待,功能更强。


总结一下

JMM 把内存分成主内存和工作内存,线程间数据同步靠 volatile 管可见性,synchronized 管独占性。从啥也不干到加锁,再到原子类和锁的精细优化,线程安全一步步稳如老狗。两个线程各加 1000 次,最后就是 2000,数字准,方案扎实,拿去用绝对靠谱!

相关推荐
欢乐少年190434 分钟前
SpringBoot集成Sentry日志收集-3 (Spring Boot集成)
spring boot·后端·sentry
浪九天4 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
uhakadotcom5 小时前
Apache CXF 中的拒绝服务漏洞 CVE-2025-23184 详解
后端·面试·github
uhakadotcom5 小时前
CVE-2025-25012:Kibana 原型污染漏洞解析与防护
后端·面试·github
uhakadotcom5 小时前
揭秘ESP32芯片的隐藏命令:潜在安全风险
后端·面试·github
uhakadotcom5 小时前
Apache Camel 漏洞 CVE-2025-27636 详解与修复
后端·面试·github
uhakadotcom5 小时前
OpenSSH CVE-2025-26466 漏洞解析与防御
后端·面试·github
uhakadotcom5 小时前
PostgreSQL的CVE-2025-1094漏洞解析:SQL注入与元命令执行
后端·面试·github
zhuyasen5 小时前
Go语言开发实战:app库实现多服务启动与关闭的优雅方案
后端·go
ITlinuxP6 小时前
2025最新Postman、Apipost和Apifox API 协议与工具选择方案解析
后端·测试工具·postman·开发工具·apipost·apifox·api协议