SpringMVC中出现的线程安全问题分析

作者 : 开心源码 本文共22285个字,预计阅读时间需要56分钟 发布时间: 2022-05-12 共143人阅读

(ps:前几个星期发生的事情)之前同事跟我说不要用@Autowired方式注入HttpServletRequest(ps:我们的代码之前使用的是第2种方式)。同事的意思大概是注入的HttpServletRequest对象是同一个而且存在线程安全问题。我保持质疑的态度,看了下源码,证实了@Autowired方式不存在线程安全问题,而@ModelAttribute方式存在线程安全问题。

观看本文章之前,最好看一下我上一篇写的文章:
1.通过循环引使用问题来分析Spring源码
2.你真的理解Spring MVC解决请求流程吗?

public abstract class BaseController {    @Autowired    protected HttpSession httpSession;    @Autowired    protected HttpServletRequest request;}
public abstract class BaseController1 {    protected HttpServletRequest request;    protected HttpServletResponse response;    protected HttpSession httpSession;    @ModelAttribute    public void init(HttpServletRequest request,                     HttpServletResponse response,                     HttpSession httpSession) {        this.request = request;        this.response = response;        this.httpSession = httpSession;    }}

线程安全测试

@RequestMapping("/test")@RestControllerpublic class TestController extends BaseController {    @GetMapping("/1")    public void test1() throws InterruptedException {//        System.out.println("thread.id=" + Thread.currentThread().getId());//        System.out.println("thread.name=" + Thread.currentThread().getName());//        ServletRequestAttributes servletRequestAttributes =//                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());////        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();        TimeUnit.SECONDS.sleep(10);//        System.out.println("base.request=" + request);        System.out.println("base.request.name=" + request.getParameter("name"));    }    @GetMapping("/2")    public void test2() throws InterruptedException {//        System.out.println("thread.id=" + Thread.currentThread().getId());//        System.out.println("thread.name=" + Thread.currentThread().getName());//        ServletRequestAttributes servletRequestAttributes =//                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());////        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();//        System.out.println("base.request=" + request);        System.out.println("base.request.name=" + request.getParameter("name"));    }    @InitBinder    public void initBinder(WebDataBinder binder) {        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");        binder.registerCustomEditor(Date.class, new CustomDateEditor(sdf, true));    }}

通过JUC的CountDownLatch,模拟同一时刻100个并发请求。

public class Test {    public static void main(String[] args) {        CountDownLatch start = new CountDownLatch(1);        CountDownLatch end = new CountDownLatch(100);        CustomThreadPoolExecutor customThreadPoolExecutor = new CustomThreadPoolExecutor(                100, 100, 0L,                TimeUnit.SECONDS,                new ArrayBlockingQueue<Runnable>(100)        );        for (int i = 0; i < 100; i++) {            final int finalName = i;            CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(                    new Runnable() {                        @Override                        public void run() {                            try {                                start.await();                                HttpUtil.get("http://localhost:8081/test/2?name=" + finalName);                            } catch (Exception ex) {                                ex.printStackTrace();                            } finally {                                end.countDown();                            }                        }                    }            , "success");            customThreadPoolExecutor.submit(task);        }        start.countDown();        try {            end.await();        } catch (InterruptedException ex) {            ex.printStackTrace();        }        customThreadPoolExecutor.shutdown();    }}

通过观看base.request.name的值并没有null值和存在值重复的现象,很一定的说@Autowired注入的HttpServletRequest不存在线程安全问题。

base.request.name=78base.request.name=20base.request.name=76base.request.name=49base.request.name=82base.request.name=12base.request.name=80base.request.name=91base.request.name=92base.request.name=30base.request.name=28base.request.name=36base.request.name=41base.request.name=73base.request.name=29base.request.name=2base.request.name=81base.request.name=43base.request.name=35base.request.name=22base.request.name=6base.request.name=27base.request.name=17base.request.name=70base.request.name=65base.request.name=84base.request.name=14base.request.name=54base.request.name=67base.request.name=19base.request.name=21base.request.name=66base.request.name=11base.request.name=53base.request.name=9base.request.name=72base.request.name=64base.request.name=0base.request.name=44base.request.name=89base.request.name=77base.request.name=48base.request.name=1base.request.name=8base.request.name=74base.request.name=46base.request.name=88base.request.name=26base.request.name=24base.request.name=62base.request.name=61base.request.name=51base.request.name=96base.request.name=33base.request.name=45base.request.name=5base.request.name=95base.request.name=68base.request.name=60base.request.name=56base.request.name=42base.request.name=57base.request.name=10base.request.name=55base.request.name=90base.request.name=47base.request.name=97base.request.name=40base.request.name=85base.request.name=86base.request.name=69base.request.name=98base.request.name=13base.request.name=32base.request.name=37base.request.name=4base.request.name=23base.request.name=50base.request.name=38base.request.name=59base.request.name=99base.request.name=71base.request.name=25base.request.name=58base.request.name=34base.request.name=7base.request.name=93base.request.name=31base.request.name=3base.request.name=39base.request.name=75base.request.name=94base.request.name=83base.request.name=63base.request.name=79base.request.name=16base.request.name=52base.request.name=15base.request.name=87base.request.name=18

很显著发现base.request.name的值存在null或者者重复的现象,说明通过@ModelAttribute注入的HttpServletRequest存在线程安全问题。

base.request.name=97base.request.name=59base.request.name=63base.request.name=14base.request.name=82base.request.name=49base.request.name=86base.request.name=13base.request.name=99base.request.name=29base.request.name=45base.request.name=85base.request.name=8base.request.name=35base.request.name=69base.request.name=70base.request.name=16base.request.name=21base.request.name=74base.request.name=20base.request.name=34base.request.name=23base.request.name=96base.request.name=19base.request.name=67base.request.name=15base.request.name=27base.request.name=43base.request.name=39base.request.name=47base.request.name=87base.request.name=71base.request.name=41base.request.name=38base.request.name=nullbase.request.name=31base.request.name=32base.request.name=76base.request.name=55base.request.name=75base.request.name=93base.request.name=nullbase.request.name=56base.request.name=1base.request.name=18base.request.name=89base.request.name=65base.request.name=10base.request.name=78base.request.name=nullbase.request.name=80base.request.name=24base.request.name=88base.request.name=88base.request.name=44base.request.name=53base.request.name=58base.request.name=61base.request.name=60base.request.name=37base.request.name=92base.request.name=42base.request.name=11base.request.name=68base.request.name=72base.request.name=91base.request.name=79base.request.name=33base.request.name=66base.request.name=54base.request.name=40base.request.name=94base.request.name=46base.request.name=83base.request.name=17base.request.name=64base.request.name=26base.request.name=90base.request.name=7base.request.name=62base.request.name=57base.request.name=73base.request.name=98base.request.name=30base.request.name=6base.request.name=2base.request.name=28base.request.name=5base.request.name=95base.request.name=9base.request.name=3base.request.name=51base.request.name=4base.request.name=52base.request.name=12base.request.name=25base.request.name=36base.request.name=84base.request.name=81base.request.name=50

源码分析

1.在Spring容器初始化中,refresh()方法会调使用postProcessBeanFactory(beanFactory);。它是个模板方法,在BeanDefinition被装载后(所有BeanDefinition被加载,但是没有bean被实例化),提供一个修改beanFactory容器的入口。这里还是贴下AbstractApplicationContext中的refresh()方法吧。

    @Override    public void refresh() throws BeansException, IllegalStateException {        synchronized (this.startupShutdownMonitor) {            // 1.Prepare this context for refreshing.            prepareRefresh();            // 2.Tell the subclass to refresh the internal bean factory.            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();            // 3.Prepare the bean factory for use in this context.            prepareBeanFactory(beanFactory);            try {                // 4.Allows post-processing of the bean factory in context subclasses.                postProcessBeanFactory(beanFactory);                // 5.Invoke factory processors registered as beans in the context.                invokeBeanFactoryPostProcessors(beanFactory);                // 6.Register bean processors that intercept bean creation.                registerBeanPostProcessors(beanFactory);                // 7.Initialize message source for this context.                initMessageSource();                // 8.Initialize event multicaster for this context.                initApplicationEventMulticaster();                // 9.Initialize other special beans in specific context subclasses.                onRefresh();                //10. Check for listener beans and register them.                registerListeners();                // 11.Instantiate all remaining (non-lazy-init) singletons.                finishBeanFactoryInitialization(beanFactory);                //12. Last step: publish corresponding event.                finishRefresh();            }            catch (BeansException ex) {                if (logger.isWarnEnabled()) {                    logger.warn("Exception encountered during context initialization - " +                            "cancelling refresh attempt: " + ex);                }                // Destroy already created singletons to avoid dangling resources.                destroyBeans();                // Reset 'active' flag.                cancelRefresh(ex);                // Propagate exception to caller.                throw ex;            }            finally {                // Reset common introspection caches in Spring's core, since we                // might not ever need metadata for singleton beans anymore...                resetCommonCaches();            }        }    }

2.因为postProcessBeanFactory是模板方法,它会被子类AbstractRefreshableWebApplicationContext重写。在AbstractRefreshableWebApplicationContext的postProcessBeanFactory()做以下几件事情。

1.注册ServletContextAwareProcessor。
2.注册需要忽略的依赖接口ServletContextAwareServletConfigAware
3.注册Web应使用的作使用域和环境配置信息。

    @Override    protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {        beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext, this.servletConfig));        beanFactory.ignoreDependencyInterface(ServletContextAware.class);        beanFactory.ignoreDependencyInterface(ServletConfigAware.class);        WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);        WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);    }
  1. WebApplicationContextUtils中的registerWebApplicationScopes(),beanFactory注册了request,application,session,globalSession作使用域,也注册了需要处理的依赖:ServletRequest、ServletResponse、HttpSession、WebRequest。
    public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory, ServletContext sc) {        beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());        beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope(false));        beanFactory.registerScope(WebApplicationContext.SCOPE_GLOBAL_SESSION, new SessionScope(true));        if (sc != null) {            ServletContextScope appScope = new ServletContextScope(sc);            beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);            // Register as ServletContext attribute, for ContextCleanupListener to detect it.            sc.setAttribute(ServletContextScope.class.getName(), appScope);        }        beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());        beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());        beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());        beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());        if (jsfPresent) {            FacesDependencyRegistrar.registerFacesDependencies(beanFactory);        }    }

