Od stolu bych tipoval: Volání BufferInputStream.read(bytes) na každý bytě je zpočátku neefektivní, ale JIT ho potom zoptimalizuje, proto při opakovaném použití to není problém. JIT typicky nekomplikuje kód hned, ale až po nějakém množství použití. Tím si jednak zajišťuje, že nekomplikuje kdeco, a jednak díky tomu má nějaké statistiky, kterých může využít. Takže část cyklu poprvé proběhne neoptimálně, proběhne kompilace a dál je to OK.
To, že záleží, ve kterém vlákně se to spustí, je na první pohled divné, ale v zásadě to není v rozporu s hypotézou. Mj. je možné, že i další vlákna budou mít nějaký vliv na JIT.
Jak to ověřit?
a) Napadá mě použití -Xint, tím JIT vypadne ze hry. Ano, bude to pomalejší, ale mělo by to být konzistentně pomalejší. Problém ale je, že to může být tak pomalé, že ten rozdíl v tom nebude patrný.
b) GraalVM native-image – vše se zkompiluje ještě před startem, JIT do toho nebude házet vidle. U dlouho běžících aplikací může JIT udělat lepší práci než native-image, ale native-image dává predikovatelnější výkon. Nevýhodou je, že nelze použít na každou aplikaci, protože to klade nějaké dodatečné požadavky – zejména musí vědět, co všechno má zkompilovat, do čehož mu může do nějaké míry házet vidle třeba reflexe provedená později než při statické inicializaci. Spoustu věcí lze řešit, jen jsem chtěl upozornit, že ne všechny aplikace lze takto přeložit na první dobrou.
c) Experimentovat s -Xcomp (zkompiluje vše při prvním použití; bude toho kompilovat více, bude častěji rekompilovat (protože častěji nevyjde spekulativní předpoklad), asi z toho nebudou padat tak dobré výsledky), případně -XX:CompileThreshold. Neříkám, že se to hodí do produkce, spíš to může přinést predikovatelnější chování.
BTW, Executor obecně nemusí kód spustit hned (a dokonce ani v jiném vlákně). Zřejmě s tím není problém (jinak by problém nevyřešil onen workaround), ale je dobré s tím počítat a nebrat volání Executor.execute(Runnable) za začátek práce.