SpringBoot的controller为什么不能并行执行?同一个浏览器连续多次访问同一个url竟然是串行的?- 第329篇

前言

       在知乎看到这么一个提问:在springboot的controller中使用Thread.sleep,为什么不能并行执行?

       如代码所示,在controller的sleep方法中,使用了 Thread.sleep,然后用chrome打开两个页签模拟并行访问,发现这两次请求是串行执行的。第二次请求需要等待第一次请求执行完毕才可以:

  1. package com.kfit.controller;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. @RestController
  5. public class AsyncController {
  6. @RequestMapping("async")
  7. public String sleep() throws InterruptedException {
  8. long startTime = System.currentTimeMillis();
  9. System.out.println("[before]name is " + Thread.currentThread().getName() + " time is " + startTime);
  10. Thread.sleep(10000);
  11. long endTime = System.currentTimeMillis();
  12. System.out.println("[after]name is " + Thread.currentThread().getName() + " time is " + endTime + " cos " + (endTime-startTime));
  13. return Thread.currentThread().getName();
  14. }
  15. }

控制台输出如下:

[before]name is http-nio-8080-exec-2 time is 1602751326997

[after]name is http-nio-8080-exec-2 time is 1602751337000 cos10003

[before]name is http-nio-8080-exec-3 time is 1602751337004

[after]name is http-nio-8080-exec-3 time is 1602751347008 cos10004

 

       为什么不能并行执行?按照我的理解,多个http请求到达controller的时候,是不同的线程进行处理的。照理说应该是可以并行的。

       这个和我对于这个多线程的认知结果不太一样,于是好奇心作祟,就试了下,结果还真是串行执行呐。这个是为什么呢?如果你也有我和一样的疑问,本文为你解惑。

 

一、准备工作

       为了模拟上面的场景,我们需要创建一个SpringBoot项目,这个步骤略过,如果还有不懂的小盆友,那么你得看看SpringBoot的helloworld了。

1.1 环境说明

(1)OS:mac os。

(2)Spring Boot : 2.3.4.RELEASE

(3)idea:Intellj Idea

(4)Chrome浏览器:86.0.4240.75(正式版本) (x86_64)

(5)Safari浏览器:11.1.2 (13605.3.8)

(6)Firefox浏览器:72.0.2 (64 位)

 

1.2 测试代码

       为了后面的测试,这里我们编写两个方法async()和async1():

  1. package com.kfit.controller;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. import org.springframework.web.bind.annotation.ResponseBody;
  4. import org.springframework.web.bind.annotation.RestController;
  5. /**
  6. * 测试:SpringBoot的controller中使用Thread.sleep,为什么不能并行执行?
  7. *
  8. * @author 悟纤「公众号SpringBoot」
  9. * @date 2020-10-16
  10. * @slogan 大道至简 悟在天成
  11. */
  12. @RestController //等价于:@Controller+@ResponseBody
  13. public class AsyncController {
  14. @RequestMapping("async")
  15. public String async() throws InterruptedException {
  16. long startTime = System.currentTimeMillis();
  17. System.out.println("async->[before]name is " + Thread.currentThread().getName() + " time is " + startTime);
  18. Thread.sleep(10000);
  19. long endTime = System.currentTimeMillis();
  20. System.out.println("async->[after]name is " + Thread.currentThread().getName() + " time is " + endTime + " cos " + (endTime-startTime));
  21. return Thread.currentThread().getName();
  22. }
  23. @RequestMapping("async1")
  24. public String async1() throws InterruptedException {
  25. long startTime = System.currentTimeMillis();
  26. System.out.println("async1->[before]name is " + Thread.currentThread().getName() + " time is " + startTime);
  27. Thread.sleep(10000);
  28. long endTime = System.currentTimeMillis();
  29. System.out.println("async1->[after]name is " + Thread.currentThread().getName() + " time is " + endTime + " cos " + (endTime-startTime));
  30. return Thread.currentThread().getName();
  31. }
  32. }

二、一触即发:不是我的锅我不背