4.RequestObjectFactory, ResponseObjectFactory, SessionObjectFactory都实现了ObjectFactory的接口,注入的值其实是getObject()的值。

    /**     * Factory that exposes the current request object on demand.     */    @SuppressWarnings("serial")    private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {        @Override        public ServletRequest getObject() {            return currentRequestAttributes().getRequest();        }        @Override        public String toString() {            return "Current HttpServletRequest";        }    }    /**     * Factory that exposes the current response object on demand.     */    @SuppressWarnings("serial")    private static class ResponseObjectFactory implements ObjectFactory<ServletResponse>, Serializable {        @Override        public ServletResponse getObject() {            ServletResponse response = currentRequestAttributes().getResponse();            if (response == null) {                throw new IllegalStateException("Current servlet response not available - " +                        "consider using RequestContextFilter instead of RequestContextListener");            }            return response;        }        @Override        public String toString() {            return "Current HttpServletResponse";        }    }    /**     * Factory that exposes the current session object on demand.     */    @SuppressWarnings("serial")    private static class SessionObjectFactory implements ObjectFactory<HttpSession>, Serializable {        @Override        public HttpSession getObject() {            return currentRequestAttributes().getRequest().getSession();        }        @Override        public String toString() {            return "Current HttpSession";        }    }    /**     * Factory that exposes the current WebRequest object on demand.     */    @SuppressWarnings("serial")    private static class WebRequestObjectFactory implements ObjectFactory<WebRequest>, Serializable {        @Override        public WebRequest getObject() {            ServletRequestAttributes requestAttr = currentRequestAttributes();            return new ServletWebRequest(requestAttr.getRequest(), requestAttr.getResponse());        }        @Override        public String toString() {            return "Current ServletWebRequest";        }    }

