今天配合同事一起和外部系统进行联调测试,其实昨天我们已经成功走通了一遍。今天同事得到对方反馈系统可能有一个潜在的问题,所以就又严格地联调了一遍。这一遍,我也是一遍又一遍地盯日志,关注告警邮件。正是在这一遍联调系统中,我发现了一个小问题,程序里面一封相同内容的通知邮件,几乎是同一个时间发送了两次。
通过分析日志外加比对代码,终于找到问题,是遇到线程安全问题引起的。下面和大家分享一下,伪代码:
public class Email{ private String emailContent; @Async public void sendEmail(){ "send email" + emailContent; } } //Email类的实例被Spring容器管理,只有一个实例emailInstance。 public class TestDemo{ @Resource Email emailInstance; @Async public void testMethod(){ lock(); //处理业务逻辑,同时操作emailInstance.emailContent; operationBs(); emailInstance. sendEmail(); unlock(); } }
问题原因复述:testMethod方法为防止多线程同时操作,在此处使用了锁,而为了确保发送邮件不影响主程序的执行时间,所以在调用sendEmail方法时,另行开辟了异步线程处理。正是这个方法允许了异步线程处理,所以当第一个线程释放锁后,而异步线程尚未完成邮件发送时,第二个获得锁并执行sendEmail方法时,开启了新的异步线程处理邮件发送,这样就出现了多个线程共同使用emailInstance对象,并同时操作emailInstance的成员变量emailContent。
因为如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时,他们对该成员变量的操作是彼此影响的(也就是说一个线程对成员变量的改变会影响到另一个线程)。这样就会出现上述发送相同内容邮件的情况。
理论讲解:JAVA 多线程同时调用单例模式的对象时,该对象中的对成员变量与局部变量是否会受到多个线程的影响?
当多个线程对同一个单例对象的同一个成员变量进行操作时,它们对该成员变量的操作是彼此互相影响的(也就是说一个线程对该成员变量的改变会影响到另一个线程) 。对于成员变量的操作,可以使用ThreadLocal来保证线程安全。
而多线程调用同一个对象的同一个方法时,每个线程会对方法内部的局部变量都是在线程自己独立的内存区域进行的,也就是说在每个线程的独立内存中都一个局部变量的拷贝,这样一个线程对同一个单例对象的同一方法内的局部变量的改变就不会影响到其他线程中的局部变量,所以是线程安全的。
总结,局部变量不会受多线程影响,成员变量会受到多线程影响。多个线程调用同一个对象的同一个方法时,如果方法里无成员变量,那么不受任何影响;如果方法里有成员变量,只有读操作,不受影响,存在写操作,考虑多线程影响值。
解决方案:
- 简化方案,去掉Email中sendEmail方法上@Async注解,也就是说将处理业务逻辑和发送邮件合并为同步操作,这样就保证了同一时间只会有一个线程操作成员变量,这样也就避免了线程安全问题。但注意这是以牺牲核心业务逻辑的处理时间来换得安全。
- 将操作成员变量,调整为操作局部变量。
参考文档: