├── .gitattributes ├── .gitignore ├── README.md ├── build.gradle ├── cachedThreadPool_no_shutdown.png ├── fixedThreadPool_no_shutdown.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── kr └── pe └── kwonnam └── java_spring_threadpool ├── PoolStrategy.java ├── ThreadPoolTester.java ├── TooManyCachedThreadPoolTester.java └── TooManyFixedThreadPoolTester.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | build/ 3 | .gradle/ 4 | gradle/ 5 | out/ 6 | .git/ 7 | .idea/ 8 | *.log 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java 와 SpringFramework 의 Thread Pool 작동 방식 테스트 2 | * Java의 `Executors.newCachedThreadPool()`과 `Executors.newFixedThreadPool(갯수)` 3 | * SpringFramework 의 `ThreadPoolTaskExecutor` 설정에 따른 작동 방식 변화. 4 | * 위 두가지를 실제 코드로 확인해보기. 5 | * [kwonnam wiki - Spring @Async](https://kwonnam.pe.kr/wiki/springframework/async) 6 | * [kwonnam wiki - Java ExecutorService](https://kwonnam.pe.kr/wiki/java/concurrent/executorservice) 7 | 8 | ## 최대 쓰레드 갯수 9 | * [Java: What is the limit to the number of threads you can create? | Vanilla #Java](http://vanillajava.blogspot.com/2011/07/java-what-is-limit-to-number-of-threads.html) 10 | * 위 문서에 따르면 64Bit Linux 에서 32,072 개의 쓰레드가 생성가능함. 11 | 12 | ## java.util.concurrent.ThreadPoolExecutor 13 | * [java.util.concurrent.ThreadPoolExecutor](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html) 14 | * [Executors](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Executors.html) 와 15 | SpringFramework의 [ThreadPoolTaskExecutor](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html) 가 16 | 사용하는 쓰레드 풀 구현체 17 | * 기본적으로 `corePoolSize` 만큼의 쓰레드를 만들고, 18 | * `corePool`이 꽉차면 `workQueue`(`queueCapacity` 만큼의 크기로 된 큐)에 넣는데 19 | * `workQueue`조차도 꽉차면 그제서야 `maxPoolSize` 까지 쓰레드를 생성해가면서 작업. 20 | * 따라서 `corePoolSize`가 0이 아니고 일정 수준 이상되고 `queueCapacity`가 매우 크다면(보통 `Integer.MAX_VALUE`) 별다른 문제가 없는한 21 | 쓰레드 풀의 크기는 `corePoolSize`를 넘길 수 없다. 22 | 23 | ## ThreadPoolTaskExecutor 를 CachedThreadPool 처럼 사용하는 방법 24 | * `corePoolSize` : `0` 25 | * `maxPoolSize` : `Integer.MAX_VALUE` 26 | * `queueCapacity` : `0` 27 | 28 | ## ThreadPoolTaskExecutor 를 FixedThreadPool 처럼 사용하는 방법 29 | * `corePoolSize` : 원하는 고정 크기 쓰레드 갯수 30 | * `maxPoolSize` : `corePoolSize`와 동일하게. 31 | * `queueCapacity` : `Integer.MAX_VALUE` 32 | * 위와 같이 설정하면 실제로는 `corePoolSize` 만큼만 쓰레드가 생성된다. 33 | * 만약 쓰레드가 적체되어 `corePoolSize` 이상의 작업이 들어오면 `workQueue` 에 `queueCapacity`만큼 들어가고, 34 | `corePool` 에 남는 자리가 생기면 `workQueue`에 있던것이 들어간다. 35 | * `queueCapacity=Integer.MAX_VALUE`일 경우에는 여기까지 가는 것은 불가능하다고 보는게 맞다. 36 | 만약 `queueCapacity`를 넘어간다면 이미 그 자체로 커다란 문제가 발생한 것이다. 37 | 38 | ## 결론 부터 먼저 39 | * `Executors.newCachedThreadPool()` 혹은 `ThreadPoolTaskExecutor`를 CachedThreadPool과 유사하게 설정하면 40 | 쓰레드의 작업이 적체될 경우 시스템 한계치에 달하는 쓰레드를 생성하다가 죽어버린다. 41 | * 따라서, 42 | * cachedThreadPool 이 필요한 경우 43 | * **명확하게 정말 빠르게 끝나는 task 만 할당하는게 확실할 경우**에는 cachedThreadPool 혹은 이에 준하는 설정이 낫다. 44 | * cachedThreadPool 은 **항상 필요한 만큼만 쓰레드를 생성하고, 불필요해지면 자동으로 쓰레드를 반환**하므로 45 | 최적 상태가 된다. 46 | * **지연이 발생할 가능성이 있다면 cachedThreadPool 의 경우 Java 프로세스가 수만개의 쓰레드를 생성하다가 죽을 수 있다.** 47 | * 쓰레드 작업에 적체가 발생할 가능성이 큰 경우에는 fixedThreadPool을 사용하는게 나아보인다. 48 | * `Executors.newFixedThreadPool(적당한쓰레드갯수)` 를 사용하거나, 49 | * `ThreadPoolTaskExecutor`를 위에 설명한 대로 설정한다. 50 | * 단점은, 일단 `corePoolSize` 만큼의 쓰레드가 생성되면 불필요하게 항상 고정 크기 쓰레드가 생성된 상태로 유지된다. 51 | 실제로 사용되지 않아도 유지된다. 52 | * 쓰레드 생성요청이 매우 많이 들어와도 애플리케이션이 죽지는 않지만 해당 쓰레드풀을 사용하는 작업이 53 | 매우 느려지기만 한다. 54 | * SpringFramework 에서는 `ThreadPoolTaskExecutor`를 사용한다. 55 | * Spring 이 자동으로 bean lifecycle 을 관리해준다. 56 | * 따라서 애플리케이션 종료시 shutdown 을 해준다. 57 | 58 | ## EXECUTOR_SERVICE_CACHED 59 | * `Executors.newCachedThreadPool()` 사용. 60 | ``` 61 | ./gradlew run --args="EXECUTOR_SERVICE_CACHED" 62 | ``` 63 | * 결과 64 | * 총 쓰레드 32600 개를 생성하고 죽음. 가끔씩 task가 처리되는 시간에 따라 65 | 안죽을 때도 있으나 쓰레드를 수만개 생성해서 메모리가 폭주하는 것은 마찬가지임. 66 | * 일부 쓰레드 작업을 마쳤으나 대부분 `sleep interrupted` 67 | * `# after thread generation ...`, `# The end` 출력안됨. 즉, 쓰레드 생성 반복문을 마치지도 못했음. 68 | * 결과 출력 69 | ``` 70 | # current thread [pool-1-thread-32597] idx : 32596, current active thread count 32599 71 | # current thread [pool-1-thread-32598] idx : 32597, current active thread count 32600 72 | # 73 | # There is insufficient memory for the Java Runtime Environment to continue. 74 | # Native memory allocation (mmap) failed to map 12288 bytes for committing reserved memory. 75 | # An error report file with more information is saved as: 76 | # .../java-spring-thread-pool-test/hs_err_pid945971.log 77 | [thread 140060855953152 also had an error] 78 | 79 | OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00007f58dad0b000, 12288, 0) failed; error='메모리를 할당할 수 없습니다' (errno=12) 80 | Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread 81 | at java.lang.Thread.start0(Native Method) 82 | at java.lang.Thread.start(Thread.java:717) 83 | at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:957) 84 | at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1378) 85 | at kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester.main(ThreadPoolTester.java:43) 86 | OpenJDK 64-Bit Server VM warning: Attempt to deallocate stack guard pages failed. 87 | OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00007f627580f000, 12288, 0) failed; error='메모리를 할당할 수 없습니다' (errno=12) 88 | 89 | # shutting down executor 90 | 91 | ``` 92 | * 참고 93 | * `ExecutorService.newCachedThreadPool()` 는 `corePoolSize=0`, `maxPoolSize=Integer.MAX_VALUE`, workQueue 로 `SynchronousQueue`를 사용하는데, 94 | * 이는 항상 poll 해가는 쓰레드가 존재할 때만 insert 를 할 수 있는 큐이다(queue size를 항상 0으로 유지). 95 | * 즉, 비록 corePoolSize 가 0 이라해도 뭔가 쓰레드 생성을 요청하는 순간 queue 에 넣고 빼가는게 즉시 이뤄져서 96 | 항상 필요한만큼의 쓰레드가 즉시 생성된다. 97 | ``` 98 | return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 99 | 60L, TimeUnit.SECONDS, 100 | new SynchronousQueue()); 101 | ``` 102 | ## EXECUTOR_SERVICE_FIXED_1000 103 | * `Executors.newFixedThreadPool(1000)` 사용. 104 | ``` 105 | ./gradlew run --args="EXECUTOR_SERVICE_FIXED_1000" 106 | ``` 107 | * 결과 108 | * 쓰레드 생성 반복문 직후 찍는 `after thread generation ...`가 1000개의 쓰레드의 시작 실행 구문이 찍힌 뒤에 109 | 출력된 것으로 보아, **workQueue 가 존재하여 이미 모든 쓰레드 정보는 queue에 다들어간 생태** 임을 알 수 있다. 110 | * 계속 `1001`개의 쓰레드 갯수를 유지했다. 111 | * 느리지만 끝까지 문제 없이 실행됐다. 112 | * 참고 : `workQueue` 113 | * `newFixedThreadPool()` 은 내부적으로 `workQueue` 를 다음과 같이 생성하는데, 114 | 해당 코드를 보면 `Integer.MAX_VALUE` 크기의 큐를 생성하는 것을 볼 수 있다. 115 | 116 | ``` 117 | // java.util.concurrent.LinkedBlockingQueue.LinkedBlockingQueue() 118 | 119 | /** 120 | * Creates a {@code LinkedBlockingQueue} with a capacity of 121 | * {@link Integer#MAX_VALUE}. 122 | */ 123 | public LinkedBlockingQueue() { 124 | this(Integer.MAX_VALUE); 125 | } 126 | ``` 127 | ``` 128 | # current thread [pool-1-thread-999] idx : 998, current active thread count 1001 129 | # current thread [pool-1-thread-1000] idx : 999, current active thread count 1001 130 | # after thread generation ... 131 | # current thread [pool-1-thread-1] idx : 0, , current active thread count 1001, countDownLatch : 49998 END 132 | # current thread [pool-1-thread-27] idx : 26, , current active thread count 1001, countDownLatch : 49986 END 133 | .... 134 | # current thread [pool-1-thread-970] idx : 49997, , current active thread count 1001, countDownLatch : 2 END 135 | # current thread [pool-1-thread-643] idx : 49998, , current active thread count 1001, countDownLatch : 1 END 136 | # The end 137 | # shutting down executor 138 | # current thread [pool-1-thread-247] idx : 49999, , current active thread count 1001, countDownLatch : 0 END 139 | ``` 140 | 141 | ## THREAD_POOL_TASK_EXECUTOR_CORE_POOL_SIZE_AND_QUEUE_0 142 | * `ThreadPoolTaskExecutor` == `Executors.newCachedThreadPool()` 유사한 설정 143 | * `corePoolSize` : `0` - 쓰레드 풀이 반환되면 일정 시간 기다렸다가 0개로 줄인다. 144 | * `maxPoolSize` : `Integer.MAX_VALUE` 145 | * `queueCapacity` : `0` - 큐가 없으므로 `maxPoolSize`만큼 즉각 증가시킨다. 146 | ``` 147 | ./gradlew run --args="THREAD_POOL_TASK_EXECUTOR_CORE_POOL_SIZE_AND_QUEUE_0" 148 | ``` 149 | * 결과 150 | * 총 쓰레드 32596 개를 생성하고 죽음. 151 | * 한개의 쓰레드도 작업을 마치지 못함. 152 | * `# after thread generation ...` 출력안됨. 즉, 쓰레드 생성 반복문을 마치지도 못했음. 153 | * `Executors.newCachedThreadPool()`과 동일한 결과 154 | 155 | ``` 156 | # current thread [MYTHREADPOOL-32589] idx : 32588, current active thread count 32591 157 | # current thread [MYTHREADPOOL-32590] idx : 32589, current active thread count 32592 158 | OpenJDK 64-Bit Server VM warning: Attempt to protect stack guard pages failed. 159 | # current thread [MYTHREADPOOL-32591] idx : 32590, current active thread count 32593 160 | OpenJDK 64-Bit Server VM warning: Attempt to protect stack guard pages failed. 161 | # current thread [MYTHREADPOOL-32592] idx : 32591, current active thread count 32594 162 | OpenJDK 64-Bit Server VM warning: Attempt to protect stack guard pages failed. 163 | # current thread [MYTHREADPOOL-32594] idx : 32593, current active thread count 32596 164 | OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00007f74311b0000, 12288, 0) failed; error='메모리를 할당할 수 없습니다' (errno=12) 165 | # 166 | # There is insufficient memory for the Java Runtime Environment to continue. 167 | # Native memory allocation (mmap) failed to map 12288 bytes for committing reserved memory. 168 | # An error report file with more information is saved as: 169 | # .../java-spring-thread-pool-test/hs_err_pid111281.log 170 | [thread 140137015731968 also had an error] 171 | OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00007f7430fae000, 12288, 0) failed; error='메모리를 할당할 수 없습니다' (errno=12) 172 | # shutting down executor 173 | [thread 140178252187392 also had an error] 174 | 175 | ``` 176 | 177 | ## THREAD_POOL_TASK_EXECUTOR_QUEUE_INTMAX 178 | * `ThreadPoolTaskExecutor` == `Executors.newFixedThreadPool()` 유사한 설정 179 | * `corePoolSize` : `1000` - 쓰레드 풀 항상 1000개 유지 180 | * `maxPoolSize` : `1000` 181 | * `queueCapacity` : `Integer.MAX_VALUE` 182 | ``` 183 | ./gradlew run --args="THREAD_POOL_TASK_EXECUTOR_QUEUE_INTMAX" 184 | ``` 185 | 186 | * 결과 187 | * `idx: 999` 에서 `# after thread generation ...` 가 출렸됐다는 것은 이 시점에는 이미 188 | 쓰레드 풀 `workQueue` 에 모든 요청이 들어갔다는 의미임. 189 | * 최대 쓰레드 갯수 1001개를 유지하면서 느리지만 모든 작업을 무사히 마침. 190 | * `Executors.newFixedThreadPool(1000)`와 동일한 결과 191 | 192 | ``` 193 | # current thread [MYTHREADPOOL-999] idx : 998, current active thread count 1001 194 | # current thread [MYTHREADPOOL-1000] idx : 999, current active thread count 1001 195 | # after thread generation ... 196 | # current thread [MYTHREADPOOL-1] idx : 0, , current active thread count 1001, countDownLatch : 49999 END 197 | # current thread [MYTHREADPOOL-3] idx : 2, , current active thread count 1001, countDownLatch : 49998 END 198 | # current thread [MYTHREADPOOL-1] idx : 1000, current active thread count 1001 199 | 200 | .... 201 | # current thread [MYTHREADPOOL-572] idx : 49997, , current active thread count 1001, countDownLatch : 2 END 202 | # current thread [MYTHREADPOOL-984] idx : 49998, , current active thread count 1001, countDownLatch : 1 END 203 | # current thread [MYTHREADPOOL-507] idx : 49999, , current active thread count 1001, countDownLatch : 0 END 204 | # The end 205 | # shutting down executor 206 | ``` 207 | 208 | ## THREAD_POOL_TASK_EXECUTOR_MAX_INTMAX_QUEUE_40000 209 | * `corePoolSize`, `maxPoolSize`, `queueCapacity`의 관계를 보여주는 예제. 210 | * `corePoolSize` : `1000` 211 | * `maxPoolSize` : `Integer.MAX_VALUE` 212 | * `queueCapacity` : `40,000` 213 | ``` 214 | ./gradlew run --args="THREAD_POOL_TASK_EXECUTOR_MAX_INTMAX_QUEUE_40000" 215 | ``` 216 | 217 | * 결과 218 | * 작업을 총 50,000개 생성하는데, `idx=999`까지만 쓰레드를 생성하다가 그 이후에는 queue에 넣다가 219 | * queue 가 꽉차는 41,000 개 째부터 더이상 queue에 넣을 수 없어서 `maxPoolSize=Integer.MAX_VALUE`에 따라 Thread를 220 | 생성하기 시작하는 것을 볼 수 있다. 221 | * 쓰레드가 `총 생성 작업갯수(50,000)-queueSize(40,000)`즈음인 `10001`에 이르자, queue도 꽉차고 queue에 못 넣은 것은 222 | corePoolSize를 넘어서는 갯수의 쓰레드를 생성함으로써 223 | 모두 다 할당이 완료되었기 때문에 그 순간 `# after thread generation ...`이 출력되면서 224 | 모든 작업을 threadPool에 넣는 것이 완료됨을 볼 수 있다. 225 | * 쓰레드 갯수가 한계 수치인 `30,000`개 이상까지 가지 않고 `10,001`에 계속 머물렀기 때문에 모든 작업을 완료하였다. 226 | 227 | ``` 228 | # current thread [MYTHREADPOOL-997] idx : 996, current active thread count 999 229 | # current thread [MYTHREADPOOL-998] idx : 997, current active thread count 1000 230 | # current thread [MYTHREADPOOL-999] idx : 998, current active thread count 1001 231 | # current thread [MYTHREADPOOL-1000] idx : 999, current active thread count 1001 232 | 233 | # current thread [MYTHREADPOOL-1001] idx : 41000, current active thread count 1003 234 | # current thread [MYTHREADPOOL-1002] idx : 41001, current active thread count 1004 235 | # current thread [MYTHREADPOOL-1003] idx : 41002, current active thread count 1005 236 | # current thread [MYTHREADPOOL-1004] idx : 41003, current active thread count 1006 237 | .... 238 | # current thread [MYTHREADPOOL-9998] idx : 49997, current active thread count 10000 239 | # current thread [MYTHREADPOOL-9999] idx : 49998, current active thread count 10001 240 | # after thread generation ... 241 | # current thread [MYTHREADPOOL-10000] idx : 49999, current active thread count 10001 242 | # current thread [MYTHREADPOOL-1] idx : 0, , current active thread count 10001, countDownLatch : 49999 END 243 | # current thread [MYTHREADPOOL-5] idx : 4, , current active thread count 10001, countDownLatch : 49998 END 244 | 245 | .... 246 | # current thread [MYTHREADPOOL-8673] idx : 40950, , current active thread count 10001, countDownLatch : 38 END 247 | # current thread [MYTHREADPOOL-8663] idx : 40952, , current active thread count 10001, countDownLatch : 39 END 248 | # current thread [MYTHREADPOOL-8660] idx : 40961, , current active thread count 10001, countDownLatch : 40 END 249 | # current thread [MYTHREADPOOL-8657] idx : 40959, , current active thread count 10001, countDownLatch : 42 END 250 | # The end 251 | # shutting down executor 252 | # current thread [MYTHREADPOOL-8665] idx : 40958, , current active thread count 10001, countDownLatch : 43 END 253 | # current thread [MYTHREADPOOL-8654] idx : 40957, , current active thread count 10001, countDownLatch : 44 END 254 | # current thread [MYTHREADPOOL-8662] idx : 40955, , current active thread count 10001, countDownLatch : 46 END 255 | # current thread [MYTHREADPOOL-8668] idx : 40948, , current active thread count 10001, countDownLatch : 47 END 256 | # current thread [MYTHREADPOOL-8658] idx : 40949, , current active thread count 10001, countDownLatch : 49 END 257 | # current thread [MYTHREADPOOL-8633] idx : 40999, , current active thread count 10001, countDownLatch : 0 END 258 | # current thread [MYTHREADPOOL-8637] idx : 40997, , current active thread count 10001, countDownLatch : 2 END 259 | ``` 260 | 261 | ## THREAD_POOL_TASK_EXECUTOR_MAX_LIMITED_QUEUE_0 262 | * `maxPoolSize`에 다다르면 예외가 발생함을 보여주는 예제. queue를 없애고, maxPoolSize를 작게 잡았다. 263 | * `corePoolSize` : `1000` 264 | * `maxPoolSize` : `2000` 265 | * `queueCapacity` : `0` 266 | 267 | ``` 268 | ./gradlew run --args="THREAD_POOL_TASK_EXECUTOR_MAX_LIMITED_QUEUE_0" 269 | ``` 270 | 271 | * 결과 272 | * 첫 코드는 `try/catch`로 감싸지 않았는데, 2000개의 작업을 넣고나서 멈춰버렸다. 273 | * `# after thread generation ...`, `# The end` 둘다 출력이 안된다. 274 | * `finally` block 의 `# shutting down executor`만 출력됐다. 즉, 예외가 발생했음을 뜻한다. 275 | * 2000개 이상 작업을 넣으려고 하면 예외가 발생한다. 276 | * `Exception in thread "main" org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@479d31f3[Running, pool size = 2000, active threads = 2000, queued tasks = 0, completed tasks = 0]] did not accept task: kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester$$Lambda$3/1598924227@333291e3` 277 | * `Caused by: java.util.concurrent.RejectedExecutionException: Task kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester$$Lambda$3/1598924227@333291e3 rejected from java.util.concurrent.ThreadPoolExecutor@479d31f3[Running, pool size = 2000, active threads = 2000, queued tasks = 0, completed tasks = 0]` 278 | * `shutdown`이 호출됐기 때문에 이미 쓰레드 풀에 들어간 작업은 수행이 잘 종료됐고, active thread count 도 줄어들었다. 279 | * (처음에 try/finally로 감싸기 전에는) `main` 쓰레드가 Lock 대기 상태에 들어갔다. 280 | * 이미 main 쓰레드에서 `try/catch` 없이 Exception 이 발생했으므로 `CountDownLatch.await()` 때문은 아니다. 281 | * `main` 쓰레드는 이미 종료되었고 `DestroyJavaVM` 쓰레드가 활성화된 상태로 Lock 대기 상태에 빠짐. 282 | * `try/finally` 블럭으로 감싸고, finally 에서 executor shutdown을 해주면 잘 종료된다. 283 | 284 | ``` 285 | # current thread [MYTHREADPOOL-1998] idx : 1997, current active thread count 2000 286 | # current thread [MYTHREADPOOL-1999] idx : 1998, current active thread count 2001 287 | # current thread [MYTHREADPOOL-2000] idx : 1999, current active thread count 2001 288 | # shutting down executor 289 | ... 290 | # current thread [MYTHREADPOOL-1994] idx : 1993, , current active thread count 8, countDownLatch : 48006 END 291 | # current thread [MYTHREADPOOL-1995] idx : 1994, , current active thread count 7, countDownLatch : 48005 END 292 | # current thread [MYTHREADPOOL-1996] idx : 1995, , current active thread count 6, countDownLatch : 48004 END 293 | # current thread [MYTHREADPOOL-1997] idx : 1996, , current active thread count 5, countDownLatch : 48003 END 294 | # current thread [MYTHREADPOOL-1998] idx : 1997, , current active thread count 4, countDownLatch : 48002 END 295 | # current thread [MYTHREADPOOL-1999] idx : 1998, , current active thread count 3, countDownLatch : 48001 END 296 | # current thread [MYTHREADPOOL-2000] idx : 1999, , current active thread count 2, countDownLatch : 48000 END 297 | Exception in thread "main" org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@479d31f3[Running, pool size = 2000, active threads = 2000, queued tasks = 0, completed tasks = 0]] did not accept task: kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester$$Lambda$3/1598924227@333291e3 298 | ┆...at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.execute(ThreadPoolTaskExecutor.java:324) 299 | ┆...at kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester.main(ThreadPoolTester.java:45) 300 | Caused by: java.util.concurrent.RejectedExecutionException: Task kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester$$Lambda$3/1598924227@333291e3 rejected from java.util.concurrent.ThreadPoolExecutor@479d31f3[Running, pool size = 2000, active threads = 2000, queued tasks = 0, completed tasks = 0] 301 | ┆...at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) 302 | ┆...at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) 303 | ┆...at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) 304 | ┆...at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.execute(ThreadPoolTaskExecutor.java:321) 305 | ┆...... 1 more 306 | ``` 307 | 308 | ## FORK_JOIN_COMMON_POOL 309 | * `java.util.concurrent.ForkJoinPool.commonPool()` 310 | * 아무 설정이 없으면 CPU Thread 갯수만큼의 크기(`Runtime.getRuntime().availableProcessors()`)로 쓰레드 풀을 생성한다. 311 | * `Intel(R) Core(TM) i7-9750H CPU` 에서 12 개 크기로 생성됨. 312 | * 너무 적게 생성되고 자동 확장이 일어나지 않으므로 주의해서 사용한다. 313 | * System Property `java.util.concurrent.ForkJoinPool.common.parallelism=1000` 형태로 쓰레드 풀의 크기를 명시할 수 있다. 314 | 315 | ``` 316 | ./gradlew run --args="FORK_JOIN_COMMON_POOL" 317 | ``` 318 | * 결과 319 | * 11개 정도의 쓰레드가 시작되자 `# after thread generation ...` 이 출력되었으므로, Thread 에 `workQueue`가 존재하고 모든 요청이 큐에 적재됐음을 뜻한다. 320 | * `current active thread count 12` 로 항상 12개의 쓰레드를 유지하고 있다. 321 | * main 포함 13개여야 하는데 12개만 active 이고, 다른 작업이 끝난 뒤에 idx 12 번이 `# current thread [ForkJoinPool.commonPool-worker-15] idx : 12, current active thread count 12` 이렇게 시작되었다. 322 | 즉, 쓰레드 풀이 실제로는 11개만 작동하고 있다. 323 | * `# The end`와 `# shutting down executor` 마지막에 함께 출력되면서 무사히 작어을 마쳤다. 324 | ``` 325 | # Starting with Thread Pool FORK_JOIN_COMMON_POOL 326 | current java.util.concurrent.ForkJoinPool.common.parallelism : null 327 | # current thread [ForkJoinPool.commonPool-worker-9] idx : 0, current active thread count 6 328 | # current thread [ForkJoinPool.commonPool-worker-2] idx : 1, current active thread count 6 329 | # current thread [ForkJoinPool.commonPool-worker-11] idx : 2, current active thread count 6 330 | # current thread [ForkJoinPool.commonPool-worker-6] idx : 5, current active thread count 12 331 | # current thread [ForkJoinPool.commonPool-worker-13] idx : 4, current active thread count 11 332 | # current thread [ForkJoinPool.commonPool-worker-4] idx : 3, current active thread count 10 333 | # current thread [ForkJoinPool.commonPool-worker-8] idx : 6, current active thread count 10 334 | # current thread [ForkJoinPool.commonPool-worker-3] idx : 10, current active thread count 12 335 | # current thread [ForkJoinPool.commonPool-worker-10] idx : 9, current active thread count 12 336 | # current thread [ForkJoinPool.commonPool-worker-1] idx : 8, current active thread count 12 337 | # current thread [ForkJoinPool.commonPool-worker-15] idx : 7, current active thread count 12 338 | # after thread generation ... 339 | # current thread [ForkJoinPool.commonPool-worker-9] idx : 0, , current active thread count 12, countDownLatch : 49997 END 340 | # current thread [ForkJoinPool.commonPool-worker-15] idx : 7, , current active thread count 12, countDownLatch : 49989 END 341 | # current thread [ForkJoinPool.commonPool-worker-15] idx : 12, current active thread count 12 342 | 343 | ..... 344 | 345 | # current thread [ForkJoinPool.commonPool-worker-11] idx : 49997, , current active thread count 12, countDownLatch : 2 END 346 | # current thread [ForkJoinPool.commonPool-worker-2] idx : 49998, , current active thread count 12, countDownLatch : 1 END 347 | # current thread [ForkJoinPool.commonPool-worker-6] idx : 49999, , current active thread count 12, countDownLatch : 0 END 348 | # The end 349 | # shutting down executor 350 | ``` 351 | 352 | ## FORK_JOIN_COMMON_POOL_PARALLELISM_1000 353 | * `java.util.concurrent.ForkJoinPool.commonPool()` 이지만 `parallelism` 을 `1000` 으로 지정하였다. 354 | * `System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "1000");` 355 | ``` 356 | ./gradlew run --args="FORK_JOIN_COMMON_POOL_PARALLELISM_1000" 357 | ``` 358 | 359 | * 결과 360 | * 실행순서가 상당히 뒤죽박죽이다. 361 | * 다른 쓰레드 풀(`ThreadPoolExecutor` 기반)은 Java Heap Size와 무관하게 작동하였는데, `ForkJoinPool`은 Heap Size에 따라 메모리 할당을 못하는 현상이 발생하였다. 362 | * Pool 사이즈가 `1000`이 되기 이전에 `# after thread generation ...`가 출력되었다. 363 | * `# after thread generation ...` 가 출력되었으므로 `workQueue`에 모든 작업이 들어갔다. 364 | * 중간에 Memory 부족이 발생했으나, 일부 쓰레드만 실패하고, 계속 진행되었다. 365 | * 계속 작업은 진행되고 시스템 다운도 안되지만, `# The end` 는 출력되지 않는다. 366 | * `OutOfMemory`로 실행되지 못한 쓰레드들이 `CountDownLatch`를 제대로 줄여주지 못해서 `countDownLatch.await();` 부분에 영원히 멈춰있게 된다. 367 | * `java.util.concurrent.CountDownLatch.await(CountDownLatch.java:231)` 368 | * `-Xmx4g` 옵션을 주고 실행하면 모두 정상 실행 및 종료된다. 369 | ``` 370 | # 최종 멈춘 상태에서의 main thread dump 371 | "main" #1 prio=5 os_prio=0 tid=0x00007f2c6800c000 nid=0xb39b waiting on condition [0x00007f2c6d051000] 372 | java.lang.Thread.State: WAITING (parking) 373 | at sun.misc.Unsafe.park(Native Method) 374 | - parking to wait for <0x00000000fe2e62d0> (a java.util.concurrent.CountDownLatch$Sync) 375 | at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 376 | at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836) 377 | at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:997) 378 | at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1304) 379 | at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:231) 380 | at kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester.main(ThreadPoolTester.java:61) 381 | ``` 382 | 383 | ``` 384 | # Starting with Thread Pool FORK_JOIN_COMMON_POOL_PARALLELISM_1000 385 | current java.util.concurrent.ForkJoinPool.common.parallelism : 1000 386 | # current thread [ForkJoinPool.commonPool-worker-441] idx : 0, current active thread count 5 387 | # current thread [ForkJoinPool.commonPool-worker-740] idx : 3, current active thread count 7 388 | # current thread [ForkJoinPool.commonPool-worker-15] idx : 6, current active thread count 10 389 | # current thread [ForkJoinPool.commonPool-worker-299] idx : 2, current active thread count 6 390 | 391 | # current thread [ForkJoinPool.commonPool-worker-247] idx : 46, current active thread count 56 392 | # current thread [ForkJoinPool.commonPool-worker-546] idx : 51, current active thread count 59 393 | # after thread generation ... 394 | # current thread [ForkJoinPool.commonPool-worker-688] idx : 48, current active thread count 59 395 | # current thread [ForkJoinPool.commonPool-worker-120] idx : 54, current active thread count 60 396 | 397 | ... 398 | Exception in thread "ForkJoinPool.commonPool-worker-918" Exception in thread "ForkJoinPool.commonPool-worker-335" Exception in thread "ForkJoinPool.commonPool-worker-193" Exception in thread "ForkJoinPool.commonPool-worker-776" # current thread [ForkJoinPool.commonPool-worker-604] idx : 827, current active thread count 836 399 | Exception in thread "ForkJoinPool.commonPool-worker-634" # current thread [ForkJoinPool.commonPool-worker-477] idx : 836, current active thread count 843 400 | # current thread [ForkJoinPool.commonPool-worker-36] idx : 835, current active thread count 842 401 | # current thread [ForkJoinPool.commonPool-worker-21] idx : 830, current active thread count 842 402 | # current thread [ForkJoinPool.commonPool-worker-903] idx : 832, current active thread count 841 403 | java.lang.OutOfMemoryError: Java heap space# current thread [ForkJoinPool.commonPool-worker-619] idx : 404 | 833, current active thread count 841 405 | # current thread [ForkJoinPool.commonPool-worker-178] idx : 834, current active thread count java.lang.OutOfMemoryError: Java heap space 406 | java.lang.OutOfMemoryError: Java heap space 407 | 841 408 | # current thread [ForkJoinPool.commonPool-worker-320 at java.util.concurrent.ForkJoinPool$WorkQueue.growArray(ForkJoinPool.java:886) 409 | ] idx : at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1687) 410 | at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157) 411 | 829, current active thread count 837 412 | java.lang.OutOfMemoryError: Java heap space 413 | # current thread [ForkJoinPool.commonPool-worker-462] idx : at java.util.concurrent.ForkJoinPool$WorkQueue.growArray(ForkJoinPool.java:886) 414 | 828, current active thread count at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1687) 415 | at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157) 416 | 837 417 | # current thread [ForkJoinPool.commonPool-worker-761] idx : 831, current active thread count 837 418 | java.lang.OutOfMemoryError: Java heap space 419 | # current thread [ForkJoinPool.commonPool-worker-441] idx : 0, , current active thread count 838, countDownLatch : 49998 END 420 | 421 | # current thread [ForkJoinPool.commonPool-worker-845] idx : 49891, , current active thread count 906, countDownLatch : 533 END 422 | # current thread [ForkJoinPool.commonPool-worker-660] idx : 49892, , current active thread count 906, countDownLatch : 532 END 423 | ``` 424 | 425 | ## Thread 가 생성하는 메모리는 어디에? 426 | * [memory - How does Java (JVM) allocate stack for each thread - Stack Overflow](https://stackoverflow.com/questions/36898701/how-does-java-jvm-allocate-stack-for-each-thread) 427 | * Thread Stack 메모리는 Heap 과 무관한 별도의 영역에 생긴다. 428 | * Linux 64bit 에서는 기본 Thread 하나당 1024kb(1mb)를 차지한다. 429 | * 따라서 heap 사이즈를 매우 작게 해도 Thread 를 수만개 생성할 수 있다. 단, 쓰레드에서 힙을 적게 사용할 때한해서. 430 | 431 | ## 어째서 ForkJoinPool 에서 Java Heap OutOfMemory가 발생했나? 432 | * `ForkJoinPool` 에서 Pool size가 1000 밖에 안되는데도 Java Heap OutOfMemory 가 발생했는데, 이는 `-Xmx32m` 설정 때문이다. 433 | * Heap 이 너무 작았는데, `ForkJoinPool` 이 `workQueue`에 넣는 `ForkJoinTask`가 약 32mb 저도를 차지했기 때문에 발생했다. 434 | * 일반적으로 64mb 이상의 Heap 만 할당했다고 하더라도 발생할 수 없는 오류였다. 435 | * 따라서 `ForkJoinPool`이 쓰레드 생성을 위해서 heap 을 매번 사용한다고 생각할 필요는 없다. 436 | * `ThreadPoolExecutor`도 `workQueue`를 `LinkedBlockingQueue` 로 생성하고 `LinkedBlockingQueue#Node` 객체를 생성하는데, 437 | 이게 용량을 `ForkJoinTask` 보다 적게 사용하는 편이어서 `-Xmx32m`으로도 충분했다. 438 | 439 | 440 | ## shutdown 없이 Thread Pool 자체를 계속 생성하면? 441 | * 결론부터 442 | * Thread Pool 은 목적에 따라 특정 갯수만 생성해서 계속해서 재사용하는 것이지, 매번 필요할 때마다 생성하는게 아니다. 443 | * Thread Pool 자체를 올바로 shutdown 하지 않고 계속 생성하면 제한 갯수에 다다르면 시스템이 다운되고, 444 | * cahcedThreadPool 은 사용하지 않는 쓰레드를 반환하지만 어쨌든 이미 생성한 쓰레드를 재사용하지 않는 것은 Pool 의 개념에 어긋나 성능이 저하될 것이고, 445 | * fixedThreadPool 은 이미 생성된 쓰레드를 반환하지 않기 때문에 결국 제한 갯수에 다다라서 crash 가 발생하게 될 것이다. 446 | 447 | ### TooManyCachedThreadPoolTester 448 | * `Executors.newCachedThreadPool()` 를 계속 생성하고 shutdown 하지 않으면 제한 갯수(약 3만2천개)에 다다르면 시스템이 다운된다. 449 | * 제한 갯수에 다다르지 않으면 저절로 Thread Pool 의 쓰레드가 줄어들면서 마지막에는 올바로 종료 가능한 상태가 된다. 450 | 451 | ![cachedThreadPool이 점점 줄어드는 모습](cachedThreadPool_no_shutdown.png) 452 | 453 | ### TooManyFixedThreadPoolTester 454 | * `Executors.newFixedThreadPool(1)` 를 계속 생성하기 shutdown 하지 않으면 제한 갯수(약 3만2천개)에 다다르면 시스템이 다운된다. 455 | * 제한 갯수에 다다르지 않더라도, fixedThreadPool 은 시간이 지나도 쓰레드를 반환하지 않고 계속해서 쓰레드 풀에 생성된 쓰레드를 유지한다. 456 | * 이로 인해서 shutdown 을 명시적으로 하지 않고는 올바로 프로세스가 종료되지 않는다. 457 | * Thread Pool 에 있는 쓰레드가 daemon thread 가 아니기 때문이다. 458 | 459 | ![fixedThreadPool이 줄어들지 않는 모습](fixedThreadPool_no_shutdown.png) 460 | 461 | ``` 462 | newFiexedThreadPool(1) idx 32581 - current Active Thread count 32584 463 | newFiexedThreadPool(1) idx 32582 - current Active Thread count 32585 464 | # 465 | # There is insufficient memory for the Java Runtime Environment to continue. 466 | # Native memory allocation (mmap) failed to map 12288 bytes for committing reserved memory. 467 | # An error report file with more information is saved as: 468 | # ..../java-spring-thread-pool-test/hs_err_pid1396904.log 469 | OpenJDK 64-Bit Server VM warning: Attempt to protect stack guard pages failed. 470 | OpenJDK 64-Bit Server VM warning: Attempt to protect stack guard pages failed. 471 | OpenJDK 64-Bit Server VM warning: Attempt to deallocate stack guard pages failed. 472 | Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread 473 | at java.lang.Thread.start0(Native Method) 474 | at java.lang.Thread.start(Thread.java:717) 475 | at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:957) 476 | at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1367) 477 | at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112) 478 | at kr.pe.kwonnam.java_spring_threadpool.TooManyFixedThreadPoolTester.main(TooManyFixedThreadPoolTester.java:11) 479 | OpenJDK 64-Bit Server VM warning: Attempt to deallocate stack guard pages failed. 480 | OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00007f6d1c10d000, 12288, 0) failed; error='메모리를 할당할 수 없습니다' (errno=12) 481 | ``` 482 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This generated file contains a sample Java project to get you started. 5 | * For more details take a look at the Java Quickstart chapter in the Gradle 6 | * User Manual available at https://docs.gradle.org/6.5/userguide/tutorial_java_projects.html 7 | */ 8 | 9 | plugins { 10 | // Apply the java plugin to add support for Java 11 | id 'java' 12 | 13 | // Apply the application plugin to add support for building a CLI application. 14 | id 'application' 15 | } 16 | 17 | repositories { 18 | // Use jcenter for resolving dependencies. 19 | // You can declare any Maven/Ivy/file repository here. 20 | jcenter() 21 | } 22 | 23 | dependencies { 24 | // This dependency is used by the application. 25 | implementation 'com.google.guava:guava:29.0-jre' 26 | // https://mvnrepository.com/artifact/org.springframework/spring-context 27 | implementation 'org.springframework:spring-context:5.2.9.RELEASE' 28 | 29 | // Use JUnit Jupiter API for testing. 30 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2' 31 | 32 | // Use JUnit Jupiter Engine for testing. 33 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.2' 34 | } 35 | 36 | application { 37 | // Define the main class for the application. 38 | mainClassName = 'kr.pe.kwonnam.java_spring_threadpool.ThreadPoolTester' 39 | 40 | } 41 | 42 | run { 43 | maxHeapSize "64m" 44 | } 45 | 46 | test { 47 | // Use junit platform for unit tests 48 | useJUnitPlatform() 49 | } 50 | -------------------------------------------------------------------------------- /cachedThreadPool_no_shutdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwon37xi/java-spring-thread-pool-test/7760a4e7a452b40c29e3a9d534b88431f6a8f6c6/cachedThreadPool_no_shutdown.png -------------------------------------------------------------------------------- /fixedThreadPool_no_shutdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwon37xi/java-spring-thread-pool-test/7760a4e7a452b40c29e3a9d534b88431f6a8f6c6/fixedThreadPool_no_shutdown.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwon37xi/java-spring-thread-pool-test/7760a4e7a452b40c29e3a9d534b88431f6a8f6c6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.5/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'java-spring-thread-pool-test' 11 | -------------------------------------------------------------------------------- /src/main/java/kr/pe/kwonnam/java_spring_threadpool/PoolStrategy.java: -------------------------------------------------------------------------------- 1 | package kr.pe.kwonnam.java_spring_threadpool; 2 | 3 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 4 | 5 | import java.util.concurrent.Executor; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | import java.util.concurrent.ForkJoinPool; 9 | 10 | public enum PoolStrategy { 11 | /** 12 | * Java 기본 크기 무제한 Executors.newCachedThreadPool() 13 | */ 14 | EXECUTOR_SERVICE_CACHED { 15 | @Override 16 | public Executor getExecutor() { 17 | return Executors.newCachedThreadPool(); 18 | } 19 | 20 | @Override 21 | public void shutdown(Executor executor) { 22 | ((ExecutorService)executor).shutdownNow(); 23 | } 24 | }, 25 | /** 26 | * Java 기본 고정 크기 : Executors.newFixedThreadPool(1000) 27 | */ 28 | EXECUTOR_SERVICE_FIXED_1000 { 29 | @Override 30 | public Executor getExecutor() { 31 | return Executors.newFixedThreadPool(ThreadPoolTester.DEFAULT_CORE_POOL_SIZE); 32 | } 33 | 34 | @Override 35 | public void shutdown(Executor executor) { 36 | ((ExecutorService)executor).shutdownNow(); 37 | } 38 | }, 39 | /** 40 | * SpringFramework ThreadPoolTaskExecutor corePoolSize 1000, maxPoolSize : 정수최대, queueCapacity : 0 41 | * 사실상 Executors.newCachedThreadPool() 과 같다. 42 | */ 43 | THREAD_POOL_TASK_EXECUTOR_CORE_POOL_SIZE_AND_QUEUE_0 { 44 | @Override 45 | public Executor getExecutor() { 46 | return createThreadPoolTaskExecutor(0, Integer.MAX_VALUE, 0); 47 | } 48 | 49 | @Override 50 | public void shutdown(Executor executor) { 51 | ((ThreadPoolTaskExecutor) executor).shutdown(); 52 | } 53 | }, 54 | /** 55 | * SpringFramework ThreadPoolTaskExecutor corePoolSize 1000, maxPoolSize : corePoolSize 와 동일, queueCapacity : 정수최대 56 | * 사실상 Executors.newFixedThreadPool() 과 같다. 57 | */ 58 | THREAD_POOL_TASK_EXECUTOR_QUEUE_INTMAX { 59 | @Override 60 | public Executor getExecutor() { 61 | return createThreadPoolTaskExecutor(ThreadPoolTester.DEFAULT_CORE_POOL_SIZE, ThreadPoolTester.DEFAULT_CORE_POOL_SIZE, Integer.MAX_VALUE); 62 | } 63 | 64 | @Override 65 | public void shutdown(Executor executor) { 66 | ((ThreadPoolTaskExecutor) executor).shutdown(); 67 | } 68 | }, 69 | /** 70 | * SpringFramework ThreadPoolTaskExecutor corePoolSize 1000, maxPoolSize : 정수최대, queueCapacity : 40000 71 | *

72 | * core/max/queueCapcity 의 관계 : core 갯수만큼 생성후 queue 를 넘어선 뒤에야 maxPoolSize 만큼 증가된다. 73 | * 74 | */ 75 | THREAD_POOL_TASK_EXECUTOR_MAX_INTMAX_QUEUE_40000 { 76 | @Override 77 | public Executor getExecutor() { 78 | return createThreadPoolTaskExecutor(ThreadPoolTester.DEFAULT_CORE_POOL_SIZE, Integer.MAX_VALUE, 40000); 79 | } 80 | 81 | @Override 82 | public void shutdown(Executor executor) { 83 | ((ThreadPoolTaskExecutor) executor).shutdown(); 84 | } 85 | }, 86 | /** 87 | * SpringFramework ThreadPoolTaskExecutor corePoolSize 1000, maxPoolSize : 2000, queueCapacity : 0 88 | *

89 | * corePoolSize < maxPoolSize, queueCapacity = 0 이면, maxPoolSize에 도달하는 순간 죽어버린다. 왜? 90 | */ 91 | THREAD_POOL_TASK_EXECUTOR_MAX_LIMITED_QUEUE_0 { 92 | @Override 93 | public Executor getExecutor() { 94 | return createThreadPoolTaskExecutor(ThreadPoolTester.DEFAULT_CORE_POOL_SIZE, ThreadPoolTester.DEFAULT_CORE_POOL_SIZE * 2, 0); 95 | } 96 | 97 | @Override 98 | public void shutdown(Executor executor) { 99 | ((ThreadPoolTaskExecutor) executor).shutdown(); 100 | } 101 | }, 102 | /** 103 | * @see ForkJoinPool 104 | * @see ForkJoinPool#commonPool() 105 | * 106 | *

112 | */ 113 | FORK_JOIN_COMMON_POOL { 114 | @Override 115 | public Executor getExecutor() { 116 | System.out.printf("current java.util.concurrent.ForkJoinPool.common.parallelism : %d%n", System.getProperty("java.util.concurrent.ForkJoinPool.common.parallelism")); 117 | return ForkJoinPool.commonPool(); 118 | } 119 | 120 | @Override 121 | public void shutdown(Executor executor) { 122 | // java.util.concurrent.ForkJoinPool.commonPool 에 따르면 System.exit() 시에 자동으로 shutDown 된다. 123 | } 124 | }, 125 | /** 126 | *
127 |      *      ./gradlew run --args="FORK_JOIN_COMMON_POOL_PARALLELISM_1000"
128 |      * 
129 | */ 130 | FORK_JOIN_COMMON_POOL_PARALLELISM_1000 { 131 | @Override 132 | public Executor getExecutor() { 133 | System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "1000"); 134 | System.out.printf("current java.util.concurrent.ForkJoinPool.common.parallelism : %s%n", System.getProperty("java.util.concurrent.ForkJoinPool.common.parallelism")); 135 | return ForkJoinPool.commonPool(); 136 | } 137 | 138 | @Override 139 | public void shutdown(Executor executor) { 140 | // java.util.concurrent.ForkJoinPool.commonPool 에 따르면 System.exit() 시에 자동으로 shutDown 된다. 141 | } 142 | } 143 | ; 144 | 145 | public abstract Executor getExecutor(); 146 | public abstract void shutdown(Executor executor); 147 | 148 | public static Executor createThreadPoolTaskExecutor(int corePoolSize, int maxPoolSize, int queueCapacity) { 149 | ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); 150 | threadPoolTaskExecutor.setThreadNamePrefix("MYTHREADPOOL-"); 151 | threadPoolTaskExecutor.setCorePoolSize(corePoolSize); 152 | threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize); 153 | threadPoolTaskExecutor.setQueueCapacity(queueCapacity); 154 | threadPoolTaskExecutor.setKeepAliveSeconds(60); 155 | threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true); 156 | threadPoolTaskExecutor.setAwaitTerminationSeconds(30); 157 | threadPoolTaskExecutor.afterPropertiesSet(); 158 | return threadPoolTaskExecutor; 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/kr/pe/kwonnam/java_spring_threadpool/ThreadPoolTester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | package kr.pe.kwonnam.java_spring_threadpool; 5 | 6 | import java.util.concurrent.CountDownLatch; 7 | import java.util.concurrent.Executor; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.stream.Stream; 10 | 11 | /** 12 | * 오래 실행되는 작업을 Thread Pool 에 넣었을 때 Pool 종류별 반응 살펴보기. 13 | *

14 | * 첫번째 인자로 PoolStrategy enum 이름을 넣으면 된다. 15 | *

16 | * Spring Framework 에 의존성이 걸려 있어야 한다. 17 | * 18 | *

19 |  *     java ThreadPoolTester EXECUTOR_SERVICE_CACHED
20 |  *     java ThreadPoolTester EXECUTOR_SERVICE_FIXED_1000
21 |  *     java ThreadPoolTester THREAD_POOL_TASK_EXECUTOR_QUEUE_0
22 |  *     java ThreadPoolTester THREAD_POOL_TASK_EXECUTOR_QUEUE_INTMAX
23 |  *     java ThreadPoolTester THREAD_POOL_TASK_EXECUTOR_MAX_INTMAX_QUEUE_10
24 |  *     java ThreadPoolTester THREAD_POOL_TASK_EXECUTOR_MIN_MAX_SAME_QUEUE_0
25 |  *
26 |  * 
27 | */ 28 | public class ThreadPoolTester { 29 | 30 | public static final int TARGET_THREAD_COUNT = 50000; 31 | public static final int DEFAULT_CORE_POOL_SIZE = 1000; 32 | 33 | public static void main(String[] args) throws InterruptedException { 34 | printHeapInfo(); 35 | 36 | String strategyName = Stream.of(args).findFirst().orElse(PoolStrategy.EXECUTOR_SERVICE_CACHED.name()); 37 | PoolStrategy poolStrategy = PoolStrategy.valueOf(strategyName); 38 | 39 | System.out.printf("# Starting with Thread Pool %s%n", poolStrategy.name()); 40 | Executor executor = poolStrategy.getExecutor(); 41 | 42 | try { 43 | 44 | CountDownLatch countDownLatch = new CountDownLatch(TARGET_THREAD_COUNT); 45 | for (int idx = 0; idx < TARGET_THREAD_COUNT; idx++) { 46 | int value = idx; 47 | executor.execute(() -> { 48 | System.out.printf("# current thread [%s] idx : %d, current active thread count %d%n", 49 | Thread.currentThread().getName(), value, Thread.activeCount()); 50 | try { 51 | // 5초만 지연돼도 컴퓨터 입장에서는 상당한 지연이다. 52 | TimeUnit.SECONDS.sleep(5); 53 | } catch (InterruptedException e) { 54 | e.printStackTrace(); 55 | } 56 | countDownLatch.countDown(); 57 | System.out.printf("# current thread [%s] idx : %d, , current active thread count %d, countDownLatch : %d END%n", 58 | Thread.currentThread().getName(), value, Thread.activeCount(), countDownLatch.getCount()); 59 | }); 60 | } 61 | 62 | System.out.println("# after thread generation ..."); 63 | countDownLatch.await(); 64 | System.out.println("# The end"); 65 | } finally { 66 | System.out.println("# shutting down executor"); 67 | poolStrategy.shutdown(executor); 68 | } 69 | 70 | } 71 | 72 | /** 73 | * @see https://stackoverflow.com/a/44603197/1051402 74 | */ 75 | private static void printHeapInfo() { 76 | // Get maximum size of heap in bytes. The heap cannot grow beyond this size.// Any attempt will result in an OutOfMemoryException. 77 | long heapMaxSize = Runtime.getRuntime().maxMemory(); 78 | System.out.println("heapmaxsize : " + formatSize(heapMaxSize)); 79 | } 80 | 81 | public static String formatSize(long v) { 82 | if (v < 1024) return v + " B"; 83 | int z = (63 - Long.numberOfLeadingZeros(v)) / 10; 84 | return String.format("%.1f %sB", (double) v / (1L << (z * 10)), " KMGTPE".charAt(z)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/kr/pe/kwonnam/java_spring_threadpool/TooManyCachedThreadPoolTester.java: -------------------------------------------------------------------------------- 1 | package kr.pe.kwonnam.java_spring_threadpool; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * 9 | */ 10 | public class TooManyCachedThreadPoolTester { 11 | public static void main(String[] args) throws InterruptedException { 12 | for (int i = 0; i < 30_000; i++) { // 35_000 으로 변경하면 시스템 Down, 그러나 쓰레드 생성 속도를 늦추면 괜찮음. 13 | ExecutorService executorService = Executors.newCachedThreadPool(); 14 | executorService.submit(() -> { 15 | try { 16 | TimeUnit.SECONDS.sleep(1); 17 | } catch (InterruptedException e) { 18 | e.printStackTrace(); 19 | } 20 | }); 21 | System.out.printf("newCachedThreadPool() idx %d - current Active Thread count %d%n", i, Thread.activeCount()); 22 | } 23 | 24 | System.out.printf("Thread generation ended.%n"); 25 | TimeUnit.MINUTES.sleep(2); 26 | System.out.printf("After 2 minutes current Active Thread count %d%n", Thread.activeCount()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/kr/pe/kwonnam/java_spring_threadpool/TooManyFixedThreadPoolTester.java: -------------------------------------------------------------------------------- 1 | package kr.pe.kwonnam.java_spring_threadpool; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | public class TooManyFixedThreadPoolTester { 8 | public static void main(String[] args) throws InterruptedException { 9 | for (int i = 0; i < 30_000; i++) { // 35_000 으로 변경하면 시스템 Down 10 | ExecutorService executorService = Executors.newFixedThreadPool(1); 11 | executorService.submit(() -> { 12 | try { 13 | TimeUnit.SECONDS.sleep(1); 14 | } catch (InterruptedException e) { 15 | e.printStackTrace(); 16 | } 17 | }); 18 | System.out.printf("newFiexedThreadPool(1) idx %d - current Active Thread count %d%n", i, Thread.activeCount()); 19 | } 20 | 21 | System.out.printf("Thread generation ended.%n"); 22 | TimeUnit.MINUTES.sleep(2); 23 | System.out.printf("After 2 minutes current Active Thread count %d%n", Thread.activeCount()); 24 | } 25 | } 26 | --------------------------------------------------------------------------------