一、共享问题与分析

两个线程对初始值为0的静态变量(共享资源、共享区)一个做自增,一个做自减,各做5000次,结果是0吗?

  1. public class ThreadProblem {
  2. static int counter = 0;//共享区
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t1 = new Thread(()->{
  5. for(int i=0;i<5000;i++){
  6. counter++;
  7. }
  8. },"t1");
  9. Thread t2 =new Thread(()->{
  10. for (int i=0;i<5000;i++){
  11. counter--;
  12. }
  13. },"t2");
  14. t1.start();
  15. t2.start();
  16. //主线程等待线程t1和线程t2执行完毕
  17. t1.join();
  18. t2.join();
  19. System.out.println("counter:"+counter);
  20. }
  21. }

答案显然不一定总是0,结果可能是正数、负数、零,这是分时系统自身的弊端,注意这不是CPU调度的问题,因为即使CPU调度问题,线程t1也是执行了5000次自加操作,线程t2也是执行了5000次自减操作,只不过是执行的顺序不确定,但最后的值理应是0,最主要的原因是:Java中的自加和自减操作不是原子操作(更新的值一次存入内存中,指令无法再细分),所以在执行机器指令的时候可能会存在CPU时间片用完,而没有成功地将结果值写入静态变量的情况,具体解释如下:

对于i++而言(i为静态变量),实际会产生如下的JVM字节码指令:

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

对于i—而言(i为静态变量),实际会产生如下的JVM字节码指令:

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

所以对于Java内存模型来说,完成静态变量的自增,自减需要在内存和工作内存中进行数据交换:
image.png
学过JVM后可知,实际上线程的工作内存是栈帧内的局部变量表和操作数栈,主内存指的是方法区,因为共享静态变量存入方法区中。

如果是单线程,以上8行代码是顺序执行(不会交错),没有问题:
image.png

但实际上,因为线程执行有固定时间(时间片),时间片结束后当前线程会立马进入就绪态,所以可能出现以下问题,仅拿出现负数的情况举例:
image.png

线程1执行对counter变量的减法操作,当要将自减后的值写入方法区中时,时间片用尽,最后一条“写入方法区”的指令存入线程1的TCB的PC中,CPU切换到线程2上,线程2将自增后的值写入静态变量中,切换到线程1,线程1将之前的-1值写入静态变量中,这样即出现了写入负值的情况。

二、临界区与竞态条件

临界区 Critical Section:

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源

    1. 若多个线程读**共享资源**其实也没有问题<br /> 在多个线程对共享资源读写操作(又读又写)时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,即访问共享资源的那段代码称为临界区

例如,下面代码中的临界区为:

  1. static int counter = 0;//共享资源
  2. //对共享资源访问,故为临界区
  3. static void increment(){
  4. counter++;
  5. }
  6. //临界区
  7. static void decrement(){
  8. counter--;
  9. }

竞态条件 Race Condition:

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

三、问题总结

一句话,多线程访问共享资源,因为时间片缘故,导致多线程的指令序列混乱,从而共享资源出现异常,注意此问题一定是出现在多线程访问同一共享资源的情况下。