前言
在知乎看到這么一個提問:在springboot的controller中使用Thread.sleep,為什么不能並行執行?
如代碼所示,在controller的sleep方法中,使用了 Thread.sleep,然后用chrome打開兩個頁簽模擬並行訪問,發現這兩次請求是串行執行的。第二次請求需要等待第一次請求執行完畢才可以:
package com.kfit.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;public class AsyncController {public String sleep() throws InterruptedException {long startTime = System.currentTimeMillis();System.out.println("[before]name is " + Thread.currentThread().getName() + " time is " + startTime);Thread.sleep(10000);long endTime = System.currentTimeMillis();System.out.println("[after]name is " + Thread.currentThread().getName() + " time is " + endTime + " cos " + (endTime-startTime));return Thread.currentThread().getName();}}
控制台輸出如下:
[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():
package com.kfit.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;/*** 測試:SpringBoot的controller中使用Thread.sleep,為什么不能並行執行?** @author 悟纖「公眾號SpringBoot」* @date 2020-10-16* @slogan 大道至簡 悟在天成*/public class AsyncController {public String async() throws InterruptedException {long startTime = System.currentTimeMillis();System.out.println("async->[before]name is " + Thread.currentThread().getName() + " time is " + startTime);Thread.sleep(10000);long endTime = System.currentTimeMillis();System.out.println("async->[after]name is " + Thread.currentThread().getName() + " time is " + endTime + " cos " + (endTime-startTime));return Thread.currentThread().getName();}public String async1() throws InterruptedException {long startTime = System.currentTimeMillis();System.out.println("async1->[before]name is " + Thread.currentThread().getName() + " time is " + startTime);Thread.sleep(10000);long endTime = System.currentTimeMillis();System.out.println("async1->[after]name is " + Thread.currentThread().getName() + " time is " + endTime + " cos " + (endTime-startTime));return Thread.currentThread().getName();}}
二、一觸即發:不是我的鍋我不背
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
async1->[before]name is http-nio-8080-exec-4time is 1602833004117
async->[after]name ishttp-nio-8080-exec-3 time is 1602833012338 cos 10004
async1->[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
async1->[before]name ishttp-nio-8080-exec-8 time is 1602833283983
async->[after]name ishttp-nio-8080-exec-7 time is 1602833292535 cos 10003
async1->[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
中也有這么一段說明:
void HttpCache::Transaction::AddCacheLockTimeoutHandler(ActiveEntry* entry) {DCHECK(next_state_ == STATE_ADD_TO_ENTRY_COMPLETE ||next_state_ == STATE_FINISH_HEADERS_COMPLETE);if ((bypass_lock_for_test_ && next_state_ == STATE_ADD_TO_ENTRY_COMPLETE) ||(bypass_lock_after_headers_for_test_ &&next_state_ == STATE_FINISH_HEADERS_COMPLETE)) {base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,base::BindOnce(&HttpCache::Transaction::OnCacheLockTimeout,weak_factory_.GetWeakPtr(), entry_lock_waiting_since_));} else {int timeout_milliseconds = 20 * 1000;if (partial_ && entry->writers && !entry->writers->IsEmpty() &&entry->writers->IsExclusive()) {// Even though entry_->writers takes care of allowing multiple writers to// simultaneously govern reading from the network and writing to the cache// for full requests, partial requests are still blocked by the// reader/writer lock.// Bypassing the cache after 25 ms of waiting for the cache lock// eliminates a long running issue, http://crbug.com/31014, where// two of the same media resources could not be played back simultaneously// due to one locking the cache entry until the entire video was// downloaded.// Bypassing the cache is not ideal, as we are now ignoring the cache// entirely for all range requests to a resource beyond the first. This// is however a much more succinct solution than the alternatives, which// would require somewhat significant changes to the http caching logic.//// Allow some timeout slack for the entry addition to complete in case// the writer lock is imminently released; we want to avoid skipping// the cache if at all possible. See http://crbug.com/408765timeout_milliseconds = 25;}base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(FROM_HERE,base::BindOnce(&HttpCache::Transaction::OnCacheLockTimeout,weak_factory_.GetWeakPtr(), entry_lock_waiting_since_),TimeDelta::FromMilliseconds(timeout_milliseconds));}}
悟纖小結
Chrome瀏覽器同時只能對同一個URL發起一個請求,如果有更多的請求的話,則會串行執行。如果請求阻塞,后續相同請求也會阻塞。
這是由於Chrome的限制規則:瀏覽器同時只能對同一個URL提出一個請求,如果有更多的請求的話,對不起,請排隊。