2.1 Chrome:同一个浏览器连续多次访问同一个url

       我们先看一下同一个浏览器连续多次访问同一个url

打开Chrome浏览器,打开两个tab,分别输入如下地址:

(1)tab1:http://127.0.0.1:8080/async

(2)tab2:http://127.0.0.1:8080/async

       是不是搞错地址了,怎么两个地址一样,你眼睛没有问题,就是同一个地址。

       那么在查看控制台的输出:

async->[before]name ishttp-nio-8080-exec-5 time is 1602831935687

async->[after]name ishttp-nio-8080-exec-5 time is 1602831945692 cos 10005

async->[before]name ishttp-nio-8080-exec-6 time is 1602831945696

async->[after]name ishttp-nio-8080-exec-6 time is 1602831955701 cos 10005

    哇咔咔,这是什么情况?从上面的信息可以看出,第二次发起的请求需要等待第一次请求处理完成之后才会开始执行。

       结论:同一个浏览器连续多次访问同一个url会造成多次访问的关系变为串行。

       这是为什么呢?待会揭晓。

 



2.2 Chrome:同一个浏览器连续多次访问不同url

       难道我们这个浏览器发起的请求都是串行的嘛,要是这样子并发不是很低。

我们来看一下同一个浏览器连续多次访问不同url的情况。

打开Chrome浏览器,打开两个tab,分别输入如下地址:

(1)tab1:http://127.0.0.1:8080/async

(2)tab2:http://127.0.0.1:8080/async1

       访问上面的中,查看控制台的打印信息:

async->[before]name ishttp-nio-8080-exec-9 time is 1602832814903

async1->[before]name ishttp-nio-8080-exec-10 time is 1602832816942

async->[after]name ishttp-nio-8080-exec-9 time is 1602832824908 cos 10005

async1->[after]name ishttp-nio-8080-exec-10 time is 1602832826946 cos 10004

       还好,这两个请求确实是并行了,真的是吓死宝宝了。这说明我们上面的问题只是针对同一个url的情况下才会出现呐。

 

2.3 Chrome:同一个浏览器连续多次访问同一个url,参数不一样

       看一下同一个浏览器连续多次访问同一个url,参数不一样的情况。

打开Chrome浏览器,打开两个tab,分别输入如下地址:

(1)tab1:http://127.0.0.1:8080/async?v=1

(2)tab2:http://127.0.0.1:8080/async?v=2

       请求上面的地址,查看控制台的打印信息:

async->[before]name ishttp-nio-8080-exec-3 time is 1602833002334

async->[before]name is http-nio-8080-exec-4time is 1602833004117

async->[after]name ishttp-nio-8080-exec-3 time is 1602833012338 cos 10004

async->[after]name ishttp-nio-8080-exec-4 time is 1602833014121 cos 10004

       这就说明上面的情况只会在地址一模一样的情况下,并且请求参数也是一样的情况下才会发生了。

       在下面的说明中,同一个url就默认url是一样的,请求参数也是一样的。

 

2.4 Firefox和Safari:同一个浏览器连续多次访问同一个url

       这一次我们换个浏览器试下:

打开Firefox/Safari浏览器,打开两个tab,分别输入如下地址:

(1)tab1:http://127.0.0.1:8080/async

(2)tab2:http://127.0.0.1:8080/async

       访问上面的地址,查看控制台:

async->[before]name ishttp-nio-8080-exec-7 time is 1602833282532

async->[before]name ishttp-nio-8080-exec-8 time is 1602833283983

async->[after]name ishttp-nio-8080-exec-7 time is 1602833292535 cos 10003

async->[after]name ishttp-nio-8080-exec-8 time is 1602833293987 cos 10004

       从这里看出,这个好像不是我们代码的问题,我们代码还是很正常的,在其它浏览器下就是并行的,那么为什么唯独Chrome浏览器这么奇葩呐。

三、原来如此:你个糟老头坏得很

       对于上面的结论就是:

谷歌浏览器同时只能对同一个URL发起一个请求,如果有更多的请求的话,则会串行执行。如果请求阻塞,后续相同请求也会阻塞。

       Chrome的限制规则是:浏览器同时只能对同一个URL提出一个请求,如果有更多的请求的话,对不起,请排队。这个所谓“限制”到底好不好?可能不错,想想对同一URL的请求,如果前请求阻塞,那么后请求想必也会被阻塞,这无端增加了开销,并没多大意义,Chrome这么做应该有它的合理性。

       为此为了验证这个上面这段话,我们可以找到这么一篇文章:Http cache:Implement a timeout for the cache lock.

https://codereview.chromium.org/345643003

       有这么一段描述:

Http cache: Implement a timeout for thecache lock. The cache has a single writer / multiple reader lock to avoiddownloading the same resource n times. However, it is possible to block manytabs on the same resource, for instance behind an auth dialog. This CLimplements a 20 seconds timeout so that the scenario described in the bugresults in multiple authentication dialogs (one per blocked tab) so the usercan know what to do. It will also help with other cases when the single writerblocks for a long time. The timeout is somewhat arbitrary but it should allowmedium size resources to be downloaded before starting another request for thesame item. The general solution of detecting progress and allow readers tostart before the writer finishes should be implemented on another CL.

 

   还有就是chrome浏览器的源代码:

https://github.com/chromium/chromium/blob/d2b6b216eec33310411c81e3891cf496b7e6d3c1/net/http/http_cache_transaction.cc#L1369

       中也有这么一段说明:

  1. void HttpCache::Transaction::AddCacheLockTimeoutHandler(ActiveEntry* entry) {
  2. DCHECK(next_state_ == STATE_ADD_TO_ENTRY_COMPLETE ||
  3. next_state_ == STATE_FINISH_HEADERS_COMPLETE);
  4. if ((bypass_lock_for_test_ && next_state_ == STATE_ADD_TO_ENTRY_COMPLETE) ||
  5. (bypass_lock_after_headers_for_test_ &&
  6. next_state_ == STATE_FINISH_HEADERS_COMPLETE)) {
  7. base::ThreadTaskRunnerHandle::Get()->PostTask(
  8. FROM_HERE,
  9. base::BindOnce(&HttpCache::Transaction::OnCacheLockTimeout,
  10. weak_factory_.GetWeakPtr(), entry_lock_waiting_since_));
  11. } else {
  12. int timeout_milliseconds = 20 * 1000;
  13. if (partial_ && entry->writers && !entry->writers->IsEmpty() &&
  14. entry->writers->IsExclusive()) {
  15. // Even though entry_->writers takes care of allowing multiple writers to
  16. // simultaneously govern reading from the network and writing to the cache
  17. // for full requests, partial requests are still blocked by the
  18. // reader/writer lock.
  19. // Bypassing the cache after 25 ms of waiting for the cache lock
  20. // eliminates a long running issue, http://crbug.com/31014, where
  21. // two of the same media resources could not be played back simultaneously
  22. // due to one locking the cache entry until the entire video was
  23. // downloaded.
  24. // Bypassing the cache is not ideal, as we are now ignoring the cache
  25. // entirely for all range requests to a resource beyond the first. This
  26. // is however a much more succinct solution than the alternatives, which
  27. // would require somewhat significant changes to the http caching logic.
  28. //
  29. // Allow some timeout slack for the entry addition to complete in case
  30. // the writer lock is imminently released; we want to avoid skipping
  31. // the cache if at all possible. See http://crbug.com/408765
  32. timeout_milliseconds = 25;
  33. }
  34. base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
  35. FROM_HERE,
  36. base::BindOnce(&HttpCache::Transaction::OnCacheLockTimeout,
  37. weak_factory_.GetWeakPtr(), entry_lock_waiting_since_),
  38. TimeDelta::FromMilliseconds(timeout_milliseconds));
  39. }
  40. }

    购买完整视频,请前往:http://www.mark-to-win.com/TeacherV2.html?id=287