线程安全问题
线程共享带来的问题
线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了。
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1;i < 5000; i++){
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 1;i < 5000; i++){
count--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count的值是{}",count);
}
输出:
22:08:40.196 c.Test17 [main] - count的值是-332
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
当 CPU 时间片分给 t1 线程时,t1 线程去读取变量值为 0 并且执行 ++ 的操作,如上在字节码自增操作中,当 t1 执行完自增,还没来得急将修改后的值存入静态变量时,假如线程的时间片用完了,并且 CPU 将时间片分配给 t2 线程,t2 线程拿到时间片执行自减操作,并且将修改后的值存入静态变量,此时 count 的值为 -1,但是当 CPU 将时间片分给经历了上下文切换的 t1 线程时,t1 将修改后的值存入静态变量,此时 counter 的值为 1,覆盖了 t2 线程执行的结果,出现了丢失更新,这就是多线对共享资源读取的问题。
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
但多线程下这 8 行代码可能交错运行:
出现负数的情况:
出现正数的情况:
临界区
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
临界资源:
一次仅允许一个进程使用的资源成为临界资源
临界区:
访问临界资源的代码块
例如:
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件:
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件.
为了避免临界区的竞态条件发生(解决线程安全问题):
- 阻塞式的解决方案:synchronized,lock
- 非阻塞式的解决方案:原子变量
管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现)