Java并发问题

问题引述:最近工作写代码涉及线程的时候,出现了一个bug,所以写这篇文章记录一下这个问题.

1.知识准备

Java虚拟机(JVM)是由堆内存、虚拟机栈、本地方法栈、方法区 、程序计数器、五部分组成

  • 堆内存:存放实例化对象,Java中new的对象就存放在堆内存中
  • 虚拟机栈;是线程私有的,生命周期与线程相同,存放局部变量,变量名等信息
  • 本地方法栈:是用来供Java调用本地服务(Native)用的
  • 方法区:是用来存放静态变量,缓存代码的地方
  • 程序计数器:可以看作当前线程所执行的命令指示器.

2.问题描述

案例:

java 复制代码
// 1. 从数据库查询当前任务状态
Task task = taskService.selectOne(wrapper);
// 2. 在内存中计算新的执行次数:当前次数 + 1
int num = task.getNum() + 1;
// 3. 设置新次数到内存对象
task.setNum(num);
// 4. 更新数据库记录
taskMapper.updateById(task);
  • 这段代码是直接在Java虚拟机中运行的,当同一时间多个线程执行这段代码时,任务的num就会混乱.原因在于每个线程操作的都是自己工作内存中的task对象副本(原始对象在堆中,但读取和修改都是发生在线程的上下文中),即一个线程对堆内存对象的修改对另一个线程不是立即可见的,且数据库的更新存在延迟.

举个例子: 假设任务Task还未执行过 线程A这时执行一次任务,首先它从堆内存读取一次num, num=num+1 得到 num=1 在它要更新数据库的时候 此时线程B 也来执行该任务, 由于线程A的num = 1 还未及时更新到数据库 导致 B从数据库中读出来的 num = 0.这时,线程A更新完 => num=1,线程B更新完 => num=1 导致次数记录错误.

3.解决方法:

  • 找资料后发现,我们使用的数据库是 MYSQL(默认存储引擎InnoDB) 提供了行级锁机制.InnoDB行级锁又分为两种 共享锁(Read Lock)与排他锁(Write Lock), 这里就不展开谈了,顾名思义 当执行UPDATE . DELETE 语句时InnoDB默认会自动给WHERE条件匹配的行添加排他锁(Write Lock),一个事务持有某行的Write Lock时,其他事务无法再对该行操作.
  • 所以我们可以把这个并发问题交给数据库,即当线程A 去更新记录时,线程B也要去更新 这时候由于Write Lock的存在导致线程B需要等线程A执行完后才能去更新,这样就可以解决问题.具体的sql如下:
java 复制代码
 //原理 使用这条sql语句 在数据库执行的是原子操作,数据库保证 1.读值,2.计算,3.更新这三个操作不可分隔
 update task set num = num + 1 where task_id = #{taskId}

后记:bug相对简单,但是找资料还是有点困难 如果觉得对你有帮助就点赞支持一下!