5.很显著,我们从getObject()中获取的值是从绑定当前线程的RequestAttribute中获取的,内部实现是通过ThreadLocal去完成的。看到这里,你应该明白了一点点。

    private static final ThreadLocal<RequestAttributes> requestAttributesHolder =            new NamedThreadLocal<RequestAttributes>("Request attributes");    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =            new NamedInheritableThreadLocal<RequestAttributes>("Request context");
    private static ServletRequestAttributes currentRequestAttributes() {        RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();        if (!(requestAttr instanceof ServletRequestAttributes)) {            throw new IllegalStateException("Current request is not a servlet request");        }        return (ServletRequestAttributes) requestAttr;    }
    public static RequestAttributes currentRequestAttributes() throws IllegalStateException {        RequestAttributes attributes = getRequestAttributes();        if (attributes == null) {            if (jsfPresent) {                attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();            }            if (attributes == null) {                throw new IllegalStateException("No thread-bound request found: " +                        "Are you referring to request attributes outside of an actual web request, " +                        "or processing a request outside of the originally receiving thread? " +                        "If you are actually operating within a web request and still receive this message, " +                        "your code is probably running outside of DispatcherServlet/DispatcherPortlet: " +                        "In this case, use RequestContextListener or RequestContextFilter to expose the current request.");            }        }        return attributes;    }
    public static RequestAttributes getRequestAttributes() {        RequestAttributes attributes = requestAttributesHolder.get();        if (attributes == null) {            attributes = inheritableRequestAttributesHolder.get();        }        return attributes;    }

