Java研究院 Java研究院

人非生而知之者,知之,而后知不知。

目录
Spring单例bean的线程安全问题
/      

Spring单例bean的线程安全问题

问题重现

在Spring中,bean的注入默认是单例,也就是说一个对象,只会实例化一个对象,该对象的成员变量全局共享。为了更好的说明,举个例子:

@RestController
@RequestMapping("/test/springBean")
public class SpringBeanController {


    private Integer i = 0;
    private Integer j = 0;

    @PostMapping("/v1")
    public String test() {

        i++;
        Random r = new Random();
        try {
            Thread.sleep(r.nextInt(50));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        j = i + 2;
        try {
            Thread.sleep(r.nextInt(50));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        i = i - 1;

        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        String s = "测试方法1 " + date + " " + i.toString() + "," + j.toString();
        System.out.println(s);
        return s;
    }


}

为了模拟并发,用jmeter添加线程组来模拟并发,并发数量为15,同时请求v1、v2方法。如下图:
并发30次

实际执行结果如下:

并发30次

根据输出结果可以看出,真实得到的j值,是完全不符合预期的。

解决方法

@Scope("prototype")注解

在Controller上加上@Scope("prototype")注解后,该Controller已经不是单例对象了,一味这每次请求都会重新实例化一个对象,那成员变量也就不存在共享的说法了,也就不会存在线程安全问题,加上该注解后同样并发30次的效果如下:

image.png

利用ThreadLocal

ThreadLocal的原理,此处不再赘述。直接上代码:

@RestController
@RequestMapping("/test/springBean")
public class SpringBeanController {

    private static ThreadLocal<Integer> var = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    private Integer j = 0;

    @PostMapping("/v1")
    public String test() {
        var.set(var.get() + 1);

        Random r = new Random();
        try {
            Thread.sleep(r.nextInt(50));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        j = var.get() + 2;
        try {
            Thread.sleep(r.nextInt(50));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        var.set(var.get() + 2);

        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        String s = "测试方法1 " + date + " " + var.get().toString() + "," + j.toString();
        System.out.println(s);
        return s;
    }

}

示例代码中,删除了变量i,取而代之的是ThreadLocal变量var。并且给了var设置了默认值为0;而在v1方法中,直接对var变量做操作。实际执行结果如下图:

并发10次

避免使用成员变量

基于本问题上述场景,如果将i、j设置为方法内的局部变量,则不存在这个问题,实际开发中,也大多是勇敢这种方式来避免问题。


才疏学浅,难免有坑,有坑也不填
标题:Spring单例bean的线程安全问题
地址:https://studying.icu/articles/2021/09/22/1632301638276.html