在 Java 项目中,我们经常会遇到多线程并发问题,尤其是在生成唯一标识符的场景中。本文将结合实际项目案例,详细讲解如何使用 synchronized
解决多线程并发导致的任务编号重复问题。
问题背景
在我们的项目中,有一个创建任务的功能。每次创建任务时,系统会根据当前时间(年月日时分秒)生成一个任务编号,代码如下:
String taskNum=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
理论上,每次生成的任务编号应该是唯一的。然而,在高并发场景下,多个线程同时调用该接口时,可能会生成相同的任务编号,这显然是不合理的。如图
问题复现
为了验证问题,我添加了输出开始时间和结束时间,并且精确到毫秒,因为这个接口响应时间在100~300毫秒,如果精确到秒,出现在同一时间那是必然的,看如果是毫秒级别,能否就直接解决这个问题了,代码类似如下
/**
* 创建任务-提交
*
* @param dto 创建任务提交的数据对象
* @return
*/
@Override
@Transactional
public int addTask(TaskCreateDTO dto,String userId) {
System.out.println("开始"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
String taskNum=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
System.out.println("任务编号:"+taskNum);
//一些业务逻辑........
System.out.println("结束"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
return 1;
}
启动项目,并用APIfox进行并发测试,如图可以看到,即使精确到毫秒级,还是会生成出相同的编号,这说明在高并发场景下,多个线程同时进入了该方法。
问题分析
问题的根本原因是多个线程在同一时间调用了该方法,导致生成的任务编号相同。为了解决这个问题,我们需要确保同一时间只有一个线程可以执行该方法。
解决方案
在 Java 中,synchronized
关键字可以确保同一时间只有一个线程执行被修饰的方法。但需要注意的是,synchronized
的有效性依赖于方法所在类的实例作用域。
-
单例模式:如果类的作用域是单例(Spring 默认),则可以直接使用
synchronized
修饰方法。 -
多实例模式:如果类的作用域是多实例,则需要额外处理(例如使用类锁或静态锁)。
在我们的项目中,通过输出 this.hashCode()
验证了当前类是单例的:
System.out.println("当前实例哈希码: " + this.hashCode());
如上图,可以看到,每此在方法中输出的this的哈希码都是一样的,表示当前的实现类对象在整个程序中是单实例的,因此直接在方法上加上synchronized即可解决此问题
验证结果
代码如下
@Override
@Transactional
public synchronized int addTask(TaskCreateDTO dto,String userId) {
System.out.println("开始"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
String taskNum=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
System.out.println("任务编号:"+taskNum);
//一些业务逻辑........
System.out.println("结束"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
return 1;
}
再次使用 APIfox 进行并发测试,结果如下:
-
每次生成的任务编号都是唯一的。
-
多个线程依次执行该方法,避免了并发问题。
如图
注意事项
虽然 synchronized
可以有效解决并发问题,但在高并发场景下,可能会带来性能瓶颈。因此,建议在以下情况下使用:
-
并发量较低:适合并发量较小的场景。
-
方法执行时间较短:适合执行时间较短的方法。
-
需要严格保证线程安全:适合对数据一致性要求较高的场景。
如果并发量较大或方法执行时间较长,可以考虑其他优化方案,例如减小锁粒度、使用读写锁或无锁编程。
结语
通过幽络源本文的案例,我们深入了解了如何使用 synchronized
暂无评论内容