今天看到同事写的一段代码,感觉怪怪的,代码如下。Springboot 默认是单例模式,所有HTTP请求共用同一个对象,成员属性 testSingleton
就会被共享,这样好像会导致响应出问题吧。但是,这个项目是已经上线的了,为什么没人发现这个问题呢?我开始怀疑我自己。。。
@RestController
public class TestController {
String testSingleton = "操作成功";
@GetMapping("/testSingleton1")
public String testSingleton1() {
testSingleton = "操作成功";
return testSingleton;
}
@GetMapping("/testSingleton2")
public String testSingleton2(){
testSingleton = "操作失败";
return testSingleton;
}
}
所以,我决定要验证一下。为了更加直观,我改造了一下,加个极端环境Thread.sleep(10*1000)
:
@RestController
public class TestController {
String testSingleton = "操作成功";
@GetMapping("/testSingleton1")
public String testSingleton1() throws InterruptedException {
testSingleton = "操作成功";
Thread.sleep(10*1000);
return testSingleton;
}
@GetMapping("/testSingleton2")
public String testSingleton2(){
testSingleton = "操作失败";
return testSingleton;
}
}
先访问 /testSingleton1
再迅速访问 /testSingleton2
,测试结果如下:
操作失败
操作失败
显然,这个响应是有问题的,至此,打消了自己的怀疑。
------------------------------------------- 分割线 -------------------------------------------
那为什么没有用户发现这个问题呢?我想,原因有二:
- 大部分都是赋值后直接响应,没有
Thread.sleep(10*1000)
这么极端的场景,线程占用的情况不容易被复现 - 才几百个用户,而且该项目使用的频率比较低(心痛自己3秒钟,小丑竟是我自己。。。)
虽然问题目前还没暴露出来,但是问题确实存在,该如何解决呢?
- 成员变量改为局部变量,局部变量不会引起线程的抢占,这是人们常说的无状态的。
@RestController
public class TestController {
@GetMapping("/testSingleton1")
public String testSingleton1() throws InterruptedException {
String testSingleton = "操作成功";
Thread.sleep(10*1000);
return testSingleton;
}
@GetMapping("/testSingleton2")
public String testSingleton2(){
String testSingleton = "操作失败";
return testSingleton;
}
}
测试结果如下:
操作失败
操作成功 // 睡了10秒,后响应
- 不使用单例模式,用原型模式来代替
@Scope("prototype")
,这样每次处理HTTP请求都会new一个新的对象,testSingleton
就不会被共享,此时的成员变量也是无状态的。
@RestController
@Scope("prototype")
public class TestController {
String testSingleton = "操作成功";
@GetMapping("/testSingleton1")
public String testSingleton1() throws InterruptedException {
testSingleton = "操作成功";
Thread.sleep(10*1000);
return testSingleton;
}
@GetMapping("/testSingleton2")
public String testSingleton2(){
testSingleton = "操作失败";
return testSingleton;
}
}
测试结果如下:
操作失败
操作成功 // 睡了10秒,后响应
扩展:原型模式下,成员变量改为静态变量呢?
@RestController
@Scope("prototype")
public class TestController {
public static String testSingleton = "操作成功";
@GetMapping("/testSingleton1")
public String testSingleton1() throws InterruptedException {
testSingleton = "操作成功";
Thread.sleep(10*1000);
return testSingleton;
}
@GetMapping("/testSingleton2")
public String testSingleton2(){
testSingleton = "操作失败";
return testSingleton;
}
}
测试结果如下:
操作失败
操作失败
这个很好理解,静态变量被所有线程共享。
总结:在 Springboot 中,定义成员变量或者静态变量时要特别注意,是否会出现线程抢占的问题。