内存中的原子性
WangScaler: 一个用心创作的作者。声明:才疏学浅,如有错误,恳请指正。
原子性
原子性:不可分割,操作不能打断。
我们上节在对内存可见性造成影响的代码中讲述了可以使用volatile来保证内存的可见性,当工作内存的值变化了会立即刷新到主内存中,同时使其他线程的工作内存的值失效,从而从主内存中重新拉取新的值。那么多线程同时操作一个值,仅仅只有可见性,能得到我们预期的结果吗?
写个例子来看一下
示例
package com.wangscaler.jmm;
/**
* @author WangScaler
* @date 2021/8/4 13:49
*/
public class Atomicity {
volatile int num = 0;
public void add() {
this.num++;
}
public static void main(String[] args) {
Atomicity atomicity = new Atomicity();
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
atomicity.add();
}
}, "Add").start();
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
atomicity.add();
}
}, "Add2").start();
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("两个线程结束后最终的num值为" + atomicity.num);
}
}
正常来说,我们预期的结果应该是200000。然而当我们执行的时候却总是是少于这个数的,当然也是有几率达到预期值。为什么执行的结果大出意料呢?
this.num++;
的字节码如下(IDEA查看字节码,可参考往期文章从字节码讲解i++和++i的区别|8月更文挑战):
2 getfield #2 <com/wangscaler/jmm/Atomicity.num : I>
5 iconst_1
6 iadd
7 putfield #2 <com/wangscaler/jmm/Atomicity.num : I>
getfield从主内存读取num的值,iconst_1将值放到操作数栈位置一的位置,iadd进行++操作,putfield写回主内存。在多线程中,这四步中间可能会和其他线程交替执行。
- 假设当主内存为10的时候,线程Add读取了主内存的num的值10(Add执行字节码getfield)
- 紧接着Add2的线程也读取了主内存num的值10(Add2执行字节码getfield)
- 然后两个线程分别进行了+1操作(Add先执行字节码iadd,随后Add2执行字节码iadd),因为工作内存的数据变化了,又分别写入主内存。
- 假设线程Add先写入11;其后线程Add2也写入了11。(Add先执行字节码putfield,随后Add2执行字节码putfield)
- 这时就出现了我们非预期的结果,我们预期的是经过两个线程之后,值应该为12,现在的结果却是11。
- 多次出现这种情况那么最终的值肯定是低于200000。
如何解决原子性问题
以下的修改三次修改均是在上面示例的基础上进行修改的。
1、原子操作类(CAS)
将num
的类型由int
修改为AtomicInteger
;将add方法的num++;
修改为num.getAndIncrement();
如下所示:
package com.wangscaler.jmm;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author WangScaler
* @date 2021/8/4 13:49
*/
public class Atomicity {
AtomicInteger num = new AtomicInteger();
public void add() {
num.getAndIncrement();
}
}
为什么AtomicInteger会保证原子性呢,我们打开源码发现
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
继续查看getAndAddInt的源码
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
在这个getAndAddInt方法里是个循环,循环判断主存的值V和你预期的值A一样时,才会允许你将新值B写入主存,否则重新循环在主存获取值再次比较。(具体的细节可翻阅CAS,后期我会专门写什么是CAS,CAS操作包含三个操作数 :内存位置(V)、预期原值(A)和新值(B)。)
除了double、long之外的所有基本类型的读取或赋值也都是原子性操作。但是读取并赋值的操作不是原子性的例如我们常见的i++就不是原子性操作。
2、synchronized
给add方法使用关键字synchronized修饰。
public synchronized void add() {
this.num++;
}
使用这个关键字之后,就保证了操作不能打断。也就是说一个线程执行add方法时,需要等待add的所有字节码执行完之后,下一个线程才能执行。
- 当Add线程获得add方法的执行权之后,其他线程Add2执行到add方法时将阻塞。
- Add线程从主内存读取num,进行++操作,写回主内存
- Add2线程才能获得add方法的执行权。
从而保证了num的值达到我们预期的效果。synchronized就是给Add方法加了一把锁,所以我们也可以自己去实现这个锁。
3、Lock锁
package com.wangscaler.jmm;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author WangScaler
* @date 2021/8/4 13:49
*/
public class Atomicity {
volatile int num = 0;
Lock addLock = new ReentrantLock();
public void add() {
addLock.lock();
try {
num++;
} catch (Exception e) {
e.printStackTrace();
} finally {
addLock.unlock();
}
}
}
和synchronized一样我们给add方法加了一把锁,这样谁获取到权限谁才能执行。
总结
在保持原子性上,优先使用原子操作类(CAS),因为他是非阻塞的同步机制的乐观锁,而synchronized、Lock两种加锁的机制是阻塞的,是一种悲观的互斥锁,大大影响我们的效率。