一、原子性问题
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",i);
}
1、问题解析
例如对于 i++ 而言(i 为静态变量),i++从字节码来看并不是原子性的操作
实际会产生如下的 JVM 字节码指令:
getstatici// 获取静态变量i的值
iconst_1// 准备常量1
iadd// 自增
putstatici// 将修改后的值存入静态变量i
2、java内存模型
image.png
3、总结
因为i++最后会在字节码中生成多条操作指令,那么这些指令由于线程上下文的切换,随时都有可能被打断。
如下图所示:线程1执行到iadd执行时,这时并没有执行putstatic指令,内存中i还是0,这时上下文发生了切换,线程2读取了i值,i值依旧为0。线程2执行完之后将-1写入到了内存,i值为-1。线程1这时又获取了cpu的时间片,恢复执行。又将i=1赋值给了内存。这时i又等于了1;
image.png
二、内存可见性问题
- 每个线程都有自己的工作内存,线程对共享对象的操作首先是在自己的工作内存中进行,然后再写回主内存。这就可能导致一个线程对共享对象的修改不能及时被其他线程看到。
○ 例如,线程 A 修改了共享对象的某个字段值,但线程 B 可能由于还在使用自己工作内存中的旧值副本,而不知道这个修改,从而导致不一致的结果。
三、竞争条件问题
- 多个线程同时访问和修改共享对象时,可能会出现竞争条件。竞争条件是指多个线程对共享资源的访问没有按照预期的顺序执行,导致结果不可预测。
○ 比如两个线程同时对一个计数器进行递增操作,如果没有正确的同步机制,可能会导致计数器的值没有正确地反映两个线程的操作。
○ 假设有线程 T1 和 T2,它们都要将共享变量counter的值加 1。首先,counter的值为 5。T1 读取counter的值为 5 到自己的工作内存,此时 T2 也读取counter的值为 5。然后 T1 将其工作内存中的counter加 1 变为 6,并写回主内存。接着 T2 也将其工作内存中的counter加 1 变为 6 并写回主内存。最终,预期的结果应该是 7,但实际上却是 6,这就是竞争条件导致的错误结果。
总结:
分两种情况
第一种:
并非正在意义上的并行,由于受限于CPU内核的数据量,线程的被打断了。非原子的操作,可能导致打断前读取到值被其他线程修改,而线程恢复执行后,是根据程序计数器标记的位置继续执行,没有办法更新到已经发生了变更的值。导致了线程安全问题。
第二种:
正在的并行,两个cpu内核同时执行两个线程,两个线程同时读取一个变量,加入到自己的工作内存中。然后对自己工作内存中的变量进行修改。修改之后写回主内存,这时就出现一个线程的写入值会被另一个线程的写入值覆盖掉。