[{"data":1,"prerenderedAt":1626},["ShallowReactive",2],{"blog-\u002Fblog\u002Fjava-executors-thread-pools":3},{"id":4,"title":5,"body":6,"description":1612,"difficulty":1613,"extension":1614,"framework":1615,"frameworkSlug":131,"meta":1616,"navigation":868,"order":191,"path":1617,"qaPath":1618,"seo":1619,"stem":1620,"subtopic":1621,"topic":1622,"topicSlug":1623,"updated":1624,"__hash__":1625},"blog\u002Fblog\u002Fjava-executors-thread-pools.md","Java Executors, Thread Pools & CompletableFuture — A Complete Guide",{"type":7,"value":8,"toc":1599},"minimark",[9,14,44,48,51,126,210,222,226,261,289,379,389,393,429,491,523,527,541,586,645,654,658,680,757,768,772,831,940,996,1000,1051,1143,1193,1197,1225,1395,1419,1423,1438,1500,1522,1526,1595],[10,11,13],"h2",{"id":12},"why-executors-exist","Why executors exist",[15,16,17,18,22,23,27,28,31,32,35,36,39,40,43],"p",{},"Raw threads are seductive — ",[19,20,21],"code",{},"new Thread(task).start()"," looks like the whole story — but\nthey don't scale. Each OS thread costs a stack (often 512KB–1MB), a kernel scheduling\nentry, and bookkeeping, so spawning one per task means ",[24,25,26],"strong",{},"unbounded creation"," under load\nthat exhausts memory and thrashes the scheduler. The ",[24,29,30],{},"Executor framework"," (Java 5) fixes\nthis by ",[24,33,34],{},"decoupling task submission from task execution",": you hand work to a pool, and a\nbounded set of reusable workers runs it. The two wins are ",[24,37,38],{},"reuse"," (pay thread cost once)\nand ",[24,41,42],{},"bounding"," (a hard cap on concurrency so a traffic spike can't take down the JVM).\nThe rest of this guide is about choosing and tuning that pool correctly.",[10,45,47],{"id":46},"the-executor-interface-hierarchy","The Executor interface hierarchy",[15,49,50],{},"The framework is a layered stack of interfaces, each adding capability on top of the last:",[52,53,54,72,110],"ul",{},[55,56,57,62,63,66,67,71],"li",{},[24,58,59],{},[19,60,61],{},"Executor"," — the minimal contract, a single ",[19,64,65],{},"execute(Runnable)",". It just ",[68,69,70],"em",{},"runs"," a\ntask; whether that's now, pooled, or async is the implementation's choice.",[55,73,74,79,80,83,84,87,88,91,92,95,96,87,99,87,102,105,106,109],{},[24,75,76],{},[19,77,78],{},"ExecutorService"," — adds ",[24,81,82],{},"lifecycle"," (",[19,85,86],{},"shutdown",", ",[19,89,90],{},"awaitTermination",") and\n",[24,93,94],{},"result-bearing"," submission (",[19,97,98],{},"submit",[19,100,101],{},"invokeAll",[19,103,104],{},"invokeAny",") that hand back\n",[19,107,108],{},"Future","s.",[55,111,112,117,118,121,122,125],{},[24,113,114],{},[19,115,116],{},"ScheduledExecutorService"," — adds running tasks ",[24,119,120],{},"after a delay"," or\n",[24,123,124],{},"periodically",".",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-java shiki shiki-themes github-light github-dark","Executor e = Runnable::run;                          \u002F\u002F simplest possible Executor\nExecutorService es = Executors.newFixedThreadPool(4); \u002F\u002F lifecycle + Futures\nScheduledExecutorService ses = Executors.newScheduledThreadPool(2);\n","java","",[19,134,135,161,189],{"__ignoreMap":132},[136,137,140,144,148,151,154,157],"span",{"class":138,"line":139},"line",1,[136,141,143],{"class":142},"sVt8B","Executor e ",[136,145,147],{"class":146},"szBVR","=",[136,149,150],{"class":142}," Runnable",[136,152,153],{"class":146},"::",[136,155,156],{"class":142},"run;                          ",[136,158,160],{"class":159},"sJ8bj","\u002F\u002F simplest possible Executor\n",[136,162,164,167,169,172,176,179,183,186],{"class":138,"line":163},2,[136,165,166],{"class":142},"ExecutorService es ",[136,168,147],{"class":146},[136,170,171],{"class":142}," Executors.",[136,173,175],{"class":174},"sScJk","newFixedThreadPool",[136,177,178],{"class":142},"(",[136,180,182],{"class":181},"sj4cs","4",[136,184,185],{"class":142},"); ",[136,187,188],{"class":159},"\u002F\u002F lifecycle + Futures\n",[136,190,192,195,197,199,202,204,207],{"class":138,"line":191},3,[136,193,194],{"class":142},"ScheduledExecutorService ses ",[136,196,147],{"class":146},[136,198,171],{"class":142},[136,200,201],{"class":174},"newScheduledThreadPool",[136,203,178],{"class":142},[136,205,206],{"class":181},"2",[136,208,209],{"class":142},");\n",[15,211,212,213,217,218,221],{},"You almost always program against ",[24,214,215],{},[19,216,78],{},": it gives you both task results and\na clean way to shut the pool down. Coding to the interface (not ",[19,219,220],{},"ThreadPoolExecutor","\ndirectly in call sites) keeps you free to swap implementations later.",[10,223,225],{"id":224},"factory-methods-and-why-you-should-distrust-them","Factory methods and why you should distrust them",[15,227,228,229,232,233,236,237,87,240,87,243,236,246,249,250,252,253,256,257,260],{},"The ",[19,230,231],{},"Executors"," class is a factory of preconfigured pools — ",[19,234,235],{},"newFixedThreadPool(n)",",\n",[19,238,239],{},"newCachedThreadPool()",[19,241,242],{},"newSingleThreadExecutor()",[19,244,245],{},"newScheduledThreadPool(n)",[19,247,248],{},"newWorkStealingPool()",". They're convenient, and they're also where production incidents\ncome from, because each one is just a ",[19,251,220],{}," with ",[24,254,255],{},"hidden defaults"," that\ncan trigger ",[19,258,259],{},"OutOfMemoryError",":",[52,262,263,279],{},[55,264,265,267,268,271,272,278],{},[19,266,175],{}," and ",[19,269,270],{},"newSingleThreadExecutor"," back their queue with an ",[24,273,274,275],{},"unbounded\n",[19,276,277],{},"LinkedBlockingQueue"," — if tasks arrive faster than they finish, the queue grows until\nthe heap is gone.",[55,280,281,284,285,288],{},[19,282,283],{},"newCachedThreadPool"," has ",[24,286,287],{},"no upper bound on threads"," — a burst can spawn thousands and\nexhaust native memory.",[127,290,292],{"className":129,"code":291,"language":131,"meta":132,"style":132},"\u002F\u002F Effective Java's guidance: construct ThreadPoolExecutor directly so every\n\u002F\u002F knob — queue bound, max threads, rejection behavior — is a deliberate choice.\nExecutorService pool = new ThreadPoolExecutor(\n    8, 16, 60L, TimeUnit.SECONDS,\n    new ArrayBlockingQueue\u003C>(1000),            \u002F\u002F bounded queue\n    new ThreadPoolExecutor.CallerRunsPolicy()  \u002F\u002F backpressure on overflow\n);\n",[19,293,294,299,304,320,339,357,374],{"__ignoreMap":132},[136,295,296],{"class":138,"line":139},[136,297,298],{"class":159},"\u002F\u002F Effective Java's guidance: construct ThreadPoolExecutor directly so every\n",[136,300,301],{"class":138,"line":163},[136,302,303],{"class":159},"\u002F\u002F knob — queue bound, max threads, rejection behavior — is a deliberate choice.\n",[136,305,306,309,311,314,317],{"class":138,"line":191},[136,307,308],{"class":142},"ExecutorService pool ",[136,310,147],{"class":146},[136,312,313],{"class":146}," new",[136,315,316],{"class":174}," ThreadPoolExecutor",[136,318,319],{"class":142},"(\n",[136,321,323,326,328,331,333,336],{"class":138,"line":322},4,[136,324,325],{"class":181},"    8",[136,327,87],{"class":142},[136,329,330],{"class":181},"16",[136,332,87],{"class":142},[136,334,335],{"class":181},"60L",[136,337,338],{"class":142},", TimeUnit.SECONDS,\n",[136,340,342,345,348,351,354],{"class":138,"line":341},5,[136,343,344],{"class":146},"    new",[136,346,347],{"class":142}," ArrayBlockingQueue\u003C>(",[136,349,350],{"class":181},"1000",[136,352,353],{"class":142},"),            ",[136,355,356],{"class":159},"\u002F\u002F bounded queue\n",[136,358,360,362,365,368,371],{"class":138,"line":359},6,[136,361,344],{"class":146},[136,363,364],{"class":142}," ThreadPoolExecutor.",[136,366,367],{"class":174},"CallerRunsPolicy",[136,369,370],{"class":142},"()  ",[136,372,373],{"class":159},"\u002F\u002F backpressure on overflow\n",[136,375,377],{"class":138,"line":376},7,[136,378,209],{"class":142},[15,380,381,382,385,386,125],{},"The factories are fine for scripts and tests. For anything that takes real traffic, build\nthe executor explicitly so the failure mode is ",[68,383,384],{},"rejection",", not ",[68,387,388],{},"crash",[10,390,392],{"id":391},"threadpoolexecutor-the-six-knobs-and-the-task-flow","ThreadPoolExecutor: the six knobs and the task flow",[15,394,395,397,398,401,402,405,406,409,410,413,414,417,418,421,422,425,426,260],{},[19,396,220],{}," is defined entirely by six parameters: ",[19,399,400],{},"corePoolSize"," (threads kept\nalive when idle), ",[19,403,404],{},"maximumPoolSize"," (hard cap), ",[19,407,408],{},"keepAliveTime"," (how long non-core idle\nthreads survive), the ",[19,411,412],{},"workQueue",", a ",[19,415,416],{},"threadFactory",", and a ",[19,419,420],{},"RejectedExecutionHandler",".\nWhat trips people up is the ",[24,423,424],{},"order"," in which capacity is used on ",[19,427,428],{},"execute",[127,430,432],{"className":129,"code":431,"language":131,"meta":132,"style":132},"\u002F\u002F core=2, queue=2, max=4 — capacity surfaces in THIS order:\nnew ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue\u003C>(2));\n\u002F\u002F tasks 1-2 -> start 2 core threads (even if a thread is idle, prefer a new core thread)\n\u002F\u002F tasks 3-4 -> wait in the queue (capacity 2)\n\u002F\u002F tasks 5-6 -> spawn 2 extra threads up to max=4 (only once the queue is full)\n\u002F\u002F task  7   -> rejected\n",[19,433,434,439,471,476,481,486],{"__ignoreMap":132},[136,435,436],{"class":138,"line":139},[136,437,438],{"class":159},"\u002F\u002F core=2, queue=2, max=4 — capacity surfaces in THIS order:\n",[136,440,441,444,446,448,450,452,454,456,459,462,464,466,468],{"class":138,"line":163},[136,442,443],{"class":146},"new",[136,445,316],{"class":174},[136,447,178],{"class":142},[136,449,206],{"class":181},[136,451,87],{"class":142},[136,453,182],{"class":181},[136,455,87],{"class":142},[136,457,458],{"class":181},"60",[136,460,461],{"class":142},", TimeUnit.SECONDS, ",[136,463,443],{"class":146},[136,465,347],{"class":142},[136,467,206],{"class":181},[136,469,470],{"class":142},"));\n",[136,472,473],{"class":138,"line":191},[136,474,475],{"class":159},"\u002F\u002F tasks 1-2 -> start 2 core threads (even if a thread is idle, prefer a new core thread)\n",[136,477,478],{"class":138,"line":322},[136,479,480],{"class":159},"\u002F\u002F tasks 3-4 -> wait in the queue (capacity 2)\n",[136,482,483],{"class":138,"line":341},[136,484,485],{"class":159},"\u002F\u002F tasks 5-6 -> spawn 2 extra threads up to max=4 (only once the queue is full)\n",[136,487,488],{"class":138,"line":359},[136,489,490],{"class":159},"\u002F\u002F task  7   -> rejected\n",[15,492,493,494,497,498,500,501,504,505,507,508,511,512,518,519,522],{},"The non-obvious consequence: with an ",[24,495,496],{},"unbounded queue",", step 3 never fires, so\n",[19,499,404],{}," is ",[24,502,503],{},"ignored"," and the pool never grows past core — which is exactly why\n",[19,506,175],{}," only ever runs ",[19,509,510],{},"n"," threads. Always pass a ",[24,513,514,515],{},"named ",[19,516,517],{},"ThreadFactory"," in\nproduction too; default names like ",[19,520,521],{},"pool-1-thread-3"," make thread dumps useless.",[10,524,526],{"id":525},"rejection-policies-what-happens-when-the-pool-is-full","Rejection policies: what happens when the pool is full",[15,528,529,530,533,534,537,538,540],{},"A task is ",[24,531,532],{},"rejected"," when the queue is full ",[68,535,536],{},"and"," the pool is at ",[19,539,404],{}," (or\nafter shutdown). The handler decides the fate of that task, and the choice is a real design\ndecision about how your system degrades:",[52,542,543,555,566,574],{},[55,544,545,550,551,554],{},[24,546,547],{},[19,548,549],{},"AbortPolicy"," (default) — throws ",[19,552,553],{},"RejectedExecutionException",". Loud failure.",[55,556,557,561,562,565],{},[24,558,559],{},[19,560,367],{}," — runs the task on the ",[24,563,564],{},"submitting thread",". Natural backpressure.",[55,567,568,573],{},[24,569,570],{},[19,571,572],{},"DiscardPolicy"," — silently drops the task.",[55,575,576,581,582,585],{},[24,577,578],{},[19,579,580],{},"DiscardOldestPolicy"," — drops the ",[24,583,584],{},"oldest queued"," task, retries the new one.",[127,587,589],{"className":129,"code":588,"language":131,"meta":132,"style":132},"var pool = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,\n    new ArrayBlockingQueue\u003C>(10),\n    new ThreadPoolExecutor.CallerRunsPolicy()); \u002F\u002F submitter slows down under load\n",[19,590,591,619,631],{"__ignoreMap":132},[136,592,593,596,599,601,603,605,607,609,611,613,615,617],{"class":138,"line":139},[136,594,595],{"class":146},"var",[136,597,598],{"class":142}," pool ",[136,600,147],{"class":146},[136,602,313],{"class":146},[136,604,316],{"class":174},[136,606,178],{"class":142},[136,608,206],{"class":181},[136,610,87],{"class":142},[136,612,182],{"class":181},[136,614,87],{"class":142},[136,616,458],{"class":181},[136,618,338],{"class":142},[136,620,621,623,625,628],{"class":138,"line":163},[136,622,344],{"class":146},[136,624,347],{"class":142},[136,626,627],{"class":181},"10",[136,629,630],{"class":142},"),\n",[136,632,633,635,637,639,642],{"class":138,"line":191},[136,634,344],{"class":146},[136,636,364],{"class":142},[136,638,367],{"class":174},[136,640,641],{"class":142},"()); ",[136,643,644],{"class":159},"\u002F\u002F submitter slows down under load\n",[15,646,647,649,650,653],{},[19,648,367],{}," is the favorite for throughput-with-stability: when the pool is\noverwhelmed, the producer is ",[68,651,652],{},"slowed down"," (it does the work itself) rather than throwing\nor losing data. You can also implement your own handler to log, meter, or persist rejected\nwork — useful when dropping a task silently would be a bug.",[10,655,657],{"id":656},"sizing-the-pool-cpu-bound-vs-io-bound","Sizing the pool: CPU-bound vs IO-bound",[15,659,660,661,664,665,83,668,671,672,675,676,679],{},"The right size depends entirely on what threads spend their time doing. ",[24,662,663],{},"CPU-bound"," work\nkeeps the core busy, so more threads than cores only adds context-switching overhead — size\naround the ",[24,666,667],{},"number of cores",[19,669,670],{},"cores + 1"," to cover occasional page faults). ",[24,673,674],{},"IO-bound","\nwork blocks on network or disk, leaving the CPU idle, so you want ",[24,677,678],{},"many more threads than\ncores"," to keep the CPU saturated while others wait.",[127,681,683],{"className":129,"code":682,"language":131,"meta":132,"style":132},"int cores = Runtime.getRuntime().availableProcessors();\nExecutorService cpuPool = Executors.newFixedThreadPool(cores);     \u002F\u002F CPU-bound\nExecutorService ioPool  = Executors.newFixedThreadPool(cores * 8); \u002F\u002F IO-bound (illustrative)\n\u002F\u002F Brian Goetz formula: threads = cores * (1 + waitTime \u002F computeTime)\n",[19,684,685,710,727,752],{"__ignoreMap":132},[136,686,687,690,693,695,698,701,704,707],{"class":138,"line":139},[136,688,689],{"class":146},"int",[136,691,692],{"class":142}," cores ",[136,694,147],{"class":146},[136,696,697],{"class":142}," Runtime.",[136,699,700],{"class":174},"getRuntime",[136,702,703],{"class":142},"().",[136,705,706],{"class":174},"availableProcessors",[136,708,709],{"class":142},"();\n",[136,711,712,715,717,719,721,724],{"class":138,"line":163},[136,713,714],{"class":142},"ExecutorService cpuPool ",[136,716,147],{"class":146},[136,718,171],{"class":142},[136,720,175],{"class":174},[136,722,723],{"class":142},"(cores);     ",[136,725,726],{"class":159},"\u002F\u002F CPU-bound\n",[136,728,729,732,734,736,738,741,744,747,749],{"class":138,"line":191},[136,730,731],{"class":142},"ExecutorService ioPool  ",[136,733,147],{"class":146},[136,735,171],{"class":142},[136,737,175],{"class":174},[136,739,740],{"class":142},"(cores ",[136,742,743],{"class":146},"*",[136,745,746],{"class":181}," 8",[136,748,185],{"class":142},[136,750,751],{"class":159},"\u002F\u002F IO-bound (illustrative)\n",[136,753,754],{"class":138,"line":322},[136,755,756],{"class":159},"\u002F\u002F Brian Goetz formula: threads = cores * (1 + waitTime \u002F computeTime)\n",[15,758,759,760,763,764,767],{},"Treat the formula as a ",[24,761,762],{},"starting point",", not gospel — measure throughput and latency\nunder realistic load and tune from there. And keep CPU and IO work in ",[24,765,766],{},"separate pools",":\nmixing them means a flood of slow IO tasks can starve your CPU work of threads.",[10,769,771],{"id":770},"submit-vs-execute-runnable-vs-callable-and-future","submit vs execute, Runnable vs Callable, and Future",[15,773,774,776,777,779,780,785,786,776,789,791,792,795,796,795,799,802,803,807,808,284,812,815,816,284,821,824,825,267,828,125],{},[19,775,65],{}," comes from ",[19,778,61],{},", returns ",[24,781,782],{},[19,783,784],{},"void",", and is fire-and-forget.\n",[19,787,788],{},"submit(...)",[19,790,78],{},", accepts a ",[19,793,794],{},"Runnable"," ",[68,797,798],{},"or",[19,800,801],{},"Callable",", and hands\nback a ",[24,804,805],{},[19,806,108],{},". The difference between the two task types is the shape of the work:\n",[24,809,810],{},[19,811,794],{},[19,813,814],{},"void run()"," — no return value, no checked exceptions — while\n",[24,817,818],{},[19,819,820],{},"Callable\u003CV>",[19,822,823],{},"V call() throws Exception",", so it ",[24,826,827],{},"returns a result",[24,829,830],{},"may\nthrow",[127,832,834],{"className":129,"code":833,"language":131,"meta":132,"style":132},"pool.execute(() -> log(\"fire and forget\"));   \u002F\u002F void, no handle, no result\n\nCallable\u003CInteger> c = () -> compute();          \u002F\u002F returns a value, may throw checked\nFuture\u003CInteger> f = pool.submit(c);\nInteger result = f.get(2, TimeUnit.SECONDS);    \u002F\u002F blocks (with timeout) for the result\n",[19,835,836,864,870,897,917],{"__ignoreMap":132},[136,837,838,841,843,846,849,852,854,858,861],{"class":138,"line":139},[136,839,840],{"class":142},"pool.",[136,842,428],{"class":174},[136,844,845],{"class":142},"(() ",[136,847,848],{"class":146},"->",[136,850,851],{"class":174}," log",[136,853,178],{"class":142},[136,855,857],{"class":856},"sZZnC","\"fire and forget\"",[136,859,860],{"class":142},"));   ",[136,862,863],{"class":159},"\u002F\u002F void, no handle, no result\n",[136,865,866],{"class":138,"line":163},[136,867,869],{"emptyLinePlaceholder":868},true,"\n",[136,871,872,875,878,881,883,886,888,891,894],{"class":138,"line":191},[136,873,874],{"class":142},"Callable\u003C",[136,876,877],{"class":146},"Integer",[136,879,880],{"class":142},"> c ",[136,882,147],{"class":146},[136,884,885],{"class":142}," () ",[136,887,848],{"class":146},[136,889,890],{"class":174}," compute",[136,892,893],{"class":142},"();          ",[136,895,896],{"class":159},"\u002F\u002F returns a value, may throw checked\n",[136,898,899,902,904,907,909,912,914],{"class":138,"line":322},[136,900,901],{"class":142},"Future\u003C",[136,903,877],{"class":146},[136,905,906],{"class":142},"> f ",[136,908,147],{"class":146},[136,910,911],{"class":142}," pool.",[136,913,98],{"class":174},[136,915,916],{"class":142},"(c);\n",[136,918,919,922,924,927,930,932,934,937],{"class":138,"line":341},[136,920,921],{"class":142},"Integer result ",[136,923,147],{"class":146},[136,925,926],{"class":142}," f.",[136,928,929],{"class":174},"get",[136,931,178],{"class":142},[136,933,206],{"class":181},[136,935,936],{"class":142},", TimeUnit.SECONDS);    ",[136,938,939],{"class":159},"\u002F\u002F blocks (with timeout) for the result\n",[15,941,942,943,946,947,950,951,954,955,958,959,962,963,965,966,969,970,972,973,978,979,982,983,986,987,989,990,992,993,125],{},"A ",[19,944,945],{},"Future\u003CV>"," is a ",[24,948,949],{},"handle to a result that may not exist yet",": ",[19,952,953],{},"get()"," blocks until\ndone, ",[19,956,957],{},"isDone()"," polls, ",[19,960,961],{},"cancel(true)"," attempts interruption. Watch the exception trap —\nwith ",[19,964,428],{},", an uncaught exception hits the thread's ",[19,967,968],{},"UncaughtExceptionHandler"," (you see\nit); with ",[19,971,98],{},", the exception is ",[24,974,975,976],{},"captured in the ",[19,977,108],{}," and only re-thrown\n(wrapped in ",[19,980,981],{},"ExecutionException",", unwrap via ",[19,984,985],{},"getCause()",") when you call ",[19,988,953],{},". A\n",[19,991,98],{},"ted task you never inspect can ",[24,994,995],{},"fail silently",[10,997,999],{"id":998},"completablefuture-composing-async-work","CompletableFuture: composing async work",[15,1001,1002,1003,1005,1006,1009,1010,1013,1014,1017,1018,1023,1024,185,1027,1032,1033,1036,1037,1040,1041,1046,1047,1050],{},"A plain ",[19,1004,108],{}," can only be ",[24,1007,1008],{},"polled or blocked on"," — there's no way to attach a\ncontinuation or combine results without tying up a thread. ",[19,1011,1012],{},"CompletableFuture"," (Java 8)\nimplements ",[19,1015,1016],{},"CompletionStage"," and turns callback spaghetti into a fluent, non-blocking\npipeline. The three composition shapes you must know: ",[24,1019,1020],{},[19,1021,1022],{},"thenApply"," transforms a result\nwith a plain function (",[19,1025,1026],{},"T -> U",[24,1028,1029],{},[19,1030,1031],{},"thenCompose"," chains a function that itself returns a\nfuture (",[19,1034,1035],{},"T -> CompletableFuture\u003CU>",") and ",[24,1038,1039],{},"flattens"," it — the async \"flatMap\"; and\n",[24,1042,1043],{},[19,1044,1045],{},"thenCombine"," waits for ",[24,1048,1049],{},"two independent"," futures and merges them.",[127,1052,1054],{"className":129,"code":1053,"language":131,"meta":132,"style":132},"CompletableFuture\n    .supplyAsync(() -> fetchUser(id), ioPool) \u002F\u002F run async on YOUR pool, not the common one\n    .thenApply(User::name)                    \u002F\u002F sync transform of the result\n    .thenCompose(name -> fetchOrdersAsync(name)) \u002F\u002F chain a dependent async call (flatten)\n    .exceptionally(ex -> List.of());          \u002F\u002F fallback value on any failure upstream\n",[19,1055,1056,1061,1082,1099,1119],{"__ignoreMap":132},[136,1057,1058],{"class":138,"line":139},[136,1059,1060],{"class":142},"CompletableFuture\n",[136,1062,1063,1066,1069,1071,1073,1076,1079],{"class":138,"line":163},[136,1064,1065],{"class":142},"    .",[136,1067,1068],{"class":174},"supplyAsync",[136,1070,845],{"class":142},[136,1072,848],{"class":146},[136,1074,1075],{"class":174}," fetchUser",[136,1077,1078],{"class":142},"(id), ioPool) ",[136,1080,1081],{"class":159},"\u002F\u002F run async on YOUR pool, not the common one\n",[136,1083,1084,1086,1088,1091,1093,1096],{"class":138,"line":191},[136,1085,1065],{"class":142},[136,1087,1022],{"class":174},[136,1089,1090],{"class":142},"(User",[136,1092,153],{"class":146},[136,1094,1095],{"class":142},"name)                    ",[136,1097,1098],{"class":159},"\u002F\u002F sync transform of the result\n",[136,1100,1101,1103,1105,1108,1110,1113,1116],{"class":138,"line":322},[136,1102,1065],{"class":142},[136,1104,1031],{"class":174},[136,1106,1107],{"class":142},"(name ",[136,1109,848],{"class":146},[136,1111,1112],{"class":174}," fetchOrdersAsync",[136,1114,1115],{"class":142},"(name)) ",[136,1117,1118],{"class":159},"\u002F\u002F chain a dependent async call (flatten)\n",[136,1120,1121,1123,1126,1129,1131,1134,1137,1140],{"class":138,"line":341},[136,1122,1065],{"class":142},[136,1124,1125],{"class":174},"exceptionally",[136,1127,1128],{"class":142},"(ex ",[136,1130,848],{"class":146},[136,1132,1133],{"class":142}," List.",[136,1135,1136],{"class":174},"of",[136,1138,1139],{"class":142},"());          ",[136,1141,1142],{"class":159},"\u002F\u002F fallback value on any failure upstream\n",[15,1144,1145,1146,1149,1150,1155,1156,1159,1160,1163,1164,1167,1168,1170,1171,1174,1175,1178,1179,1181,1182,1185,1186,1189,1190,1192],{},"For independent work, ",[19,1147,1148],{},"thenCombine(other, (a, b) -> ...)"," joins two parallel results, and\n",[24,1151,1152],{},[19,1153,1154],{},"allOf(cf...)"," acts as a barrier: it returns ",[19,1157,1158],{},"CompletableFuture\u003CVoid>",", so you\n",[19,1161,1162],{},"join()"," it and then read each future's result individually. Errors propagate down the\nchain wrapped in ",[19,1165,1166],{},"CompletionException"," — ",[19,1169,1125],{}," recovers with a fallback, while\n",[19,1172,1173],{},"handle((res, ex) -> ...)"," sees ",[24,1176,1177],{},"both"," outcomes. One gotcha: ",[19,1180,1068],{},"\u002F",[19,1183,1184],{},"runAsync"," use\nthe ",[24,1187,1188],{},"common ForkJoinPool"," by default, which is shared JVM-wide and sized to your cores —\npass an explicit ",[19,1191,61],{}," for any blocking work so you don't starve it.",[10,1194,1196],{"id":1195},"graceful-shutdown","Graceful shutdown",[15,1198,1199,1200,1202,1203,1206,1207,1212,1213,1218,1219,1224],{},"An ",[19,1201,78],{}," keeps its (often non-daemon) threads alive until you stop it, so you\n",[24,1204,1205],{},"must"," shut it down or the JVM may never exit. There are two stop methods and one wait\nmethod: ",[24,1208,1209],{},[19,1210,1211],{},"shutdown()"," is graceful — stops accepting new tasks but lets submitted ones\nfinish; ",[24,1214,1215],{},[19,1216,1217],{},"shutdownNow()"," is aggressive — interrupts running tasks, drains the queue, and\nreturns the never-started tasks; ",[24,1220,1221],{},[19,1222,1223],{},"awaitTermination(timeout, unit)"," blocks until the\npool terminates or the timeout elapses. Neither shutdown method blocks on its own — the\ncanonical pattern combines all three:",[127,1226,1228],{"className":129,"code":1227,"language":131,"meta":132,"style":132},"void shutdownAndAwait(ExecutorService pool) {\n    pool.shutdown();                                    \u002F\u002F stop taking new tasks\n    try {\n        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {\n            pool.shutdownNow();                         \u002F\u002F force in-flight tasks\n            if (!pool.awaitTermination(60, TimeUnit.SECONDS))\n                log(\"pool did not terminate\");\n        }\n    } catch (InterruptedException ie) {\n        pool.shutdownNow();\n        Thread.currentThread().interrupt();             \u002F\u002F restore the interrupt flag\n    }\n}\n",[19,1229,1230,1240,1253,1261,1282,1296,1316,1328,1334,1353,1363,1383,1389],{"__ignoreMap":132},[136,1231,1232,1234,1237],{"class":138,"line":139},[136,1233,784],{"class":146},[136,1235,1236],{"class":174}," shutdownAndAwait",[136,1238,1239],{"class":142},"(ExecutorService pool) {\n",[136,1241,1242,1245,1247,1250],{"class":138,"line":163},[136,1243,1244],{"class":142},"    pool.",[136,1246,86],{"class":174},[136,1248,1249],{"class":142},"();                                    ",[136,1251,1252],{"class":159},"\u002F\u002F stop taking new tasks\n",[136,1254,1255,1258],{"class":138,"line":191},[136,1256,1257],{"class":146},"    try",[136,1259,1260],{"class":142}," {\n",[136,1262,1263,1266,1268,1271,1273,1275,1277,1279],{"class":138,"line":322},[136,1264,1265],{"class":146},"        if",[136,1267,83],{"class":142},[136,1269,1270],{"class":146},"!",[136,1272,840],{"class":142},[136,1274,90],{"class":174},[136,1276,178],{"class":142},[136,1278,458],{"class":181},[136,1280,1281],{"class":142},", TimeUnit.SECONDS)) {\n",[136,1283,1284,1287,1290,1293],{"class":138,"line":341},[136,1285,1286],{"class":142},"            pool.",[136,1288,1289],{"class":174},"shutdownNow",[136,1291,1292],{"class":142},"();                         ",[136,1294,1295],{"class":159},"\u002F\u002F force in-flight tasks\n",[136,1297,1298,1301,1303,1305,1307,1309,1311,1313],{"class":138,"line":359},[136,1299,1300],{"class":146},"            if",[136,1302,83],{"class":142},[136,1304,1270],{"class":146},[136,1306,840],{"class":142},[136,1308,90],{"class":174},[136,1310,178],{"class":142},[136,1312,458],{"class":181},[136,1314,1315],{"class":142},", TimeUnit.SECONDS))\n",[136,1317,1318,1321,1323,1326],{"class":138,"line":376},[136,1319,1320],{"class":174},"                log",[136,1322,178],{"class":142},[136,1324,1325],{"class":856},"\"pool did not terminate\"",[136,1327,209],{"class":142},[136,1329,1331],{"class":138,"line":1330},8,[136,1332,1333],{"class":142},"        }\n",[136,1335,1337,1340,1343,1346,1350],{"class":138,"line":1336},9,[136,1338,1339],{"class":142},"    } ",[136,1341,1342],{"class":146},"catch",[136,1344,1345],{"class":142}," (InterruptedException ",[136,1347,1349],{"class":1348},"s4XuR","ie",[136,1351,1352],{"class":142},") {\n",[136,1354,1356,1359,1361],{"class":138,"line":1355},10,[136,1357,1358],{"class":142},"        pool.",[136,1360,1289],{"class":174},[136,1362,709],{"class":142},[136,1364,1366,1369,1372,1374,1377,1380],{"class":138,"line":1365},11,[136,1367,1368],{"class":142},"        Thread.",[136,1370,1371],{"class":174},"currentThread",[136,1373,703],{"class":142},[136,1375,1376],{"class":174},"interrupt",[136,1378,1379],{"class":142},"();             ",[136,1381,1382],{"class":159},"\u002F\u002F restore the interrupt flag\n",[136,1384,1386],{"class":138,"line":1385},12,[136,1387,1388],{"class":142},"    }\n",[136,1390,1392],{"class":138,"line":1391},13,[136,1393,1394],{"class":142},"}\n",[15,1396,1397,1398,1401,1402,1404,1405,1407,1408,1411,1412,1414,1415,1418],{},"The habits that matter: ",[24,1399,1400],{},"two await phases",", escalate from ",[19,1403,86],{}," to ",[19,1406,1289],{},",\nand never swallow ",[19,1409,1410],{},"InterruptedException",". Remember ",[19,1413,1289],{}," only ",[68,1416,1417],{},"requests","\ninterruption — tasks that ignore the flag keep running. Wire this into a JVM shutdown hook\nor your framework's lifecycle.",[10,1420,1422],{"id":1421},"virtual-threads-when-sizing-stops-mattering","Virtual threads: when sizing stops mattering",[15,1424,1425,1426,1429,1430,1433,1434,1437],{},"Java 21's ",[24,1427,1428],{},"virtual threads"," (JEP 444) are lightweight threads scheduled by the JVM rather\nthan mapped 1:1 to OS threads. They cost a few hundred bytes, so you can have ",[24,1431,1432],{},"millions",",\nand a blocking call ",[24,1435,1436],{},"unmounts"," the virtual thread from its carrier OS thread instead of\nblocking it.",[127,1439,1441],{"className":129,"code":1440,"language":131,"meta":132,"style":132},"\u002F\u002F one virtual thread per task — no pooling, no sizing, no queue tuning\ntry (var executor = Executors.newVirtualThreadPerTaskExecutor()) {\n    for (var task : tasks) executor.submit(task);\n} \u002F\u002F try-with-resources auto-closes (shutdown + awaitTermination)\n",[19,1442,1443,1448,1470,1492],{"__ignoreMap":132},[136,1444,1445],{"class":138,"line":139},[136,1446,1447],{"class":159},"\u002F\u002F one virtual thread per task — no pooling, no sizing, no queue tuning\n",[136,1449,1450,1453,1455,1457,1460,1462,1464,1467],{"class":138,"line":163},[136,1451,1452],{"class":146},"try",[136,1454,83],{"class":142},[136,1456,595],{"class":146},[136,1458,1459],{"class":142}," executor ",[136,1461,147],{"class":146},[136,1463,171],{"class":142},[136,1465,1466],{"class":174},"newVirtualThreadPerTaskExecutor",[136,1468,1469],{"class":142},"()) {\n",[136,1471,1472,1475,1477,1479,1482,1484,1487,1489],{"class":138,"line":191},[136,1473,1474],{"class":146},"    for",[136,1476,83],{"class":142},[136,1478,595],{"class":146},[136,1480,1481],{"class":142}," task ",[136,1483,260],{"class":146},[136,1485,1486],{"class":142}," tasks) executor.",[136,1488,98],{"class":174},[136,1490,1491],{"class":142},"(task);\n",[136,1493,1494,1497],{"class":138,"line":322},[136,1495,1496],{"class":142},"} ",[136,1498,1499],{"class":159},"\u002F\u002F try-with-resources auto-closes (shutdown + awaitTermination)\n",[15,1501,1502,1503,1505,1506,1509,1510,1513,1514,1517,1518,1521],{},"This upends the classic advice for ",[24,1504,674],{}," work: you no longer pool threads or agonize\nover sizing — create one virtual thread per task. The caveats: ",[24,1507,1508],{},"don't pool"," virtual\nthreads, avoid ",[24,1511,1512],{},"pinning"," (long ",[19,1515,1516],{},"synchronized"," blocks or native calls keep the carrier\nblocked), and keep using ",[24,1519,1520],{},"bounded platform-thread pools for CPU-bound"," work, where\nlimiting parallelism to the core count is still exactly right.",[10,1523,1525],{"id":1524},"recap","Recap",[15,1527,1528,1529,1532,1533,1536,1537,1541,1542,1546,1547,1550,1551,1554,1555,1557,1558,1561,1562,1564,1565,1571,1572,83,1576,1181,1578,1181,1580,1181,1582,1181,1585,1587,1588,1591,1592,1594],{},"The Executor framework exists to ",[24,1530,1531],{},"decouple submission from execution"," and give you\n",[24,1534,1535],{},"reuse plus bounding",". Program against ",[24,1538,1539],{},[19,1540,78],{},", but build pools with an\nexplicit ",[24,1543,1544],{},[19,1545,220],{}," so the queue bound, max threads, and ",[24,1548,1549],{},"rejection policy","\nare deliberate — the factories' unbounded defaults are an OOM waiting to happen. Understand\nthe ",[24,1552,1553],{},"core → queue → max → reject"," flow (and why an unbounded queue neuters\n",[19,1556,404],{},"), size pools by ",[24,1559,1560],{},"CPU-bound vs IO-bound",", and prefer ",[19,1563,98],{}," +\n",[24,1566,1567,1181,1569],{},[19,1568,801],{},[19,1570,108],{}," when you need results and visible failures. Compose async work with\n",[24,1573,1574],{},[19,1575,1012],{},[19,1577,1022],{},[19,1579,1031],{},[19,1581,1045],{},[19,1583,1584],{},"allOf",[19,1586,1125],{},")\non your own executor, always shut pools down with the ",[24,1589,1590],{},"two-phase graceful pattern",", and\non Java 21+ reach for ",[24,1593,1428],{}," to make IO-bound sizing a non-problem.",[1596,1597,1598],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":132,"searchDepth":163,"depth":163,"links":1600},[1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611],{"id":12,"depth":163,"text":13},{"id":46,"depth":163,"text":47},{"id":224,"depth":163,"text":225},{"id":391,"depth":163,"text":392},{"id":525,"depth":163,"text":526},{"id":656,"depth":163,"text":657},{"id":770,"depth":163,"text":771},{"id":998,"depth":163,"text":999},{"id":1195,"depth":163,"text":1196},{"id":1421,"depth":163,"text":1422},{"id":1524,"depth":163,"text":1525},"A deep guide to Java concurrency with executors — the Executor framework, tuning ThreadPoolExecutor, Callable and Future, composing async work with CompletableFuture, graceful shutdown, and virtual threads.","hard","md","Java",{},"\u002Fblog\u002Fjava-executors-thread-pools","\u002Fjava\u002Fconcurrency\u002Fexecutors-thread-pools",{"title":5,"description":1612},"blog\u002Fjava-executors-thread-pools","Executors & Thread Pools","Concurrency","concurrency","2026-06-20","0cFyNmqj4pAosW2_V0Kzny4UokxCdR1d9BajA7H8oAA",1782244090110]