Java-01-源码篇-并发编程-资源竞争
目录
一,统计案例
1.1 原生代码
1.2 追加volatile关键字
1.3 追加 synchronized 关键字
二,总结
2.1 关键字总结
2.2 原子性,可见性,有序性
2.3【总结】
三,系列文章推荐
一,统计案例
在上一章《多线程基础讲解》中提到多线程的基础使用,生命周期,常用API等基本使用的知识点。并且我们可以体会到每个子任务可以交给不同的线程执行,实现真正的“分而治之”。
在多线程进行任务分批处理的时候。如何做到资源在实时更新的同时也能被其他线程及时感知到。多线程在资源共享与资源竞争提供哪些机制确保数据的一致性。通过一个案例讲解。
1.1 原生代码
/**
* @author toast
* @time 2025/3/19
* @remark
*/
class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子操作
}
public int getCount() {
return count;
}
}
public class RaceConditionExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程同时修改 count 变量追加10000次
Thread t1 = new Thread(() -> {
IntStream.range(0, 10000).forEach(i -> counter.increment());
System.out.println("T1执行完毕");
});
Thread t2 = new Thread(() -> {
IntStream.range(0, 10000).forEach(i -> counter.increment());
System.out.println("T2执行完毕");
});
t1.start();
t2.start();
t1.join(); // main主线程等待t1线程执行完毕
t2.join(); // main主线程等待t2线程执行完毕
System.out.println("最终 count 值:" + counter.getCount());
}
}
输出结果:
T2执行完毕
T1执行完毕
最终 count 值:17829
输出结果并非20000。而是一个 17829。这是为什么?我们可以先讲一下这个赋值过程我们就知道了。
从统计算计的赋值流程,我们可以了解到线程T1,T2分别先从主内存(Main memory)读取 count = 10; 之后又各自运行各自的统计业务。在这过程之中线程之间的工作内存又是不共享,相当于两个线程同时读取 10
并执行 +1
,最终 count
只增加了一次,变成 11
❌。
1.2 追加volatile
关键字
在1.1 的案例当中,我们发现线程每一次都需要进行从主内存进行一次数据读取到线程的工作内存之中,现在通过 volatile 关键字,让其线程直接使用主内存(Main momery)的数据。代码如下
package com.toast.javase.source.thread;
import java.util.stream.IntStream;
/**
* @author toast
* @time 2025/3/19
* @remark
*/
class VolatileCounter {
private volatile int count = 0;
public void increment() {
count++; // 仍然不是原子操作
}
public int getCount() {
return count;
}
}
public class VolatileExample {
public static void main(String[] args) throws InterruptedException {
VolatileCounter counter = new VolatileCounter();
Thread t1 = new Thread(() -> {
IntStream.range(0, 10000).forEach(i -> counter.increment());
System.out.println("T1执行完毕");
});
Thread t2 = new Thread(() -> {
IntStream.range(0, 10000).forEach(i -> counter.increment());
System.out.println("T2执行完毕");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终 count 值:" + counter.getCount());
}
}
输出结果
T1执行完毕
T2执行完毕
最终 count 值:18499
现在追加了 volatile 关键字,使其线程之间的count是可见的,就算数据修改了,其他线程也是可以感知到了。但是为什么还不是 20000?观察如下示意图
count 追加了 volatile 关键字,确实实现了线程之间的数据可见性,但没有解决count数据覆盖导致统计失败。其原因就是T1, T2 在争抢资源的时候没有一个界限。按道理第二次计算,不管是T1执行第二次计算,还是T2执行第二次计算,其读取到的值应该是count = 11; 而非 count = 10;
而这个界限,或者说临界点的开放与关闭在 JVM里面提供一个关键字,这个关键字就是synchronized,也被称为“内置锁”( Intrinsic Lock )。
1.3 追加 synchronized 关键字
package com.toast.javase.source.thread;
import java.util.stream.IntStream;
/**
* @author liuwq
* @time 2025/3/19
* @remark
*/
class SynchronizedCounter {
private volatile int count = 0;
// 现在是线程安全的
public synchronized void increment() {count++;}
public int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread t1 = new Thread(() -> {
IntStream.range(0, 10000).forEach(i -> counter.increment());
System.out.println("T1执行完毕");
});
Thread t2 = new Thread(() -> {
IntStream.range(0, 10000).forEach(i -> counter.increment());
System.out.println("T2执行完毕");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终 count 值:" + counter.getCount()); // 结果一定是 20000
}
}
输出结果
T1执行完毕
T2执行完毕
最终 count 值:20000
赋值流程图如下
二,总结
2.1 关键字总结
从上面的统计案例当中,我们知道了关键字 volatile 以及 synchronized 关键字的作用。
volatile | 作用一:保证了数据在线程之间的可见性, 作用二:防止CPU指令重排,上面案例并未明显体现 |
synchronized | 作用一:保证了代码的互斥性,同一时间只允许一个线程执行。 作用二:保证了可见性,进入synchronized 代码之前,会从主内存读取数据,代码结束,退出synchronized代码,必须回写数据到主 内存。(问大家一个问题,如果数据没有 volatile 关键字修饰,还会从主内存读取数据吗?当然会了,只不过是读取完之后存储在自己的工作内存空间里,计算完再回写过去。只不过整个过程数据没有可见性) 作用三:保证了原子性,其代码内的操作是不可分割的 |
2.2 原子性,可见性,有序性
其实在上面的 2.1 关键字总结上就已经总结出来了原子性,可见性(数据可见性),以及有序性
原子性 | 原子性,顾名思义,借用“原子”不可分割的性质。表示 一个操作要么全部执行成功,要么全部失败,不能被其他线程打断 |
可见性 | 可见性表示数据在线程之间是否可见,在修改变量数据时其他线程是否能感知到 |
有序性 | 有序性指的是程序执行的顺序符合代码编写的逻辑顺序,但由于 JVM JIT 编译器优化、CPU 指令重排(Instruction Reordering),实际执行顺序可能不同。 |
2.3【总结】
对于保证原子性,JVM提供内置锁 synchronized 关键字,以及JDK1.5之后的Lock 接口。
对于可见性,提供volatile 关键字,确保数据在线程之间的可见性
对于有序性,也是 volatile 通过禁止 指令重排 来确保有序性。指令重排是 CPU 为提高性能而对程序指令执行顺序进行调整的技术。指令重排可能会导致线程看到不一致的状态,从而产生竞态条件。
三,系列文章推荐
最后,如果这篇文章对你有帮助,欢迎 点赞👍、收藏📌、关注👀!
我会持续分享 Java、Spring Boot、MyBatis-Plus、微服务架构 相关的实战经验,记得关注,第一时间获取最新文章!🚀
这篇文章是 【Java SE 17源码】系列 的一部分,详细地址:
java SE 17 源码篇_吐司呐的博客-CSDN博客
记得 关注我,后续还会更新更多高质量技术文章!
你在实际开发中遇到过类似的问题吗?
欢迎在评论区留言交流,一起探讨 Java 开发的最佳实践! 🚀