小故事引入

  • 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快

image.png

  • 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
  • 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)

image.png

  • 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
  • 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
  • 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
  • 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
  • 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上
  • 计算流程是这样的:

image.png

  • 但是由于分时系统,有一天还是发生了事故
  • 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
  • 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地到一边待着去了(上下文切换)
  • 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果-1 写入笔记本
  • 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写入了笔记本

image.png

  • 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0

通过上面的小故事,其实多个线程同时工作存在的问题就是资源共享的问题,上面的记事本中的被修改的值就是一个共享资源,如果不能确保这个共享资源在多个线程每时每刻都保持同步,就会发生上述的事故

线程同步问题在Java中的体现:

  1. @Slf4j(topic = "c.ShareTest")
  2. public class ShareTest {
  3. static int counter = 0;
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread t1 = new Thread(() -> {
  6. for (int i = 0; i < 5000; i++) {
  7. counter++;
  8. }
  9. }, "t1");
  10. Thread t2 = new Thread(() -> {
  11. for (int i = 0; i < 5000; i++) {
  12. counter--;
  13. }
  14. }, "t2");
  15. t1.start();
  16. t2.start();
  17. //main线程等待t1、t2的执行完毕才去打印counter
  18. t1.join();
  19. t2.join();
  20. //理想值是0,但是由于存在线程同步问题,而导致达不到理想值
  21. log.debug("{}",counter);
  22. }
  23. }

image.png
可以看到,对于共享资源,多个线程操作共享资源的时候就会存在线程同步问题。

事故分析

为什么上面的静态变量 counter 自增会导致线程的不同步问题,要分析这个就需要从 JVM 指令的角度分析了。i++这行语句,会对于 JVM 中的四中指令:

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 准备常量1
  3. iadd // 自增
  4. putstatic i // 将修改后的值存入静态变量i

而i—同样也是:

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 准备常量1
  3. isub // 自减
  4. putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
image.png
如果是单线程执行,完全是不会存在不同步问题的:
image.png
但是如果是两个线程去操作共享资源i的话,就可能存在线程同步问题:
image.png
image.png
因此,导致了i出现-1和+1的情况都有可能产生,导致i不是理想的0。通过上面的分析,一个程序运行多个线程是没有问题的,这多个线程【读】共享资源也没有问题,而问题就出在多个线程对共享资源进行【读写】操作时发生指令交错,不能保证多个线程中的 i 保持一致。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为【临界区】。多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了【竞态条件】。
image.png