6.我们再来捋一捋@Autowired注入HttpServletRequest对象的过程。这里以HttpServletRequest对象注入举例。首先调使用DefaultListableBeanFactory中的findAutowireCandidates()方法,判断autowiringType类型能否和requiredType类型一致或者者是autowiringType能否是requiredType的父接口(父类)。假如满足条件的话,我们会从resolvableDependencies中通过autowiringType(对应着上文的ServletRequest)拿到autowiringValue(对应着上文的RequestObjectFactory)。而后调使用AutowireUtils.resolveAutowiringValue()对我们的ObjectFactory进行解决。

    protected Map<String, Object> findAutowireCandidates(            String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {        String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(                this, requiredType, true, descriptor.isEager());        Map<String, Object> result = new LinkedHashMap<String, Object>(candidateNames.length);        for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {            if (autowiringType.isAssignableFrom(requiredType)) {                Object autowiringValue = this.resolvableDependencies.get(autowiringType);                autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);                if (requiredType.isInstance(autowiringValue)) {                    result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);                    break;                }            }        }        for (String candidate : candidateNames) {            if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {                addCandidateEntry(result, candidate, descriptor, requiredType);            }        }        if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {            // Consider fallback matches if the first pass failed to find anything...            DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();            for (String candidate : candidateNames) {                if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {                    addCandidateEntry(result, candidate, descriptor, requiredType);                }            }            if (result.isEmpty()) {                // Consider self references as a final pass...                // but in the case of a dependency collection, not the very same bean itself.                for (String candidate : candidateNames) {                    if (isSelfReference(beanName, candidate) &&                            (!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&                            isAutowireCandidate(candidate, fallbackDescriptor)) {                        addCandidateEntry(result, candidate, descriptor, requiredType);                    }                }            }        }        return result;    }
  1. 很显著,对我们的RequestObjectFactory进行了JDK动态代理商。原来我们通过@Autowired注入拿到的HttpServletRequest对象是代理商对象。
    public static Object resolveAutowiringValue(Object autowiringValue, Class<?> requiredType) {        if (autowiringValue instanceof ObjectFactory && !requiredType.isInstance(autowiringValue)) {            ObjectFactory<?> factory = (ObjectFactory<?>) autowiringValue;            if (autowiringValue instanceof Serializable && requiredType.isInterface()) {                autowiringValue = Proxy.newProxyInstance(requiredType.getClassLoader(),                        new Class<?>[] {requiredType}, new ObjectFactoryDelegatingInvocationHandler(factory));            }            else {                return factory.getObject();            }        }        return autowiringValue;    }
    private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {        private final ObjectFactory<?> objectFactory;        public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {            this.objectFactory = objectFactory;        }        @Override        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {            String methodName = method.getName();            if (methodName.equals("equals")) {                // Only consider equal when proxies are identical.                return (proxy == args[0]);            }            else if (methodName.equals("hashCode")) {                // Use hashCode of proxy.                return System.identityHashCode(proxy);            }            else if (methodName.equals("toString")) {                return this.objectFactory.toString();            }            try {                return method.invoke(this.objectFactory.getObject(), args);            }            catch (InvocationTargetException ex) {                throw ex.getTargetException();            }        }    }

8.我们再来看SpringMVC是怎样把HttpServletRequest对象放入到ThreadLocal中。当使用户发出请求后,会经过FrameworkServlet中的processRequest()方法做了少量骚操作,而后再交给子类DispatcherServlet中的doService()去解决这个请求。这些骚操作就包括把request,response对象包装成ServletRequestAttributes对象,而后放入到ThreadLocal中。

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        long startTime = System.currentTimeMillis();        Throwable failureCause = null;        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();        LocaleContext localeContext = buildLocaleContext(request);        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());        initContextHolders(request, localeContext, requestAttributes);        try {            doService(request, response);        }        catch (ServletException ex) {            failureCause = ex;            throw ex;        }        catch (IOException ex) {            failureCause = ex;            throw ex;        }        catch (Throwable ex) {            failureCause = ex;            throw new NestedServletException("Request processing failed", ex);        }        finally {            resetContextHolders(request, previousLocaleContext, previousAttributes);            if (requestAttributes != null) {                requestAttributes.requestCompleted();            }            if (logger.isDebugEnabled()) {                if (failureCause != null) {                    this.logger.debug("Could not complete request", failureCause);                }                else {                    if (asyncManager.isConcurrentHandlingStarted()) {                        logger.debug("Leaving response open for concurrent processing");                    }                    else {                        this.logger.debug("Successfully completed request");                    }                }            }            publishRequestHandledEvent(request, response, startTime, failureCause);        }    }
  1. buildRequestAttributes()方法将当前request和response对象包装成ServletRequestAttributes对象。initContextHolders()负责把RequestAttributes对象放入到requestAttributesHolder(ThreadLocal)中。一切真相大白。
    protected ServletRequestAttributes buildRequestAttributes(            HttpServletRequest request, HttpServletResponse response, RequestAttributes previousAttributes) {        if (previousAttributes == null || previousAttributes instanceof ServletRequestAttributes) {            return new ServletRequestAttributes(request, response);        }        else {            return null;  // preserve the pre-bound RequestAttributes instance        }    }
    private void initContextHolders(            HttpServletRequest request, LocaleContext localeContext, RequestAttributes requestAttributes) {        if (localeContext != null) {            LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);        }        if (requestAttributes != null) {            RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);        }        if (logger.isTraceEnabled()) {            logger.trace("Bound request context to thread: " + request);        }    }
    public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {        if (attributes == null) {            resetRequestAttributes();        }        else {            if (inheritable) {                inheritableRequestAttributesHolder.set(attributes);                requestAttributesHolder.remove();            }            else {                requestAttributesHolder.set(attributes);                inheritableRequestAttributesHolder.remove();            }        }    }
    private static final boolean jsfPresent =            ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());    private static final ThreadLocal<RequestAttributes> requestAttributesHolder =            new NamedThreadLocal<RequestAttributes>("Request attributes");    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =            new NamedInheritableThreadLocal<RequestAttributes>("Request context");
  1. SpringMVC会优先执行被@ModelAttribute注解的方法。也就是说我们每一次请求,都会去调使用init()方法,对request,response,httpSession进行赋值操作,并发问题也由此产生。
    private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container)            throws Exception {        while (!this.modelMethods.isEmpty()) {            InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();            ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);            if (container.containsAttribute(ann.name())) {                if (!ann.binding()) {                    container.setBindingDisabled(ann.name());                }                continue;            }            Object returnValue = modelMethod.invokeForRequest(request, container);            if (!modelMethod.isVoid()){                String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());                if (!ann.binding()) {                    container.setBindingDisabled(returnValueName);                }                if (!container.containsAttribute(returnValueName)) {                    container.addAttribute(returnValueName, returnValue);                }            }        }    }
public abstract class BaseController1 {    protected HttpServletRequest request;    protected HttpServletResponse response;    protected HttpSession httpSession;    @ModelAttribute    public void init(HttpServletRequest request,                     HttpServletResponse response,                     HttpSession httpSession) {        this.request = request;        this.response = response;        this.httpSession = httpSession;    }}

尾言

大家好,我是cmazxiaoma(寓意是沉梦昂志的小马),希望和你们一起成长进步,感谢各位阅读本文章。

小弟不才。
假如您对这篇文章有什么意见或者者错误需要改进的地方,欢迎与我探讨。
假如您觉得还不错的话,希望你们可以点个赞。
希望我的文章对你能有所帮助。
有什么意见、见地或者疑惑,欢迎留言探讨。

最后送上:心之所向,素履以往。生如逆旅,一苇以航。

saoqi.png 上一篇 目录 已是最后

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » SpringMVC中出现的线程安全问题分析

发表回复