一、共享问题与分析
两个线程对初始值为0的静态变量(共享资源、共享区)一个做自增,一个做自减,各做5000次,结果是0吗?
public class ThreadProblem {
static int counter = 0;//共享区
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
counter++;
}
},"t1");
Thread t2 =new Thread(()->{
for (int i=0;i<5000;i++){
counter--;
}
},"t2");
t1.start();
t2.start();
//主线程等待线程t1和线程t2执行完毕
t1.join();
t2.join();
System.out.println("counter:"+counter);
}
}
答案显然不一定总是0,结果可能是正数、负数、零,这是分时系统自身的弊端,注意这不是CPU调度的问题,因为即使CPU调度问题,线程t1也是执行了5000次自加操作,线程t2也是执行了5000次自减操作,只不过是执行的顺序不确定,但最后的值理应是0,最主要的原因是:Java中的自加和自减操作不是原子操作(更新的值一次存入内存中,指令无法再细分),所以在执行机器指令的时候可能会存在CPU时间片用完,而没有成功地将结果值写入静态变量的情况,具体解释如下:
对于i++而言(i为静态变量),实际会产生如下的JVM字节码指令:
getstatic i //获取静态变量i的值
iconst_1 //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量i
对于i—而言(i为静态变量),实际会产生如下的JVM字节码指令:
getstatic i //获取静态变量i的值
iconst_1 //准备常量1
isub //自增
putstatic i //将修改后的值存入静态变量i
所以对于Java内存模型来说,完成静态变量的自增,自减需要在内存和工作内存中进行数据交换:
学过JVM后可知,实际上线程的工作内存是栈帧内的局部变量表和操作数栈,主内存指的是方法区,因为共享静态变量存入方法区中。
如果是单线程,以上8行代码是顺序执行(不会交错),没有问题:
但实际上,因为线程执行有固定时间(时间片),时间片结束后当前线程会立马进入就绪态,所以可能出现以下问题,仅拿出现负数的情况举例:
线程1执行对counter变量的减法操作,当要将自减后的值写入方法区中时,时间片用尽,最后一条“写入方法区”的指令存入线程1的TCB的PC中,CPU切换到线程2上,线程2将自增后的值写入静态变量中,切换到线程1,线程1将之前的-1值写入静态变量中,这样即出现了写入负值的情况。
二、临界区与竞态条件
临界区 Critical Section:
- 一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
若多个线程读**共享资源**其实也没有问题<br /> 在多个线程对共享资源读写操作(又读又写)时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,即访问共享资源的那段代码称为临界区
例如,下面代码中的临界区为:
static int counter = 0;//共享资源
//对共享资源访问,故为临界区
static void increment(){
counter++;
}
//临界区
static void decrement(){
counter--;
}
竞态条件 Race Condition:
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
三、问题总结
一句话,多线程访问共享资源,因为时间片缘故,导致多线程的指令序列混乱,从而共享资源出现异常,注意此问题一定是出现在多线程访问同一共享资源的情况下。