Spring Controller单例与线程安全那些事儿
程序员文章站
2023-09-20 12:46:46
目录 "单例(singleton)作用域" "原型(Prototype)作用域" "多个HTTP请求在Spring控制器内部串行还是并行执行方法?" "实现单例模式并模拟大量并发请求,验证线程安全" "附录:Spring Bean作用域" 单例(singleton)作用域 每个添加@RestCont ......
目录
- 单例(singleton)作用域
- 原型(prototype)作用域
- 多个http请求在spring控制器内部串行还是并行执行方法?
- 实现单例模式并模拟大量并发请求,验证线程安全
- 附录:spring bean作用域
单例(singleton)作用域
每个添加@restcontroller或@controller的控制器,默认是单例(singleton),这也是spring bean的默认作用域。
下面代码示例参考了building a restful web service,该教程搭建基于spring boot的web项目,源代码可参考
greetingcontroller.java代码如下:
package com.example.controller; import java.util.concurrent.atomic.atomiclong; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestparam; import org.springframework.web.bind.annotation.restcontroller; @restcontroller public class greetingcontroller { private static final string template = "hello, %s!"; private final atomiclong counter = new atomiclong(); @getmapping("/greeting") public greeting greeting(@requestparam(value = "name", defaultvalue = "world") string name) { greeting greet = new greeting(counter.incrementandget(), string.format(template, name)); system.out.println("id=" + greet.getid() + ", instance=" + this); return greet; } }
我们使用http基准工具来生成大量http请求。在终端输入如下命令来测试:
wrk -t12 -c400 -d10s http://127.0.0.1:8080/greeting
在服务端的标准输出中,可以看到类似日志。
id=162440, instance=com.example.controller.greetingcontroller@368b1b03 id=162439, instance=com.example.controller.greetingcontroller@368b1b03 id=162438, instance=com.example.controller.greetingcontroller@368b1b03 id=162441, instance=com.example.controller.greetingcontroller@368b1b03 id=162442, instance=com.example.controller.greetingcontroller@368b1b03 id=162443, instance=com.example.controller.greetingcontroller@368b1b03 id=162444, instance=com.example.controller.greetingcontroller@368b1b03 id=162445, instance=com.example.controller.greetingcontroller@368b1b03 id=162446, instance=com.example.controller.greetingcontroller@368b1b03
日志中所有greetingcontroller实例的地址都是一样的,说明多个请求对同一个 greetingcontroller 实例进行处理,并且它的atomiclong类型的counter字段正按预期在每次调用时递增。
原型(prototype)作用域
如果我们在@restcontroller注解上方增加@scope("prototype")注解,使bean作用域变成原型作用域,其它内容保持不变。
... @scope("prototype") @restcontroller public class greetingcontroller { ... }
服务端的标准输出日志如下,说明改成原型作用域后,每次请求都会创建新的bean,所以返回的id始终是1,bean实例地址也不同。
id=1, instance=com.example.controller.greetingcontroller@2437b9b6 id=1, instance=com.example.controller.greetingcontroller@c35e3b8 id=1, instance=com.example.controller.greetingcontroller@6ea455db id=1, instance=com.example.controller.greetingcontroller@3fa9d3a4 id=1, instance=com.example.controller.greetingcontroller@3cb58b3
多个http请求在spring控制器内部串行还是并行执行方法?
如果我们在greeting()方法中增加休眠时间,来看下每个http请求是否会串行调用控制器里面的方法。
@restcontroller public class greetingcontroller { private static final string template = "hello, %s!"; private final atomiclong counter = new atomiclong(); @getmapping("/greeting") public greeting greeting(@requestparam(value = "name", defaultvalue = "world") string name) throws interruptedexception { thread.sleep(1000); // 休眠1s greeting greet = new greeting(counter.incrementandget(), string.format(template, name)); system.out.println("id=" + greet.getid() + ", instance=" + this); return greet; } }
还是使用wrk来创建大量请求,可以看出即使服务端的方法休眠1秒,导致每个请求的平均延迟达到1.18s,但每秒能处理的请求仍达到166个,证明http请求在spring mvc内部是并发调用控制器的方法,而不是串行。
wrk -t12 -c400 -d10s http://127.0.0.1:8080/greeting running 10s test @ http://127.0.0.1:8080/greeting 12 threads and 400 connections thread stats avg stdev max +/- stdev latency 1.18s 296.41ms 1.89s 85.22% req/sec 37.85 38.04 153.00 80.00% 1664 requests in 10.02s, 262.17kb read socket errors: connect 155, read 234, write 0, timeout 0 requests/sec: 166.08 transfer/sec: 26.17kb
实现单例模式并模拟大量并发请求,验证线程安全
单例类的定义:singleton.java
package com.demo.designpattern; import java.util.concurrent.atomic.atomicinteger; public class singleton { private volatile static singleton singleton; private int counter = 0; private atomicinteger atomicinteger = new atomicinteger(0); private singleton() { } public static singleton getsingleton() { if (singleton == null) { synchronized (singleton.class) { if (singleton == null) { singleton = new singleton(); } } } return singleton; } public int getunsafenext() { return ++counter; } public int getunsafecounter() { return counter; } public int getsafenext() { return atomicinteger.incrementandget(); } public int getsafecounter() { return atomicinteger.get(); } }
测试单例类并创建大量请求并发调用:singletontest.java
package com.demo.designpattern; import java.util.*; import java.util.concurrent.*; public class singletontest { public static void main(string[] args) { // 定义可返回计算结果的非线程安全的callback实例 callable<integer> unsafecallabletask = () -> singleton.getsingleton().getunsafenext(); runtask(unsafecallabletask); // unsafe counter may less than 1000, i.e. 984 system.out.println("current counter = " + singleton.getsingleton().getunsafecounter()); // 定义可返回计算结果的线程安全的callback实例(基于atomicinteger) callable<integer> safecallabletask = () -> singleton.getsingleton().getsafenext(); runtask(safecallabletask); // safe counter should be 1000 system.out.println("current counter = " + singleton.getsingleton().getsafecounter()); } public static void runtask(callable<integer> callabletask) { int cores = runtime.getruntime().availableprocessors(); executorservice threadpool = executors.newfixedthreadpool(cores); list<callable<integer>> callabletasks = new arraylist<>(); for (int i = 0; i < 1000; i++) { callabletasks.add(callabletask); } map<integer, integer> frequency = new hashmap<>(); try { list<future<integer>> futures = threadpool.invokeall(callabletasks); for (future<integer> future : futures) { frequency.put(future.get(), frequency.getordefault(future.get(), 0) + 1); //system.out.printf("counter=%s\n", future.get()); } } catch (interruptedexception | executionexception e) { e.printstacktrace(); } threadpool.shutdown(); } }
附录:spring bean作用域
范围 |
描述 |
---|---|
singleton(单例) | (默认值)将每个spring ioc容器的单个bean定义范围限定为单个对象实例。 换句话说,当您定义一个bean并且其作用域为单例时,spring ioc容器将为该bean所定义的对象创建一个实例。该单例存储在单例beans的高速缓存中,并且对该命名bean的所有后续请求和引用都返回该高速缓存的对象。 |
prototype(原型) | 将单个bean定义的作用域限定为任意数量的对象实例。 每次对特定bean发出请求时,bean原型作用域都会创建一个新bean实例。也就是说,将bean注入到另一个bean中,或者您可以调用容器上的getbean()方法来请求它。通常,应将原型作用域用于所有有状态bean,将单例作用域用于无状态bean。 |
request | 将单个bean定义的范围限定为单个http请求的生命周期。也就是说,每个http请求都有一个在单个bean定义后创建的bean实例。仅在web-aware的spring applicationcontext上下文有效。 |
session | 将单个bean定义的范围限定为http session的生命周期。仅在基于web的spring applicationcontext上下文有效。 |
application | 将单个bean定义的范围限定为servletcontext的生命周期。仅在基于web的spring applicationcontext上下文有效。 |
websocket | 将单个bean定义的作用域限定为websocket的生命周期。仅在基于web的spring applicationcontext上下文有效。 |