本文纲要
- 概述
- 原子性概念与问题引入
2.1 什么是原子性
2.2 多线程操作共享数据的问题
2.3volatile不能保证原子性 - 使用
synchronized实现原子性 AtomicInteger原子类
4.1AtomicInteger简介与构造
4.2 常用方法演示
4.3 使用AtomicInteger解决冰淇淋问题
4.4 内存解析:CAS算法与自旋
4.5 源码解析- 悲观锁与乐观锁
概述
在现代Java开发中,多线程并发是非常常见的场景,而原子性是保证数据安全的核心基石之一。本文通过一个“送冰淇淋”的生动案例,循序渐进地剖析原子性的概念、问题、解决方案,并深入解析volatile、synchronized、AtomicInteger及CAS算法的原理。内容贴近实际开发,适合Java初学者一次性掌握并发安全的基础。
原子性概念与问题引入
1 ) 什么是原子性
用一个小故事来理解:你送给心仪的小女孩一个冰淇淋,这个动作包含两个步骤:你送出与她接收。如果只有“送”没有“收”,或者只有“收”没有“送”,这件事就不完整。
原子性就是指:在一次或多次操作中,所有的操作要么全部执行并且不受任何干扰,要么全部不执行,这些步骤是一个不可分割的整体。
再比如,小贾给小皮汇款1000元,包含“小贾扣款”和“小皮到账”两步。如果第一步成功第二步失败,钱就消失了;如果第一步失败第二步成功,钱就凭空产生。所以原子性要求这多个操作要么同时成功,要么同时失败。
2 ) 多线程操作共享数据的问题
在Java多线程中,若多个线程同时操作共享变量,就可能破坏原子性。我们用代码来模拟送冰淇淋的场景:100个线程,每个线程送出100个冰淇淋,理论上总共是10000个。
项目结构:
threadatom/
└── src/
└── com/
└── wb/
└── threadatom/
├── AtomDemo.java
└── MyAtomThread.java
MyAtomThread.java
package com.wb.threadatom;
public class MyAtomThread implements Runnable {
private volatile int count = 0; // 送冰淇淋的数量
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 1.从共享数据中读取数据到本线程栈中
// 2.修改本线程栈中变量副本的值
// 3.把本线程栈中变量副本的值赋值给共享数据
count++;
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
AtomDemo.java
package com.wb.threadatom;
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
运行多次,会发现总数量经常是9999、9997等,并非10000。为什么?因为count++操作在内存中并非原子操作,它实际包含三步:
- 从共享内存读取count的值到线程本地栈;
- 在线程栈内对副本加1;
- 将新值写回共享内存。
在多线程环境下,CPU时间片可能在任何一步被切换。例如:线程A读取count=100,本地副本变成101;此时线程B抢到CPU,读到的count仍为100,也自增为101。两个线程各自加1,最终写回去的还是101,而不是102。这就是count++不具备原子性导致的数据错误。
3 ) volatile不能保证原子性
或许你会想:在count前加上volatile关键字能否解决?volatile能保证变量的可见性(每次读取都是主存中的最新值),但它不能保证操作的原子性。即便线程B在读取时能看到线程A修改后的值,如果线程A的写操作还未完成,线程B读到的仍然是旧值。以下内存图可以说明:
因此,volatile 只能保证读到最新值,但阻止不了多个线程同时读写造成的覆盖问题。要解决原子性,必须引入锁或原子类。
使用synchronized实现原子性
synchronized同步代码块可以保证同一时间只有一个线程执行临界区代码,从而保证原子性。
1 ) 项目结构:
threadatom2/
└── src/
└── com/
└── wb/
└── threadatom2/
├── AtomDemo.java
└── MyAtomThread.java
MyAtomThread.java
package com.wb.threadatom2;
public class MyAtomThread implements Runnable {
private volatile int count = 0; // 送冰淇淋的数量
private Object lock = new Object();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
count++;
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
}
AtomDemo.java
package com.wb.threadatom2;
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
我们只创建了一个MyAtomThread对象并传递给100个线程,因此锁对象lock是唯一的。无论运行多少次,输出总数始终为10000。原因:线程进入synchronized块前必须先获取锁,其他线程即使抢到CPU也无法进入,只有当前线程执行完count++并释放锁后,下一个线程才能继续操作。这保证了count++的原子性。
不过,synchronized每次都需要加锁、判断和释放锁,性能开销较大。有没有更轻量的方式呢?
AtomicInteger原子类
从JDK 1.5开始,java.util.concurrent.atomic包提供了一系列原子类,可以高效、线程安全地更新变量,无需显式加锁。其中最常用的就是AtomicInteger。
1 ) AtomicInteger简介与构造
AtomicInteger提供了两个构造方法:
AtomicInteger():创建初始值为0的原子整数。AtomicInteger(int initialValue):创建指定初始值的原子整数。
项目结构:
threadatom3/
└── src/
└── com/
└── wb/
└── threadatom3/
├── MyAtomIntergerDemo1.java
└── MyAtomIntergerDemo2.java
MyAtomIntergerDemo1.java
package com.wb.threadatom3;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomIntergerDemo1 {
public static void main(String[] args) {
// 无参构造,默认值为0
AtomicInteger ac = new AtomicInteger();
System.out.println(ac); // 输出0
// 带初始值的构造
AtomicInteger ac2 = new AtomicInteger(10);
System.out.println(ac2); // 输出10
}
}
2 ) 常用方法演示
AtomicInteger 的核心方法及作用如下:
| 方法 | 说明 |
|---|---|
int get() | 获取当前值 |
int getAndIncrement() | 以原子方式将当前值加1,返回自增前的值 |
int incrementAndGet() | 以原子方式将当前值加1,返回自增后的值 |
int addAndGet(int delta) | 以原子方式将当前值加上delta,返回相加后的值 |
int getAndSet(int newValue) | 以原子方式设置为newValue,并返回旧值 |
我们将这些方法逐个演示(原代码中多个注释块分别为独立示例,这里逐一展示):
MyAtomIntergerDemo2.java
package com.wb.threadatom3;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomIntergerDemo2 {
public static void main(String[] args) {
// --- get() 演示 ---
AtomicInteger ac1 = new AtomicInteger(10);
System.out.println(ac1.get()); // 输出10
// --- getAndIncrement() 演示 ---
AtomicInteger ac2 = new AtomicInteger(10);
int andIncrement = ac2.getAndIncrement();
System.out.println(andIncrement); // 输出10(自增前的值)
System.out.println(ac2.get()); // 输出11(当前值)
// --- incrementAndGet() 演示 ---
AtomicInteger ac3 = new AtomicInteger(10);
int i1 = ac3.incrementAndGet();
System.out.println(i1); // 输出11(自增后的值)
System.out.println(ac3.get()); // 输出11
// --- addAndGet() 演示 ---
AtomicInteger ac4 = new AtomicInteger(10);
int i2 = ac4.addAndGet(20);
System.out.println(i2); // 输出30
System.out.println(ac4.get()); // 输出30
// --- getAndSet() 演示 ---
AtomicInteger ac5 = new AtomicInteger(100);
int andSet = ac5.getAndSet(20);
System.out.println(andSet); // 输出100(旧值)
System.out.println(ac5.get()); // 输出20
}
}
3 ) 使用AtomicInteger解决冰淇淋问题
用AtomicInteger替换原有的volatile int count,并调用incrementAndGet(),无需任何锁即可保证线程安全。
项目结构:
threadatom4/
└── src/
└── com/
└── wb/
└── threadatom4/
├── AtomDemo.java
└── MyAtomThread.java
MyAtomThread.java
package com.wb.threadatom4;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomThread implements Runnable {
// 使用原子类替代 volatile int count
AtomicInteger ac = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 原子自增,并获取自增后的值
int count = ac.incrementAndGet();
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
AtomDemo.java
package com.wb.threadatom4;
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
多次运行,结果始终为10000,高效且线程安全。incrementAndGet()在底层利用了CAS(Compare-And-Swap)算法,避免了重量级锁。
4 ) 内存解析:CAS算法与自旋
CAS全称Compare-And-Swap,是CPU级别的原子指令,包含三个操作数:
- 内存值V:共享变量的当前值;
- 旧的预期值A:线程从内存中读取时记录的值;
- 要修改的值B:希望将V更新成的值。
工作流程:比较V与A,若相等则说明没有其他线程修改过,将V更新为B;若不相等,则说明已被其他线程修改,本次修改失败,重新读取最新的V作为A,再次计算B并重试。这个重试过程称为自旋。
以下用内存图演示两个线程通过CAS完成自增的过程:
最终值变为102,两次自增均成功。CAS通过不断自旋保证最终一致性,且没有加锁的开销。
5 ) 源码解析
我们看一下AtomicInteger的incrementAndGet()源码,验证上面的逻辑。
① incrementAndGet()方法:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
它调用了Unsafe类的getAndAddInt,然后对返回值加1,表示自增后的结果。参数this是当前的AtomicInteger对象,valueOffset是内部变量value的内存偏移量,1表示加1。
② getAndAddInt方法(简化版):
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 读取内存最新值作为旧值v
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
getIntVolatile(o, offset):从主存获取对象o在偏移量offset处的当前值,记为旧值v。compareAndSwapInt(o, offset, v, v + delta):比较当前内存值是否仍等于v。如果相等,则更新为v+delta并返回true,循环结束;如果不相等,返回false,循环继续(自旋),重新读取最新值。
所以整个过程与前面内存图完全吻合:读取旧值 → 计算新值 → CAS比较并交换 → 失败则自旋重试。compareAndSwapInt是native方法,直接调用CPU指令,保证了“比较+交换”的原子性。
悲观锁与乐观锁
使用synchronized和CAS都能保证共享数据的安全,但两者的设计哲学截然不同:
| 特性 | synchronized(悲观锁) | CAS(乐观锁) |
|---|---|---|
| 基本思想 | 每次操作前认为一定有其他线程会修改,因此直接加锁,阻塞其他线程 | 假设没有其他线程修改,不加锁,只在更新时检查是否被修改过 |
| 实现方式 | JVM内置锁,线程阻塞/唤醒 | CPU原子指令,自旋重试 |
| 是否阻塞线程 | 是,未获得锁的线程进入阻塞状态 | 否,失败后通过循环重试,不阻塞 |
| 性能开销 | 上下文切换开销较大,适合写操作多的场景 | 自旋消耗CPU,适合读多写少、冲突较少的场景 |
| 示例 | synchronized(lock){ ... } | AtomicInteger.incrementAndGet() |
二者在实际开发中各有适用领域。对于简单计数、标记等操作,优先考虑原子类和CAS;对于复杂业务逻辑,可能需要显式锁来保证整体一致性。理解它们的区别,能帮助你在并发编程时做出更合适的选择。
总结
我们从原子性的基本概念出发,经历了volatile的局限、synchronized的保障、AtomicInteger的高效解决方案,并深入CAS原理与源码,最后对比了悲观锁与乐观锁。
这些知识是Java并发编程的基石,希望能帮助你打下扎实的基础。


被折叠的 条评论
为什么被折叠?



