共计 1685 个字符,预计需要花费 5 分钟才能阅读完成。
JMM 之原子性
不可分割,完整性。也就是说某个线程正在做某个具体业务时,两头不能够被加塞或者被宰割,须要具体实现,要么同时胜利,要么同时失败。
代码测试 volatile 是否保障原子性
咱们创立了 20 个线程,而后每个线程别离循环 1000 次,来调用 number++ 的办法
class MyData {
// 定义 int 变量
volatile int number = 0;
public void addPlusPlus() {number++;}
}
public class Test {public static void main(String[] args) {MyData myData = new MyData();
// 创立 20 个线程,线程外面进行 1000 次循环
for (int i = 0; i < 20; i++) {new Thread(() -> {for (int j = 0; j < 1000; j++) {myData.addPlusPlus();
}
}).start();}
/*
须要期待下面 20 个线程都执行结束后,再用 main 线程获得最终的后果
这里判断线程数是否大于 2,为什么是 2?因为默认有两个线程的,一个 main 线程,一个 gc 线程
*/
while (Thread.activeCount() > 2) {Thread.yield(); // yield 示意不执行
}
System.out.println("线程运行完后,number 的值为:" + myData.number);
}
}
线程执行结束后打印 number 的值,假如 volatile 保障原子性的话,那么最初输入的值应该是 20 * 1000 = 20000。
最终后果咱们会发现,number 输入的值并没有 20000,而且是每次运行的后果都不统一的,这阐明了volatile 润饰的变量不保障原子性。
为什么呈现数据失落
A 线程和 B 线程同时批改各自工作空间里的内容。因为可见性,须要从新写入内存,然而 A 线程在写入的时候,BB 线程也同时写入,导致 A 线程的写入操作被挂起,这样造成 B 线程的写入后,A 线程笼罩了 B 线程的值,造成了数据失落的问题。
咱们将一个简略的 n ++ 操作,针对 add()
这个办法的字节码文件进行剖析:
volatile int n = 0;
public void add() {n++;}
public void add();
Code:
0: aload_0
1: dup
2: getfield
5: iconst_1
6: iadd
7: putfield
10: return
咱们能发现 n ++ 这条命令,被拆分为 3 个指令
- 执行
getfield
从主内存拿到原始 n - 执行
iadd
进行加 1 操作 - 执行
putfileld
把累加后的值写回主内存
如果咱们没有加 synchronized
那么第一步就可能存在着,三个线程同时通过 getfield
命令,拿到内存中的 n 值。而后三个线程,各自在本人的工作内存中进行加 1 操作,然而他们并发执行 iadd
命令的时候,因为只能一个进行写,所以其余操作会被挂起。假如 A 线程,先进行了写操作,在写完后,volatile 的可见性应该通知其余两个线程,主内存的值被批改了。然而因为太快,其余两个线程,陆续执行 iadd
命令,这就造成了其余线程没有接管到主内存 n 的扭转,从而笼罩了原来的值,呈现写失落,这样也就让最终的后果少于 20000。
如何解决
因而阐明,在多线程环境下 n++
是非线程平安的,如何解决呢?
-
在办法上加上
synchronized
public synchronized void addPlusPlus() {number ++;}
引入 synchronized 关键字后,保障了该办法每次只可能一个线程进行拜访和操作,最终输入的后果也就为 20000。
-
为了解决 n ++,引入重量级的同步机制,有种杀鸡焉用牛刀的感觉。
咱们还能够应用 JUC 上面的 原子包装类 ,即 int 类型的 number,能够应用
AtomicInteger
来代替// 创立一个原子 Integer 包装类,默认为 0 AtomicInteger number = new AtomicInteger(); public void addAtomic(){number.getAndIncrement(); // 相当于 number++ }