From b0222f82598a504ae8b498914eeda66155556ec5 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Tue, 26 Mar 2024 18:03:16 +0900 Subject: [PATCH 01/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EA=B8=B0=EB=8A=A5=20=E2=9C=85=20Test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 비동기 서비스 클래스 생성 2. 생성한 메서드에 대한 기본 테스트 작성 1. 여러 입출력 상황에 맞게 비동기 메서드 호출과 비동기 객체 병합 방법 강구 --- .gitignore | 6 +-- .../async/service/AsyncService.java | 21 +++++++++++ .../thread/concurrency/AsyncServiceTest.java | 37 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/thread/concurrency/async/service/AsyncService.java create mode 100644 src/test/java/com/thread/concurrency/AsyncServiceTest.java diff --git a/.gitignore b/.gitignore index bd3712a..9cfa0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ HELP.md -.gradle -build/ +.gradle/* +build/* !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ @@ -18,7 +18,7 @@ bin/ !**/src/test/**/bin/ ### IntelliJ IDEA ### -.idea +.idea/* *.iws *.iml *.ipr diff --git a/src/main/java/com/thread/concurrency/async/service/AsyncService.java b/src/main/java/com/thread/concurrency/async/service/AsyncService.java new file mode 100644 index 0000000..422c911 --- /dev/null +++ b/src/main/java/com/thread/concurrency/async/service/AsyncService.java @@ -0,0 +1,21 @@ +package com.thread.concurrency.async.service; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.concurrent.CompletableFuture; + +@Service +public class AsyncService { + @Async + public CompletableFuture voidParamStringReturn(){ + System.out.println("비동기적으로 실행 - "+ + Thread.currentThread().getName()); + try{ + Thread.sleep(1000); + return CompletableFuture.completedFuture("hello world"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/thread/concurrency/AsyncServiceTest.java b/src/test/java/com/thread/concurrency/AsyncServiceTest.java new file mode 100644 index 0000000..65a7033 --- /dev/null +++ b/src/test/java/com/thread/concurrency/AsyncServiceTest.java @@ -0,0 +1,37 @@ +package com.thread.concurrency; + +import com.thread.concurrency.async.service.AsyncService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + + +public class AsyncServiceTest { + private AsyncService asyncService; + @BeforeEach + void initUseCase() { // spring container를 사용하지 않고 순수 클래스를 사용. + asyncService = new AsyncService(); + } + @Test + @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출") + public void testGetString() throws ExecutionException, InterruptedException { + CompletableFuture helloWorld = asyncService.voidParamStringReturn(); + Assertions.assertEquals("hello world",helloWorld.get()); + } + +// @Test +// @DisplayName("입력은 void 출력은 String인 비동기 함수 복수 호출 그리고 결과 조합") +// public void testMultiGetString() throws ExecutionException, InterruptedException { +// List> completableFutures = new ArrayList<>(); +// +// for (int j = 0; j <= 23; j++) { +// completableFutures.add(asyncService.voidParamStringReturn())); +// } +// CompletableFuture.allOf(price1,price2,price3).join(); +// Assertions.assertEquals("hello world !!hello world !!", combin); +// } +} From fd930569f6ff7449ee39d63cd716334146344a7c Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 27 Mar 2024 09:46:14 +0900 Subject: [PATCH 02/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 메서드가 비동기 메서드인지 확인하는 테스트 작성 2. @Async를 붙인 메서드가 비동기로 동자가하지 않고 하나의 쓰레드에서 동작한다. --- .../async/service/AsyncService.java | 8 +-- .../thread/concurrency/AsyncServiceTest.java | 66 +++++++++++++------ 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/thread/concurrency/async/service/AsyncService.java b/src/main/java/com/thread/concurrency/async/service/AsyncService.java index 422c911..3ed96b5 100644 --- a/src/main/java/com/thread/concurrency/async/service/AsyncService.java +++ b/src/main/java/com/thread/concurrency/async/service/AsyncService.java @@ -8,11 +8,11 @@ @Service public class AsyncService { @Async - public CompletableFuture voidParamStringReturn(){ - System.out.println("비동기적으로 실행 - "+ - Thread.currentThread().getName()); + public CompletableFuture voidParamStringReturn(long waitTime){ +// System.out.println("비동기적으로 실행 - "+ +// Thread.currentThread().getName()); try{ - Thread.sleep(1000); + Thread.sleep(waitTime); return CompletableFuture.completedFuture("hello world"); } catch (InterruptedException e) { throw new RuntimeException(e); diff --git a/src/test/java/com/thread/concurrency/AsyncServiceTest.java b/src/test/java/com/thread/concurrency/AsyncServiceTest.java index 65a7033..087e4e7 100644 --- a/src/test/java/com/thread/concurrency/AsyncServiceTest.java +++ b/src/test/java/com/thread/concurrency/AsyncServiceTest.java @@ -1,37 +1,61 @@ package com.thread.concurrency; import com.thread.concurrency.async.service.AsyncService; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; - +@SpringBootTest public class AsyncServiceTest { + @Autowired private AsyncService asyncService; - @BeforeEach - void initUseCase() { // spring container를 사용하지 않고 순수 클래스를 사용. - asyncService = new AsyncService(); - } @Test @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출") public void testGetString() throws ExecutionException, InterruptedException { - CompletableFuture helloWorld = asyncService.voidParamStringReturn(); + CompletableFuture helloWorld = asyncService.voidParamStringReturn(1000); Assertions.assertEquals("hello world",helloWorld.get()); } + @Test + @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출 타임아웃 발생.") + public void testGetStringTimeOutIsThisAsync() { + // voidParamStringReturn가 비동기 메서드인지 의문이 생김. + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> { + try { + return asyncService.voidParamStringReturn(4000).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + }); + long timeOutValue = 1; + TimeUnit timeUnit = TimeUnit.SECONDS; + // 1초가 지난 후 타임아웃 발생 + Assertions.assertThrows(ExecutionException.class, () -> completableFuture.orTimeout(timeOutValue,timeUnit).get()); + } -// @Test -// @DisplayName("입력은 void 출력은 String인 비동기 함수 복수 호출 그리고 결과 조합") -// public void testMultiGetString() throws ExecutionException, InterruptedException { -// List> completableFutures = new ArrayList<>(); -// -// for (int j = 0; j <= 23; j++) { -// completableFutures.add(asyncService.voidParamStringReturn())); -// } -// CompletableFuture.allOf(price1,price2,price3).join(); -// Assertions.assertEquals("hello world !!hello world !!", combin); -// } + @Test + @DisplayName("입력은 void 출력은 String인 비동기 함수 복수 호출 그리고 결과 조합") + public void testMultiGetString() { + List> futures = new ArrayList<>(); + for (int i = 1; i <= 1000; i++) { // 동기라면 10초가 걸리고 비동기라면 0.01초가 걸릴 것이다. + futures.add(asyncService.voidParamStringReturn(10)); + } + CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); + for (CompletableFuture future : futures) { + aggregate = aggregate.thenCompose(list -> { + list.add(String.valueOf(future)); + return CompletableFuture.completedFuture(list); + }); + } + final List results = aggregate.join(); + for (int i = 0; i < 1000; i++) { + System.out.println("Finished " + results.get(i)); + } + + } } From 3c8ff758277d6f9ff18fd849e9b3c95e2c6e14d4 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Thu, 28 Mar 2024 00:12:26 +0900 Subject: [PATCH 03/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EA=B8=B0=EB=8A=A5=20=E2=9C=85=20Test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 다중 스레드 설정을 위해 taskExecutor 메서드 빈으로 저장 2. 비동기 메서드 다중 호출 시 병렬적으로 동작하는지 testGetMultiString에서 검증 3. 비동기 메서드의 지연시간이 길어질 경우 타임아웃 발생하도록 하는 부분 검증 --- .../SpringThreadConcurrencyApplication.java | 17 +++++- .../async/controller/AsyncController.java | 12 +++++ .../async/service/AsyncService.java | 15 +++--- .../thread/concurrency/AsyncServiceTest.java | 53 ++++++++----------- 4 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/thread/concurrency/async/controller/AsyncController.java diff --git a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java index 42b9717..099710c 100644 --- a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java +++ b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java @@ -2,12 +2,27 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; @SpringBootApplication +@EnableAsync public class SpringThreadConcurrencyApplication { public static void main(String[] args) { SpringApplication.run(SpringThreadConcurrencyApplication.class, args); } - + @Bean + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // Spring에서 사용하는 스레드를 제어한느 설정 + executor.setCorePoolSize(50); // thread-pool에 살아있는 thread의 최소 개수 + executor.setMaxPoolSize(50); // thread-pool에서 사용할 수 있는 최대 개수 + executor.setQueueCapacity(500); //thread-pool에 최대 queue 크기 + executor.setThreadNamePrefix("AsyncApp-"); + executor.initialize(); + return executor; + } } diff --git a/src/main/java/com/thread/concurrency/async/controller/AsyncController.java b/src/main/java/com/thread/concurrency/async/controller/AsyncController.java new file mode 100644 index 0000000..64037ed --- /dev/null +++ b/src/main/java/com/thread/concurrency/async/controller/AsyncController.java @@ -0,0 +1,12 @@ +package com.thread.concurrency.async.controller; + +import com.thread.concurrency.async.service.AsyncService; +import org.springframework.stereotype.Controller; +@Controller +public class AsyncController { + private final AsyncService asyncService; + + public AsyncController(AsyncService asyncService) { + this.asyncService = asyncService; + } +} diff --git a/src/main/java/com/thread/concurrency/async/service/AsyncService.java b/src/main/java/com/thread/concurrency/async/service/AsyncService.java index 3ed96b5..9cb2a00 100644 --- a/src/main/java/com/thread/concurrency/async/service/AsyncService.java +++ b/src/main/java/com/thread/concurrency/async/service/AsyncService.java @@ -5,17 +5,14 @@ import java.util.concurrent.CompletableFuture; + @Service public class AsyncService { @Async - public CompletableFuture voidParamStringReturn(long waitTime){ -// System.out.println("비동기적으로 실행 - "+ -// Thread.currentThread().getName()); - try{ - Thread.sleep(waitTime); - return CompletableFuture.completedFuture("hello world"); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + public CompletableFuture voidParamStringReturn (long waitTime, String message) throws InterruptedException{ + System.out.println("비동기적으로 실행 - "+ + Thread.currentThread().getName()); + Thread.sleep(waitTime); + return CompletableFuture.completedFuture(message); } } diff --git a/src/test/java/com/thread/concurrency/AsyncServiceTest.java b/src/test/java/com/thread/concurrency/AsyncServiceTest.java index 087e4e7..90b0d2d 100644 --- a/src/test/java/com/thread/concurrency/AsyncServiceTest.java +++ b/src/test/java/com/thread/concurrency/AsyncServiceTest.java @@ -2,6 +2,8 @@ import com.thread.concurrency.async.service.AsyncService; import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -13,49 +15,38 @@ @SpringBootTest public class AsyncServiceTest { + private static final Logger logger = LoggerFactory.getLogger(AsyncServiceTest.class); @Autowired private AsyncService asyncService; + @Test @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출") public void testGetString() throws ExecutionException, InterruptedException { - CompletableFuture helloWorld = asyncService.voidParamStringReturn(1000); - Assertions.assertEquals("hello world",helloWorld.get()); + CompletableFuture helloWorld = asyncService.voidParamStringReturn(1000, "기본 메세지"); + Assertions.assertEquals("기본 메세지",helloWorld.get()); } + + @Test + @DisplayName("입력은 void 출력은 String인 비동기 함수 다중 호출") + public void testGetMultiString() throws InterruptedException { + List> hellos = new ArrayList<>(); + for(int i=0; i<100; i++){ + hellos.add(asyncService.voidParamStringReturn(1000,i+"번째 메세지")); + } + // 모든 비동기 호출이 완료될 때까지 대기하고 결과를 리스트에 넣기 + List results = hellos.stream().map(CompletableFuture::join) + .toList(); + results.forEach(logger::info); + } + @Test @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출 타임아웃 발생.") - public void testGetStringTimeOutIsThisAsync() { + public void testGetStringTimeOutIsThisAsync() throws InterruptedException { // voidParamStringReturn가 비동기 메서드인지 의문이 생김. - CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> { - try { - return asyncService.voidParamStringReturn(4000).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - }); + CompletableFuture completableFuture = asyncService.voidParamStringReturn(4000, "타임아웃 발생 안 함!"); long timeOutValue = 1; TimeUnit timeUnit = TimeUnit.SECONDS; // 1초가 지난 후 타임아웃 발생 Assertions.assertThrows(ExecutionException.class, () -> completableFuture.orTimeout(timeOutValue,timeUnit).get()); } - - @Test - @DisplayName("입력은 void 출력은 String인 비동기 함수 복수 호출 그리고 결과 조합") - public void testMultiGetString() { - List> futures = new ArrayList<>(); - for (int i = 1; i <= 1000; i++) { // 동기라면 10초가 걸리고 비동기라면 0.01초가 걸릴 것이다. - futures.add(asyncService.voidParamStringReturn(10)); - } - CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); - for (CompletableFuture future : futures) { - aggregate = aggregate.thenCompose(list -> { - list.add(String.valueOf(future)); - return CompletableFuture.completedFuture(list); - }); - } - final List results = aggregate.join(); - for (int i = 0; i < 1000; i++) { - System.out.println("Finished " + results.get(i)); - } - - } } From 7660cf76c09b23666277c9603ba116814a5f118e Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim <07ily@naver.com> Date: Thu, 28 Mar 2024 03:04:07 +0900 Subject: [PATCH 04/62] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 86633b3..a19ebe1 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# spring-thread-concurrency \ No newline at end of file +# 관심사 +- [멀티 스레드의 자원 공유 문제](https://github.com/spring-templates/spring-concurrency-thread/discussions/16) +- [멀티 쓰레드 자원 업데이트 문제](https://github.com/spring-templates/spring-concurrency-thread/discussions/17) + +# 정보 +- [동시성 기본 조건과 관심사](https://github.com/spring-templates/spring-concurrency-thread/discussions/2) From c734c167888058b7b6275052cce642b79896531b Mon Sep 17 00:00:00 2001 From: ohchansol Date: Thu, 28 Mar 2024 12:32:08 +0900 Subject: [PATCH 05/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EA=B8=B0=EB=8A=A5=20=E2=9C=85=20Test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 비동기 호출을 10회 하는 비동기 메서드를 10회 호출하는 calculateRunTime 작성 2. 비동기 호출이 100회 일 때 50개는 쓰레드가 실행하고 나머지 50개는 블록킹 큐에서 대기한다. --- .../async/controller/AsyncController.java | 23 +++++++++++++ .../concurrency/AsyncControllerTest.java | 32 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/test/java/com/thread/concurrency/AsyncControllerTest.java diff --git a/src/main/java/com/thread/concurrency/async/controller/AsyncController.java b/src/main/java/com/thread/concurrency/async/controller/AsyncController.java index 64037ed..6be4cdf 100644 --- a/src/main/java/com/thread/concurrency/async/controller/AsyncController.java +++ b/src/main/java/com/thread/concurrency/async/controller/AsyncController.java @@ -1,7 +1,15 @@ package com.thread.concurrency.async.controller; import com.thread.concurrency.async.service.AsyncService; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Controller; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + @Controller public class AsyncController { private final AsyncService asyncService; @@ -9,4 +17,19 @@ public class AsyncController { public AsyncController(AsyncService asyncService) { this.asyncService = asyncService; } + + @Async + public CompletableFuture calculateRunTime(int cnt, int waitTime) throws InterruptedException { + LocalTime lt1 = LocalTime.now(); + List> hellos = new ArrayList<>(); + for(int i=0; i results = hellos.stream().map(CompletableFuture::join) + .toList(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).toMillis(); + return CompletableFuture.completedFuture(dif+"가 걸렸습니다."); + } } diff --git a/src/test/java/com/thread/concurrency/AsyncControllerTest.java b/src/test/java/com/thread/concurrency/AsyncControllerTest.java new file mode 100644 index 0000000..59d0f06 --- /dev/null +++ b/src/test/java/com/thread/concurrency/AsyncControllerTest.java @@ -0,0 +1,32 @@ +package com.thread.concurrency; + +import com.thread.concurrency.async.controller.AsyncController; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@SpringBootTest +public class AsyncControllerTest { + private static final Logger logger = LoggerFactory.getLogger(AsyncServiceTest.class); + + @Autowired + private AsyncController asyncController; + + @Test + public void invokeMultiAsyncMethod() throws InterruptedException { + List> hellos = new ArrayList<>(); + for(int i=0; i<10; i++){ + hellos.add(asyncController.calculateRunTime(10, 1000)); + } + // 모든 비동기 호출이 완료될 때까지 대기하고 결과를 리스트에 넣기 + List results = hellos.stream().map(CompletableFuture::join) + .toList(); + results.forEach(logger::info); + } +} From 4e7a0fae38443b534731b0b160032706ab6695ec Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim <07ily@naver.com> Date: Fri, 29 Mar 2024 11:24:36 +0900 Subject: [PATCH 06/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20`BasicCounter`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concurrency/counter/BasicCounter.java | 18 ++++++++++++++++++ .../thread/concurrency/counter/Counter.java | 7 +++++++ 2 files changed, 25 insertions(+) create mode 100644 src/main/java/com/thread/concurrency/counter/BasicCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/Counter.java diff --git a/src/main/java/com/thread/concurrency/counter/BasicCounter.java b/src/main/java/com/thread/concurrency/counter/BasicCounter.java new file mode 100644 index 0000000..a331799 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/BasicCounter.java @@ -0,0 +1,18 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +@Component +public class BasicCounter implements Counter { + private static int count = 100; + + @Override + public void add(int value) { + count += value; + } + + @Override + public int show() { + return count; + } +} diff --git a/src/main/java/com/thread/concurrency/counter/Counter.java b/src/main/java/com/thread/concurrency/counter/Counter.java new file mode 100644 index 0000000..908294f --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/Counter.java @@ -0,0 +1,7 @@ +package com.thread.concurrency.counter; + +public interface Counter { + void add(int value); + + int show(); +} From df915fb8b32212be8e1730a95f6fdc1a68163c84 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim <07ily@naver.com> Date: Fri, 29 Mar 2024 13:15:37 +0900 Subject: [PATCH 07/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20`Counter`=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concurrency/counter/BatchingCounter.java | 38 ++++++++++++++ .../concurrency/counter/LockCounter.java | 26 ++++++++++ .../concurrency/counter/PollingCounter.java | 34 +++++++++++++ .../com/thread/concurrency/CounterTest.java | 49 +++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 src/main/java/com/thread/concurrency/counter/BatchingCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/LockCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/PollingCounter.java create mode 100644 src/test/java/com/thread/concurrency/CounterTest.java diff --git a/src/main/java/com/thread/concurrency/counter/BatchingCounter.java b/src/main/java/com/thread/concurrency/counter/BatchingCounter.java new file mode 100644 index 0000000..f2396ef --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/BatchingCounter.java @@ -0,0 +1,38 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Component +public class BatchingCounter implements Counter { + private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + private static final ConcurrentLinkedQueue jobQueue = new ConcurrentLinkedQueue<>(); + private static volatile int count = 100; + + public BatchingCounter() { + Runnable runnableTask = () -> { + while (!jobQueue.isEmpty()) { + synchronized (this) { + var value = jobQueue.poll(); + count += value == null ? 0 : value; + } + } + }; + // context switching을 최소화하는 최소한의 시간마다 실행하여 성능 향상 + scheduledExecutorService.scheduleAtFixedRate(runnableTask, 4, 5, TimeUnit.MILLISECONDS); + } + + @Override + public void add(int value) { + jobQueue.add(value); + } + + @Override + public int show() { + return count; + } +} diff --git a/src/main/java/com/thread/concurrency/counter/LockCounter.java b/src/main/java/com/thread/concurrency/counter/LockCounter.java new file mode 100644 index 0000000..e0eec2b --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/LockCounter.java @@ -0,0 +1,26 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.locks.ReentrantLock; + +@Component +public class LockCounter implements Counter { + private static final ReentrantLock lock = new ReentrantLock(); + private static int count = 100; + + @Override + public void add(int value) { + lock.lock(); + try { + count += value; + } finally { + lock.unlock(); + } + } + + @Override + public int show() { + return count; + } +} diff --git a/src/main/java/com/thread/concurrency/counter/PollingCounter.java b/src/main/java/com/thread/concurrency/counter/PollingCounter.java new file mode 100644 index 0000000..a11ad44 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/PollingCounter.java @@ -0,0 +1,34 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +@Component +// use technique spin-lock(busy waiting) +// this approach can lead to high CPU usage if the lock is heavily contended +public class PollingCounter implements Counter { + private static int count = 100; + private static volatile boolean lock = false; + + private static void doAdd(int value) { + count += value; + } + + @Override + public void add(int value) { + while (true) { + if (!lock) { + synchronized (PollingCounter.class) { + lock = true; + doAdd(value); + lock = false; + break; + } + } + } + } + + @Override + public int show() { + return count; + } +} diff --git a/src/test/java/com/thread/concurrency/CounterTest.java b/src/test/java/com/thread/concurrency/CounterTest.java new file mode 100644 index 0000000..ff1c5c0 --- /dev/null +++ b/src/test/java/com/thread/concurrency/CounterTest.java @@ -0,0 +1,49 @@ +package com.thread.concurrency; + +import com.thread.concurrency.counter.BatchingCounter; +import com.thread.concurrency.counter.Counter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +public class CounterTest { + + private final Counter counter; + + @Autowired + // 필요한 Counter 구현체를 주입받는다. + public CounterTest(BatchingCounter counter) { + this.counter = counter; + } + + @Test + public void testAddInMultithreadedEnvironment() throws InterruptedException { + int initialValue = counter.show(); + int nThreads = 100; + int nAddsPerThread = 1000; + int valueToAdd = 1; + + try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) { + for (int i = 0; i < nThreads; i++) { + executorService.submit(() -> { + for (int j = 0; j < nAddsPerThread; j++) { + counter.add(valueToAdd); + } + }); + } + // BatchingCounter는 4ms 이후 5ms마다 작업을 처리한다. + // Batching 처리를 위해 20ms 대기 + // 하드웨어 성능에 따라 대기 시간을 조절해야 할 수도 있다. + Thread.sleep(20); + } + + int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; + assertEquals(expectedValue, counter.show()); + } +} From 3f8bcb79c74fca661649fe78669781e97723664c Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Sat, 30 Mar 2024 05:09:17 +0900 Subject: [PATCH 08/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20`@ParameterizedTes?= =?UTF-8?q?t`=20for=20Counters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../com/thread/concurrency/CounterTest.java | 49 ------------- .../concurrency/counter/CounterTest.java | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 49 deletions(-) delete mode 100644 src/test/java/com/thread/concurrency/CounterTest.java create mode 100644 src/test/java/com/thread/concurrency/counter/CounterTest.java diff --git a/src/test/java/com/thread/concurrency/CounterTest.java b/src/test/java/com/thread/concurrency/CounterTest.java deleted file mode 100644 index ff1c5c0..0000000 --- a/src/test/java/com/thread/concurrency/CounterTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.thread.concurrency; - -import com.thread.concurrency.counter.BatchingCounter; -import com.thread.concurrency.counter.Counter; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@SpringBootTest -public class CounterTest { - - private final Counter counter; - - @Autowired - // 필요한 Counter 구현체를 주입받는다. - public CounterTest(BatchingCounter counter) { - this.counter = counter; - } - - @Test - public void testAddInMultithreadedEnvironment() throws InterruptedException { - int initialValue = counter.show(); - int nThreads = 100; - int nAddsPerThread = 1000; - int valueToAdd = 1; - - try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) { - for (int i = 0; i < nThreads; i++) { - executorService.submit(() -> { - for (int j = 0; j < nAddsPerThread; j++) { - counter.add(valueToAdd); - } - }); - } - // BatchingCounter는 4ms 이후 5ms마다 작업을 처리한다. - // Batching 처리를 위해 20ms 대기 - // 하드웨어 성능에 따라 대기 시간을 조절해야 할 수도 있다. - Thread.sleep(20); - } - - int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; - assertEquals(expectedValue, counter.show()); - } -} diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java new file mode 100644 index 0000000..c1cbc0b --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -0,0 +1,70 @@ +package com.thread.concurrency.counter; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@SpringBootTest +public class CounterTest { + + public static Stream counterProvider() { + return Stream.of(new BatchingCounter(), new LockCounter(), new PollingCounter()); + } + + @ParameterizedTest + @MethodSource("counterProvider") + public void testAddInMultithreadedEnvironment( + Counter counter + ) throws InterruptedException { + int initialValue = counter.show(); + int nThreads = 100; + int nAddsPerThread = 1000; + int valueToAdd = 1; + + try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) { + for (int i = 0; i < nThreads; i++) { + executorService.submit(() -> { + for (int j = 0; j < nAddsPerThread; j++) { + counter.add(valueToAdd); + } + }); + } + if (counter instanceof BatchingCounter) { + Thread.sleep(500); + } + } + + int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; + assertEquals(expectedValue, counter.show()); + } + + @Test + public void testMalfunction() { + var counter = new BasicCounter(); + int initialValue = counter.show(); + int nThreads = 100; + int nAddsPerThread = 1000; + int valueToAdd = 1; + + try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) { + for (int i = 0; i < nThreads; i++) { + executorService.submit(() -> { + for (int j = 0; j < nAddsPerThread; j++) { + counter.add(valueToAdd); + } + }); + } + } + + int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; + assertNotEquals(expectedValue, counter.show()); + } +} From 208827f74ba29526dfc9a4e7e3ac772cfcc0aa3b Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Sat, 30 Mar 2024 05:30:53 +0900 Subject: [PATCH 09/62] =?UTF-8?q?=F0=9F=9A=9A=20Chore=20:=20set=20coverage?= =?UTF-8?q?=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- codecov.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..2429953 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + target: 40% + threshold: 10% + patch: + default: + target: 30% + threshold: 10% From dd839e66ef0474d2c1e92dd757ecd2d25e1c585a Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Sat, 30 Mar 2024 06:23:57 +0900 Subject: [PATCH 10/62] =?UTF-8?q?=F0=9F=93=9D=20Docs=20:=20Update=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index a19ebe1..3303d8c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ +[![codecov](https://codecov.io/gh/spring-templates/spring-concurrency-thread/graph/badge.svg?token=N3GEH8C5K7)](https://codecov.io/gh/spring-templates/spring-concurrency-thread) + +![Spring Boot](https://img.shields.io/badge/Spring%20Boot-6DB33F?logo=springboot&logoColor=white) +![Java](https://img.shields.io/badge/Java-ED8B00?logoColor=white) +![Gradle](https://img.shields.io/badge/Gradle-02303A?logo=gradle&logoColor=white) +![JUnit5](https://img.shields.io/badge/JUnit5-25A162?logo=junit5&logoColor=white) +![JaCoCo](https://img.shields.io/badge/JaCoCo-D22128?logo=jacoco&logoColor=white) +![Codecov](https://img.shields.io/badge/Codecov-F01F7A?logo=codecov&logoColor=white) +![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-2088FF?logo=githubactions&logoColor=white) + # 관심사 + - [멀티 스레드의 자원 공유 문제](https://github.com/spring-templates/spring-concurrency-thread/discussions/16) - [멀티 쓰레드 자원 업데이트 문제](https://github.com/spring-templates/spring-concurrency-thread/discussions/17) # 정보 + - [동시성 기본 조건과 관심사](https://github.com/spring-templates/spring-concurrency-thread/discussions/2) From f78aeaca360dd79080ff9f7ba8fd0db7f42d29f4 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sat, 30 Mar 2024 16:01:54 +0900 Subject: [PATCH 11/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EA=B8=B0=EB=8A=A5=20=E2=9C=85=20Test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. BasicCounter 구현. 멀티 쓰레드로 카운터 동시 업데이트 불가 테스트 2. SynchronizedCounter 구현. 멀티 쓰레드로 카운터 동시 업데이트 가능 테스트. Synchronized만 사용했을 때는 0.5s 소요 3. Completable 사용하면 실행시간이 대폭 감소 --- build.gradle.kts | 1 + .../concurrency/counter/BasicCounter.java | 18 +++++ .../thread/concurrency/counter/Counter.java | 6 ++ .../counter/SynchronizedCounter.java | 21 +++++ .../concurrency/counter/BasicCounterTest.java | 65 ++++++++++++++++ .../counter/SynchronizedCounterTest.java | 76 +++++++++++++++++++ .../com/thread/concurrency/package-info.java | 1 + 7 files changed, 188 insertions(+) create mode 100644 src/main/java/com/thread/concurrency/counter/BasicCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/Counter.java create mode 100644 src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java create mode 100644 src/test/java/com/thread/concurrency/counter/BasicCounterTest.java create mode 100644 src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java create mode 100644 src/test/java/com/thread/concurrency/package-info.java diff --git a/build.gradle.kts b/build.gradle.kts index dfe6a2a..85502fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,7 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter") + implementation("net.jcip:jcip-annotations:1.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.reactivestreams:reactive-streams") diff --git a/src/main/java/com/thread/concurrency/counter/BasicCounter.java b/src/main/java/com/thread/concurrency/counter/BasicCounter.java new file mode 100644 index 0000000..ae72fd6 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/BasicCounter.java @@ -0,0 +1,18 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +@Component +public class BasicCounter implements Counter{ + private static int count = 100; + + @Override + public void add(int value) { + count += value; + } + + @Override + public int show() { + return count; + } +} diff --git a/src/main/java/com/thread/concurrency/counter/Counter.java b/src/main/java/com/thread/concurrency/counter/Counter.java new file mode 100644 index 0000000..4575c3d --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/Counter.java @@ -0,0 +1,6 @@ +package com.thread.concurrency.counter; + +public interface Counter { + void add(int value); + int show(); +} diff --git a/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java new file mode 100644 index 0000000..be6863c --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java @@ -0,0 +1,21 @@ +package com.thread.concurrency.counter; + +import net.jcip.annotations.GuardedBy; +import org.springframework.stereotype.Component; + +@Component +public class SynchronizedCounter implements Counter{ + + @GuardedBy("this") + private int counter = 100; + + @Override + public synchronized void add(int value) { + counter += value; + } + + @Override + public synchronized int show() { + return counter; + } +} diff --git a/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java b/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java new file mode 100644 index 0000000..7a8a2e3 --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java @@ -0,0 +1,65 @@ +package com.thread.concurrency.counter; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +@SpringBootTest +public class BasicCounterTest { + + private final BasicCounter basicCounter; + private final int counteNumber = 1; + private final int totalCount = 100; + + @Autowired + public BasicCounterTest(BasicCounter basicCounter) { + this.basicCounter = basicCounter; + } + + @Test + @DisplayName("스레드 안전하지 않는 카운터로 동시에 여러 더하기 수행하기. 실패 예상") + public void 여러_더하기_수행(){ + int initalCount = basicCounter.show(); + + for(int i=0; i { + basicCounter.add(counteNumber); + }); + } + int finalCount = basicCounter.show(); + Assertions.assertNotEquals(initalCount+totalCount*counteNumber, finalCount); + } + + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + public void 여러_더하기_수행_CompletableFuture() { + int initalCount = basicCounter.show(); + List> tasks = new ArrayList<>(); + for(int i=0; i basicCounter.add(counteNumber))); + } + + CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); + for (CompletableFuture future : tasks) { + aggregate = aggregate.thenCompose(list -> { + try { + list.add(future.get()); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return CompletableFuture.completedFuture(list); + }); + } + aggregate.join(); // 전체 비동기 결과 집계 + int finalCount = basicCounter.show(); + Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); + } +} diff --git a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java new file mode 100644 index 0000000..340112d --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java @@ -0,0 +1,76 @@ +package com.thread.concurrency.counter; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.*; + +@SpringBootTest +public class SynchronizedCounterTest { + + private final SynchronizedCounter counter; + private final int counteNumber = 1; + private final int totalCount = 100; + + @Autowired + public SynchronizedCounterTest(SynchronizedCounter counter) { + this.counter = counter; + } + + /** + * 실행 완료까지 0.5s 정도 소요 + * @throws InterruptedException + */ + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + public void 여러_더하기_수행_Executor() throws InterruptedException { + int initalCount = counter.show(); + int numberOfThreads = totalCount; + ExecutorService service = Executors.newFixedThreadPool(10); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + for (int i = 0; i < numberOfThreads; i++) { + service.submit(() -> { + counter.add(counteNumber); + latch.countDown(); + }); + } + latch.await(); + int finalCount = counter.show(); + + Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); + } + + /** + * 실행 완료까지 0.002s 소요 + */ + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + public void 여러_더하기_수행_CompletableFuture() { + int initalCount = counter.show(); + List> tasks = new ArrayList<>(); + for(int i=0; i counter.add(counteNumber))); + } + + CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); + for (CompletableFuture future : tasks) { + aggregate = aggregate.thenCompose(list -> { + try { + list.add(future.get()); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return CompletableFuture.completedFuture(list); + }); + } + aggregate.join(); // 전체 비동기 결과 집계 + int finalCount = counter.show(); + Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); + } +} diff --git a/src/test/java/com/thread/concurrency/package-info.java b/src/test/java/com/thread/concurrency/package-info.java new file mode 100644 index 0000000..ae29573 --- /dev/null +++ b/src/test/java/com/thread/concurrency/package-info.java @@ -0,0 +1 @@ +package com.thread.concurrency; From b96f482e8005c3abff80acb8906e4936e4e1cb79 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sat, 30 Mar 2024 16:30:32 +0900 Subject: [PATCH 12/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. SynchronizedCounter 테스트의 실질적 실행시간을 로깅하도록 리팩토링 --- .../concurrency/counter/BasicCounterTest.java | 30 ---------------- .../counter/SynchronizedCounterTest.java | 36 ++++++++++++------- 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java b/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java index 7a8a2e3..ffc7e7f 100644 --- a/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java @@ -1,16 +1,11 @@ package com.thread.concurrency.counter; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; - -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; @SpringBootTest public class BasicCounterTest { @@ -37,29 +32,4 @@ public BasicCounterTest(BasicCounter basicCounter) { int finalCount = basicCounter.show(); Assertions.assertNotEquals(initalCount+totalCount*counteNumber, finalCount); } - - @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") - public void 여러_더하기_수행_CompletableFuture() { - int initalCount = basicCounter.show(); - List> tasks = new ArrayList<>(); - for(int i=0; i basicCounter.add(counteNumber))); - } - - CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); - for (CompletableFuture future : tasks) { - aggregate = aggregate.thenCompose(list -> { - try { - list.add(future.get()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - return CompletableFuture.completedFuture(list); - }); - } - aggregate.join(); // 전체 비동기 결과 집계 - int finalCount = basicCounter.show(); - Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); - } } diff --git a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java index 340112d..8d55ba7 100644 --- a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java @@ -1,11 +1,16 @@ package com.thread.concurrency.counter; +import com.thread.concurrency.AsyncServiceTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import java.time.Duration; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -14,27 +19,24 @@ @SpringBootTest public class SynchronizedCounterTest { - private final SynchronizedCounter counter; private final int counteNumber = 1; private final int totalCount = 100; - - @Autowired - public SynchronizedCounterTest(SynchronizedCounter counter) { - this.counter = counter; - } + private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); /** - * 실행 완료까지 0.5s 정도 소요 + * 실행 완료까지 871ms 정도 소요 * @throws InterruptedException */ @Test @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") public void 여러_더하기_수행_Executor() throws InterruptedException { + SynchronizedCounter counter = new SynchronizedCounter(); + LocalTime lt1 = LocalTime.now(); int initalCount = counter.show(); - int numberOfThreads = totalCount; - ExecutorService service = Executors.newFixedThreadPool(10); - CountDownLatch latch = new CountDownLatch(numberOfThreads); - for (int i = 0; i < numberOfThreads; i++) { + int numberOfThreads = 15; + ExecutorService service = Executors.newFixedThreadPool(15); + CountDownLatch latch = new CountDownLatch(100); + for (int i = 0; i < totalCount; i++) { service.submit(() -> { counter.add(counteNumber); latch.countDown(); @@ -42,16 +44,20 @@ public SynchronizedCounterTest(SynchronizedCounter counter) { } latch.await(); int finalCount = counter.show(); - + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : "+dif/1000+"ms"); Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); } /** - * 실행 완료까지 0.002s 소요 + * 실행 완료까지 1061ms 소요 */ @Test @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") public void 여러_더하기_수행_CompletableFuture() { + SynchronizedCounter counter = new SynchronizedCounter(); + LocalTime lt1 = LocalTime.now(); int initalCount = counter.show(); List> tasks = new ArrayList<>(); for(int i=0; i Date: Sat, 30 Mar 2024 17:52:12 +0900 Subject: [PATCH 13/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EA=B8=B0=EB=8A=A5=20=E2=9C=85=20Test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. CompletableFuture를 이용한 카운터 작성 2. 카운터에 int가 아닌 CompletableFuture를 저장 3. 작업의 완성본을 저장하지 않고 작업 진행 중인 Future 객체를 저장하는 점에서 추후 캐싱 시나리오에 도움될 것으로 보임 --- .../counter/CompletableFutureCounter.java | 30 +++++++++++++ .../counter/CompletableFutureCounterTest.java | 43 +++++++++++++++++++ .../counter/SynchronizedCounterTest.java | 4 +- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java create mode 100644 src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java diff --git a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java new file mode 100644 index 0000000..0a32d60 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java @@ -0,0 +1,30 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class CompletableFutureCounter implements Counter{ + + private CompletableFuture counter = new CompletableFuture<>(); + + @Override + public void add(int value) { + // 연산이 진행 중이라면 기다렸다가 thenApply + // 카운트에 값 저장 + counter = counter.thenApply((c) -> c + value); + } + + @Override + public int show() { + try { + // 카운트에 대한 연산이 실행 중이라면 기다렸다가 가져오기 + return counter.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java new file mode 100644 index 0000000..236bfd9 --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java @@ -0,0 +1,43 @@ +package com.thread.concurrency.counter; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@SpringBootTest +public class CompletableFutureCounterTest { + private final int counteNumber = 1; + private final int totalCount = 10000; + private static final Logger logger = LoggerFactory.getLogger(CompletableFutureCounterTest.class); + + @Test + public void 여러_더하기_수행_Compltable() throws InterruptedException { + SynchronizedCounter counter = new SynchronizedCounter(); + LocalTime lt1 = LocalTime.now(); + + int initalCount = counter.show(); + ExecutorService service = Executors.newFixedThreadPool(15); + CountDownLatch latch = new CountDownLatch(totalCount); + for (int i = 0; i < totalCount; i++) { + service.submit(() -> { + counter.add(counteNumber); + latch.countDown(); + }); + } + latch.await(); + int finalCount = counter.show(); + + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("여러_더하기_수행_Compltable 테스트가 걸린 시간 : "+dif/1000+"ms"); + Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); + } +} diff --git a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java index 8d55ba7..0c24bec 100644 --- a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java @@ -20,7 +20,7 @@ public class SynchronizedCounterTest { private final int counteNumber = 1; - private final int totalCount = 100; + private final int totalCount = 10000; private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); /** @@ -35,7 +35,7 @@ public class SynchronizedCounterTest { int initalCount = counter.show(); int numberOfThreads = 15; ExecutorService service = Executors.newFixedThreadPool(15); - CountDownLatch latch = new CountDownLatch(100); + CountDownLatch latch = new CountDownLatch(totalCount); for (int i = 0; i < totalCount; i++) { service.submit(() -> { counter.add(counteNumber); From 2b28fd942ebc37a96280675ef3fb0310ea7ecf79 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Mon, 1 Apr 2024 08:04:50 +0900 Subject: [PATCH 14/62] =?UTF-8?q?=F0=9F=9A=9A=20Chore=20:=20`codecov`=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .github/workflows/gradle-test-main.yml | 3 ++- .github/workflows/gradle-test.yml | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gradle-test-main.yml b/.github/workflows/gradle-test-main.yml index 537f7ec..ba4a296 100644 --- a/.github/workflows/gradle-test-main.yml +++ b/.github/workflows/gradle-test-main.yml @@ -28,5 +28,6 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} slug: spring-templates/spring-concurrency-thread - fail_ci_if_error: true + flags: integration + fail_ci_if_error: true verbose: true diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 3f90416..ab1531a 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -2,8 +2,9 @@ name: Run gradlew test on: pull_request: - branches-ignore: - - main + branches: + - develop + - feature/** jobs: build: @@ -36,6 +37,6 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} slug: spring-templates/spring-concurrency-thread - fail_ci_if_error: true - verbose: true - flags: unittests + fail_ci_if_error: true + verbose: true + flags: ${{ github.ref == 'refs/pull/develop' && 'integration' || 'unittests' }} From b77d4834f98c7c91909e23c62cb43bc1a1c44da4 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Mon, 1 Apr 2024 09:59:55 +0900 Subject: [PATCH 15/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20`stressTest`=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../concurrency/counter/CounterTest.java | 76 +++++++++---------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java index c1cbc0b..3493483 100644 --- a/src/test/java/com/thread/concurrency/counter/CounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -1,70 +1,64 @@ package com.thread.concurrency.counter; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.test.context.SpringBootTest; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static java.lang.Thread.sleep; @SpringBootTest public class CounterTest { public static Stream counterProvider() { - return Stream.of(new BatchingCounter(), new LockCounter(), new PollingCounter()); + return Stream.of(new BatchingCounter(), new LockCounter(), new PollingCounter(), new BasicCounter()); + } + + private static void assertThen(Counter counter, int expectedValue, int actualValue) { + System.out.println("Expected value: " + expectedValue); + System.out.println("Actual value: " + actualValue); + if (counter instanceof BasicCounter) { + System.out.println("BasicCounter is not thread-safe"); + Assertions.assertNotEquals(expectedValue, actualValue); + } else { + System.out.println("Counter is thread-safe"); + Assertions.assertEquals(expectedValue, actualValue); + } } @ParameterizedTest @MethodSource("counterProvider") - public void testAddInMultithreadedEnvironment( - Counter counter - ) throws InterruptedException { + public void stressTest(Counter counter) throws InterruptedException { int initialValue = counter.show(); int nThreads = 100; int nAddsPerThread = 1000; int valueToAdd = 1; - - try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) { - for (int i = 0; i < nThreads; i++) { - executorService.submit(() -> { - for (int j = 0; j < nAddsPerThread; j++) { - counter.add(valueToAdd); - } - }); - } - if (counter instanceof BatchingCounter) { - Thread.sleep(500); - } - } - int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; - assertEquals(expectedValue, counter.show()); - } - @Test - public void testMalfunction() { - var counter = new BasicCounter(); - int initialValue = counter.show(); - int nThreads = 100; - int nAddsPerThread = 1000; - int valueToAdd = 1; - try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) { - for (int i = 0; i < nThreads; i++) { - executorService.submit(() -> { - for (int j = 0; j < nAddsPerThread; j++) { - counter.add(valueToAdd); - } - }); + // define runnable job + CountDownLatch latch = new CountDownLatch(nThreads); + Runnable job = () -> { + try { + latch.countDown(); // decrease the count + latch.await(); // wait until the count reaches 0 + for (int i = 0; i < nAddsPerThread; i++) { + counter.add(valueToAdd); + } + } catch (InterruptedException ignored) { } + }; + + // start nThreads threads + for (int i = 0; i < nThreads; i++) { + Thread.ofVirtual().start(job); } - int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; - assertNotEquals(expectedValue, counter.show()); + sleep(300); // wait for all threads to finish + + assertThen(counter, expectedValue, counter.show()); } } From 83c6f93bbe816d26ac159c8064c4d50cac885f5e Mon Sep 17 00:00:00 2001 From: ohchansol Date: Tue, 2 Apr 2024 15:55:55 +0900 Subject: [PATCH 16/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 컨슈머는 브로커에게서 이벤트를 가져와 처리한다. 2. 프로듀서는 브로커에게 이벤트를 저장한다. 3. 브로커는 큐를 관리하고 큐에 이벤트 삽입과 제거를 담당한다. --- .../producerCustomer/CounterBroker.java | 28 +++++++++++++++++++ .../producerCustomer/CounterCustomer.java | 16 +++++++++++ .../producerCustomer/CounterProducer.java | 13 +++++++++ .../concurrency/counter/QueueCounter.java | 18 ++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java create mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java create mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java create mode 100644 src/test/java/com/thread/concurrency/counter/QueueCounter.java diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java new file mode 100644 index 0000000..e759333 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java @@ -0,0 +1,28 @@ +package com.thread.concurrency.counter.producerCustomer; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Function; + +public class CounterBroker { + private BlockingQueue> queue = new LinkedBlockingQueue<>(); + private Integer count; + public void addEvent(int value){ + try{ + queue.put((c) -> c + value); // 이 이벤트를 컨슈머가 처리할 당시 count와 value를 더한 값을 출력한다. + } + catch(InterruptedException e){ + Thread.currentThread().interrupt(); + } + } + + public void consumEvent(){ + try{ + // "value를 더한다"라는 이벤트는 현재 스레드만 가질 수 있다. + count = queue.take().apply(count); + } + catch(InterruptedException e){ + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java new file mode 100644 index 0000000..b539b2c --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java @@ -0,0 +1,16 @@ +package com.thread.concurrency.counter.producerCustomer; + + +public class CounterCustomer{ + private CounterBroker counterBroker; + + public CounterCustomer(CounterBroker counterBroker) { + this.counterBroker = counterBroker; + } + + public void consumEvent(){ + while(true){ + counterBroker.consumEvent(); + } + } +} diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java new file mode 100644 index 0000000..ac94c74 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java @@ -0,0 +1,13 @@ +package com.thread.concurrency.counter.producerCustomer; + +public class CounterProducer { + private CounterBroker counterBroker; + + public CounterProducer(CounterBroker counterBroker) { + this.counterBroker = counterBroker; + } + + public void add(int value){ + counterBroker.addEvent(value); + } +} diff --git a/src/test/java/com/thread/concurrency/counter/QueueCounter.java b/src/test/java/com/thread/concurrency/counter/QueueCounter.java new file mode 100644 index 0000000..a45cea7 --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/QueueCounter.java @@ -0,0 +1,18 @@ +package com.thread.concurrency.counter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class QueueCounter { + private final int counteNumber = 1; + private final int totalCount = 10000; + private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); + + @Test + @DisplayName("producer consumer 패턴을 이용해서 더하기 이벤트 발생 스레드와 더하기 이벤트 처리 스레드를 분리하자") + public void 프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머(){ + + } +} From 0b332226916f7cb13869fee3696f3dcc31c07cac Mon Sep 17 00:00:00 2001 From: ohchansol Date: Tue, 2 Apr 2024 17:20:39 +0900 Subject: [PATCH 17/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 프로듀서-컨슈머 패턴을 이용한 카운터 구현을 위해 테스트를 작성 2. 시간 측정 결과 기존 CompletableFutureCounter에 2배 시간이 걸리는 걸로 나왔다. To Do CompletableFutureCounterTest와 QueueCounterTest에 쓰이는 쓰레드 수를 같게해서 다시 시간을 측정해보기 --- .../producerCustomer/CounterBroker.java | 22 ++++++-- .../producerCustomer/CounterCustomer.java | 12 ++-- .../producerCustomer/CounterProducer.java | 2 +- .../concurrency/counter/QueueCounter.java | 18 ------ .../concurrency/counter/QueueCounterTest.java | 56 +++++++++++++++++++ 5 files changed, 83 insertions(+), 27 deletions(-) delete mode 100644 src/test/java/com/thread/concurrency/counter/QueueCounter.java create mode 100644 src/test/java/com/thread/concurrency/counter/QueueCounterTest.java diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java index e759333..c6a1e8c 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java @@ -2,14 +2,21 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import java.util.function.IntUnaryOperator; +import java.util.function.UnaryOperator; public class CounterBroker { - private BlockingQueue> queue = new LinkedBlockingQueue<>(); - private Integer count; + // 100개의 이벤트를 저장할 수 있다. + private BlockingQueue> queue = new LinkedBlockingQueue<>(100); + private AtomicInteger count = new AtomicInteger(100); // AtomicInteger로 변경 public void addEvent(int value){ try{ - queue.put((c) -> c + value); // 이 이벤트를 컨슈머가 처리할 당시 count와 value를 더한 값을 출력한다. + // 이 이벤트를 컨슈머가 처리할 당시 count와 value를 더한 값을 출력한다. + // 만약 4초 동안 프로듀서가 요소를 넣지 못하면 timeout이 발생한다. + queue.offer((c) -> c + value, 4, TimeUnit.SECONDS); } catch(InterruptedException e){ Thread.currentThread().interrupt(); @@ -19,10 +26,17 @@ public void addEvent(int value){ public void consumEvent(){ try{ // "value를 더한다"라는 이벤트는 현재 스레드만 가질 수 있다. - count = queue.take().apply(count); + // AtomicInteger의 updateAndGet 메서드를 사용하여 원자적으로 값을 업데이트 + Function event = queue.take(); + IntUnaryOperator operator = event::apply; + count.updateAndGet(operator); } catch(InterruptedException e){ Thread.currentThread().interrupt(); } } + + public int show(){ + return count.get(); // AtomicInteger의 get 메서드를 사용하여 값을 가져옴 + } } diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java index b539b2c..ae14d8b 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java @@ -1,15 +1,19 @@ package com.thread.concurrency.counter.producerCustomer; -public class CounterCustomer{ - private CounterBroker counterBroker; +public class CounterCustomer { + private final CounterBroker counterBroker; + private volatile boolean running; public CounterCustomer(CounterBroker counterBroker) { this.counterBroker = counterBroker; + this.running = true; + } + public void stop() { + running = false; // 스레드 종료를 위해 running 플래그를 false로 설정 } - public void consumEvent(){ - while(true){ + while(running){ counterBroker.consumEvent(); } } diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java index ac94c74..c35110d 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java @@ -1,7 +1,7 @@ package com.thread.concurrency.counter.producerCustomer; public class CounterProducer { - private CounterBroker counterBroker; + private final CounterBroker counterBroker; public CounterProducer(CounterBroker counterBroker) { this.counterBroker = counterBroker; diff --git a/src/test/java/com/thread/concurrency/counter/QueueCounter.java b/src/test/java/com/thread/concurrency/counter/QueueCounter.java deleted file mode 100644 index a45cea7..0000000 --- a/src/test/java/com/thread/concurrency/counter/QueueCounter.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.thread.concurrency.counter; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class QueueCounter { - private final int counteNumber = 1; - private final int totalCount = 10000; - private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); - - @Test - @DisplayName("producer consumer 패턴을 이용해서 더하기 이벤트 발생 스레드와 더하기 이벤트 처리 스레드를 분리하자") - public void 프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머(){ - - } -} diff --git a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java new file mode 100644 index 0000000..595bac8 --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java @@ -0,0 +1,56 @@ +package com.thread.concurrency.counter; + +import com.thread.concurrency.counter.producerCustomer.CounterBroker; +import com.thread.concurrency.counter.producerCustomer.CounterCustomer; +import com.thread.concurrency.counter.producerCustomer.CounterProducer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class QueueCounterTest { + private final int counteNumber = 1; + private final int totalCount = 10000; + private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); + @Test + @DisplayName("producer consumer 패턴을 이용해서 더하기 이벤트 발생 스레드와 더하기 이벤트 처리 스레드를 분리하자") + public void 프로듀서_컨슈며_더하기_멀티_프로듀서_멀티_컨슈머() throws InterruptedException { + CounterBroker counterBroker = new CounterBroker(); + CounterCustomer customer = new CounterCustomer(counterBroker); + CounterProducer producer = new CounterProducer(counterBroker); + LocalTime lt1 = LocalTime.now(); + int initalCount = counterBroker.show(); + ExecutorService service = Executors.newFixedThreadPool(15); + CountDownLatch latch = new CountDownLatch(totalCount); + + // CounterCustomer 스레드 생성 및 비동기로 처리 시작 + List> futureList = new ArrayList<>(); + for(int i=0; i<3; i++){ + futureList.add(CompletableFuture.runAsync(customer::consumEvent)); + } + // 프로듀서 스레드 생성 + for (int i = 0; i < totalCount; i++) { + service.submit(() -> { + producer.add(counteNumber); + latch.countDown(); + }); + } + latch.await(); + + int finalCount = counterBroker.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000 + "ms"); + Assertions.assertEquals(initalCount + totalCount*counteNumber, finalCount); + } +} From 06cbe1f570de27d0e5ac5725863b38b4ee0b8b50 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 3 Apr 2024 08:56:30 +0900 Subject: [PATCH 18/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 브로커를 테스트에서 제거 2. 단일한 큐를 컨슈머와 프로듀서가 공유 3. 컨슈머가 카운터를 업데이트하는 역할을 가짐 To Do 컨슈머의 consumEvent를 실행하는 스레드와 show를 호출하는 메인 스레드 간 싱크가 맞지 않는다. 모든 consumEvent가 끝나고 show를 호출할 수 있는 방법이 필요하다. --- .../producerCustomer/CounterBroker.java | 50 +++++++------------ .../producerCustomer/CounterCustomer.java | 32 ++++++++---- .../producerCustomer/CounterProducer.java | 13 +++-- .../concurrency/counter/QueueCounterTest.java | 39 ++++++++------- 4 files changed, 70 insertions(+), 64 deletions(-) diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java index c6a1e8c..da1368a 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java @@ -3,40 +3,26 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import java.util.function.IntUnaryOperator; -import java.util.function.UnaryOperator; public class CounterBroker { - // 100개의 이벤트를 저장할 수 있다. - private BlockingQueue> queue = new LinkedBlockingQueue<>(100); - private AtomicInteger count = new AtomicInteger(100); // AtomicInteger로 변경 - public void addEvent(int value){ - try{ - // 이 이벤트를 컨슈머가 처리할 당시 count와 value를 더한 값을 출력한다. - // 만약 4초 동안 프로듀서가 요소를 넣지 못하면 timeout이 발생한다. - queue.offer((c) -> c + value, 4, TimeUnit.SECONDS); - } - catch(InterruptedException e){ - Thread.currentThread().interrupt(); - } - } + // 최대 1000개의 이벤트를 저장할 수 있다. + private final BlockingQueue> queue = new LinkedBlockingQueue<>(1000); - public void consumEvent(){ - try{ - // "value를 더한다"라는 이벤트는 현재 스레드만 가질 수 있다. - // AtomicInteger의 updateAndGet 메서드를 사용하여 원자적으로 값을 업데이트 - Function event = queue.take(); - IntUnaryOperator operator = event::apply; - count.updateAndGet(operator); - } - catch(InterruptedException e){ - Thread.currentThread().interrupt(); - } - } - - public int show(){ - return count.get(); // AtomicInteger의 get 메서드를 사용하여 값을 가져옴 - } +// public void addEvent(int value){ +// try{ +// // 이 이벤트를 컨슈머가 처리할 당시 count와 value를 더한 값을 출력한다. +// queue.put((c) -> c + value); +// } +// catch(InterruptedException e){ +// Thread.currentThread().interrupt(); +// } +// } +// public Function take(){ +// try { +// return queue.take(); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } } diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java index ae14d8b..a5e8d9a 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java @@ -1,20 +1,32 @@ package com.thread.concurrency.counter.producerCustomer; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.IntUnaryOperator; public class CounterCustomer { - private final CounterBroker counterBroker; - private volatile boolean running; + private final BlockingQueue> queue; + private AtomicInteger count = new AtomicInteger(100); // 스레드 안전성은 synchronized에게 맞기기 때문에 int로 변경. - public CounterCustomer(CounterBroker counterBroker) { - this.counterBroker = counterBroker; - this.running = true; + public CounterCustomer(BlockingQueue> queue) { + this.queue = queue; } - public void stop() { - running = false; // 스레드 종료를 위해 running 플래그를 false로 설정 + + public void consumEvent() throws InterruptedException { + while(!queue.isEmpty()){ + Function event = queue.take(); + IntUnaryOperator operator = event::apply; + synchronized (this){ + System.out.println(count.updateAndGet(operator)); + } + } } - public void consumEvent(){ - while(running){ - counterBroker.consumEvent(); + public int show(){ // 큐가 비어지는 마지막 순간에 if문이 true가 되어 count를 출력해버린다... + while(true){ + if(queue.isEmpty()){ + return count.get(); + } } } } diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java index c35110d..e068f5b 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java @@ -1,13 +1,16 @@ package com.thread.concurrency.counter.producerCustomer; +import java.util.concurrent.BlockingQueue; +import java.util.function.Function; + public class CounterProducer { - private final CounterBroker counterBroker; + private final BlockingQueue> queue; - public CounterProducer(CounterBroker counterBroker) { - this.counterBroker = counterBroker; + public CounterProducer(BlockingQueue> queue) { + this.queue = queue; } - public void add(int value){ - counterBroker.addEvent(value); + public void add(int value) throws InterruptedException { + queue.put((c) -> c + value); } } diff --git a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java index 595bac8..3ff8a3f 100644 --- a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java @@ -11,12 +11,8 @@ import java.time.Duration; import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.*; +import java.util.function.Function; public class QueueCounterTest { private final int counteNumber = 1; @@ -25,29 +21,38 @@ public class QueueCounterTest { @Test @DisplayName("producer consumer 패턴을 이용해서 더하기 이벤트 발생 스레드와 더하기 이벤트 처리 스레드를 분리하자") public void 프로듀서_컨슈며_더하기_멀티_프로듀서_멀티_컨슈머() throws InterruptedException { - CounterBroker counterBroker = new CounterBroker(); - CounterCustomer customer = new CounterCustomer(counterBroker); - CounterProducer producer = new CounterProducer(counterBroker); + BlockingQueue> queue = new LinkedBlockingQueue<>(1000); + CounterCustomer customer = new CounterCustomer(queue); + CounterProducer producer = new CounterProducer(queue); LocalTime lt1 = LocalTime.now(); - int initalCount = counterBroker.show(); + int initalCount = customer.show(); ExecutorService service = Executors.newFixedThreadPool(15); CountDownLatch latch = new CountDownLatch(totalCount); - // CounterCustomer 스레드 생성 및 비동기로 처리 시작 - List> futureList = new ArrayList<>(); - for(int i=0; i<3; i++){ - futureList.add(CompletableFuture.runAsync(customer::consumEvent)); - } // 프로듀서 스레드 생성 for (int i = 0; i < totalCount; i++) { service.submit(() -> { - producer.add(counteNumber); + try { + producer.add(counteNumber); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } latch.countDown(); }); } + // CounterCustomer 스레드 생성 및 비동기로 처리 시작 + for(int i=0; i<3; i++){ + CompletableFuture.runAsync(()->{ + try{ + customer.consumEvent(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } latch.await(); - int finalCount = counterBroker.show(); + int finalCount = customer.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000 + "ms"); From a752e80080b362ec53a2c845c80792e800339d69 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 3 Apr 2024 12:46:08 +0900 Subject: [PATCH 19/62] =?UTF-8?q?=F0=9F=90=9B=20Fix=20:=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 아직 프로듀서-컨슈머 문제해결 못함 2. consumer가 아직 이벤트를 처리 중임을 다른 스레드가 알 수 있거나 이벤트를 전부 끝냈다는 정보를 다른 스레드가 알 수 있게할 필요가 있다. --- .../producerCustomer/CounterBroker.java | 28 ---------------- .../producerCustomer/CounterConsumer.java | 33 +++++++++++++++++++ .../producerCustomer/CounterCustomer.java | 32 ------------------ .../counter/CompletableFutureCounterTest.java | 2 +- .../concurrency/counter/QueueCounterTest.java | 15 ++++----- .../counter/SynchronizedCounterTest.java | 4 +-- 6 files changed, 42 insertions(+), 72 deletions(-) delete mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java create mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java delete mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java deleted file mode 100644 index da1368a..0000000 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterBroker.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.thread.concurrency.counter.producerCustomer; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -public class CounterBroker { - // 최대 1000개의 이벤트를 저장할 수 있다. - private final BlockingQueue> queue = new LinkedBlockingQueue<>(1000); - -// public void addEvent(int value){ -// try{ -// // 이 이벤트를 컨슈머가 처리할 당시 count와 value를 더한 값을 출력한다. -// queue.put((c) -> c + value); -// } -// catch(InterruptedException e){ -// Thread.currentThread().interrupt(); -// } -// } -// public Function take(){ -// try { -// return queue.take(); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } -// } -} diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java new file mode 100644 index 0000000..c80d515 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java @@ -0,0 +1,33 @@ +package com.thread.concurrency.counter.producerCustomer; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.IntUnaryOperator; + +public class CounterConsumer { + private final BlockingQueue> queue; + private final AtomicInteger count = new AtomicInteger(100); // 스레드 안전성은 synchronized에게 맞기기 때문에 int로 변경. + + public CounterConsumer(BlockingQueue> queue) { + this.queue = queue; + } + + public void consumeEvent() throws InterruptedException { + while (!queue.isEmpty()) { + System.out.println("현재 큐 사이즈 : "+queue.size()); + Function event = queue.take(); + IntUnaryOperator operator = event::apply; + System.out.println("결과 카운트 : "+count.updateAndGet(operator)); + } + } + public int show(){ // 큐가 비어지는 마지막 순간에 if문이 true가 되어 count를 출력해버린다... + while(true){ + if(queue.isEmpty()){ + int ret = count.get(); + System.out.println("정답은 ? : "+ret); + return ret; + } + } + } +} diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java deleted file mode 100644 index a5e8d9a..0000000 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterCustomer.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.thread.concurrency.counter.producerCustomer; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.function.IntUnaryOperator; - -public class CounterCustomer { - private final BlockingQueue> queue; - private AtomicInteger count = new AtomicInteger(100); // 스레드 안전성은 synchronized에게 맞기기 때문에 int로 변경. - - public CounterCustomer(BlockingQueue> queue) { - this.queue = queue; - } - - public void consumEvent() throws InterruptedException { - while(!queue.isEmpty()){ - Function event = queue.take(); - IntUnaryOperator operator = event::apply; - synchronized (this){ - System.out.println(count.updateAndGet(operator)); - } - } - } - public int show(){ // 큐가 비어지는 마지막 순간에 if문이 true가 되어 count를 출력해버린다... - while(true){ - if(queue.isEmpty()){ - return count.get(); - } - } - } -} diff --git a/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java index 236bfd9..eac7fef 100644 --- a/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java @@ -37,7 +37,7 @@ public class CompletableFutureCounterTest { LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Compltable 테스트가 걸린 시간 : "+dif/1000+"ms"); + logger.info("여러_더하기_수행_Compltable 테스트가 걸린 시간 : "+dif/1000000+"ms"); Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); } } diff --git a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java index 3ff8a3f..4903c7b 100644 --- a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java @@ -1,7 +1,6 @@ package com.thread.concurrency.counter; -import com.thread.concurrency.counter.producerCustomer.CounterBroker; -import com.thread.concurrency.counter.producerCustomer.CounterCustomer; +import com.thread.concurrency.counter.producerCustomer.CounterConsumer; import com.thread.concurrency.counter.producerCustomer.CounterProducer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; @@ -22,10 +21,10 @@ public class QueueCounterTest { @DisplayName("producer consumer 패턴을 이용해서 더하기 이벤트 발생 스레드와 더하기 이벤트 처리 스레드를 분리하자") public void 프로듀서_컨슈며_더하기_멀티_프로듀서_멀티_컨슈머() throws InterruptedException { BlockingQueue> queue = new LinkedBlockingQueue<>(1000); - CounterCustomer customer = new CounterCustomer(queue); + CounterConsumer consumer = new CounterConsumer(queue); CounterProducer producer = new CounterProducer(queue); LocalTime lt1 = LocalTime.now(); - int initalCount = customer.show(); + int initalCount = consumer.show(); ExecutorService service = Executors.newFixedThreadPool(15); CountDownLatch latch = new CountDownLatch(totalCount); @@ -44,18 +43,16 @@ public class QueueCounterTest { for(int i=0; i<3; i++){ CompletableFuture.runAsync(()->{ try{ - customer.consumEvent(); + consumer.consumeEvent(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } - latch.await(); - - int finalCount = customer.show(); + int finalCount = consumer.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000 + "ms"); + logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); Assertions.assertEquals(initalCount + totalCount*counteNumber, finalCount); } } diff --git a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java index 0c24bec..2cab7da 100644 --- a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java @@ -46,7 +46,7 @@ public class SynchronizedCounterTest { int finalCount = counter.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : "+dif/1000+"ms"); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : "+dif/1000000+"ms"); Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); } @@ -80,7 +80,7 @@ public class SynchronizedCounterTest { LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_CompletableFuture 테스트가 걸린 시간 : "+dif/1000+"ms"); + logger.info("여러_더하기_수행_CompletableFuture 테스트가 걸린 시간 : "+dif/1000000+"ms"); Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); } } From 6494b667167f66f90077329f2c22b64266a4538e Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 3 Apr 2024 13:05:41 +0900 Subject: [PATCH 20/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EA=B8=B0=EB=8A=A5=20=E2=9C=85=20Test=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. SynchronizedCounter 와 CompletableFutureCounter를 생성 2. 두 카운터의 테스트를 작성 3. 테스트는 Integer.MAX_VALUE만큼 실행시키는 것이였으나 OutOfMemory 문제가 발생해서 500만번 수행 --- .../concurrency/counter/AtomicCounter.java | 18 ++++++ .../counter/CompletableFutureCounter.java | 25 +++++++++ .../counter/SynchronizedCounter.java | 19 +++++++ .../CompletableFutureCounterTest.java | 55 +++++++++++++++++++ .../concurrency/SynchronizedCounterTest.java | 55 +++++++++++++++++++ 5 files changed, 172 insertions(+) create mode 100644 src/main/java/com/thread/concurrency/counter/AtomicCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java create mode 100644 src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java create mode 100644 src/test/java/com/thread/concurrency/SynchronizedCounterTest.java diff --git a/src/main/java/com/thread/concurrency/counter/AtomicCounter.java b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java new file mode 100644 index 0000000..1fdf3bd --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java @@ -0,0 +1,18 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class AtomicCounter implements Counter{ + private final AtomicInteger count = new AtomicInteger(100); + @Override + public void add(int value) { + count.addAndGet(value); + } + @Override + public int show() { + return count.get(); + } +} diff --git a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java new file mode 100644 index 0000000..008b94f --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java @@ -0,0 +1,25 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +@Component +public class CompletableFutureCounter implements Counter{ + + private CompletableFuture counter = new CompletableFuture<>(); + + @Override + public void add(int value) { + counter = counter.thenApply((c) -> c + value); + } + + @Override + public int show() { + try { + return counter.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java new file mode 100644 index 0000000..aa70b45 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java @@ -0,0 +1,19 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +@Component +public class SynchronizedCounter implements Counter{ + + private int counter = 100; + + @Override + public synchronized void add(int value) { + counter += value; + } + + @Override + public synchronized int show() { + return counter; + } +} diff --git a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java new file mode 100644 index 0000000..979dcce --- /dev/null +++ b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java @@ -0,0 +1,55 @@ +package com.thread.concurrency; + +import com.thread.concurrency.counter.CompletableFutureCounter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@SpringBootTest +public class CompletableFutureCounterTest { + + private final int counteNumber = 1; + private final int totalCount = 5000000; + private final int maxThreadNumber = 15; + private static final Logger logger = LoggerFactory.getLogger(CompletableFutureCounterTest.class); + + @Autowired + CompletableFutureCounter counter; + /** + * 실행 완료까지 871ms 정도 소요 + * + * @throws InterruptedException + */ + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + public void 여러_더하기_수행_Executor() throws InterruptedException { + + LocalTime lt1 = LocalTime.now(); + int initalCount = counter.show(); + + ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); + CountDownLatch latch = new CountDownLatch(totalCount); + for (int i = 0; i < totalCount; i++) { + service.submit(() -> { + counter.add(counteNumber); + latch.countDown(); + }); + } + latch.await(); + int finalCount = counter.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); + } +} diff --git a/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java new file mode 100644 index 0000000..0d5b633 --- /dev/null +++ b/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java @@ -0,0 +1,55 @@ +package com.thread.concurrency; + +import com.thread.concurrency.counter.SynchronizedCounter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@SpringBootTest +public class SynchronizedCounterTest { + + private final int counteNumber = 1; + private final int totalCount = 5000000; + private final int maxThreadNumber = 15; + private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); + + @Autowired + SynchronizedCounter counter; + /** + * 실행 완료까지 871ms 정도 소요 + * + * @throws InterruptedException + */ + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + public void 여러_더하기_수행_Executor() throws InterruptedException { + + LocalTime lt1 = LocalTime.now(); + int initalCount = counter.show(); + + ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); + CountDownLatch latch = new CountDownLatch(totalCount); + for (int i = 0; i < totalCount; i++) { + service.submit(() -> { + counter.add(counteNumber); + latch.countDown(); + }); + } + latch.await(); + int finalCount = counter.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); + } +} From db113d73f069cdcd162cd6317337c61e76e07a7a Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 3 Apr 2024 14:10:28 +0900 Subject: [PATCH 21/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. CompletableFuture 자료형은 스레드에 안전하지 않다. 2. 따라서 counter 값을 읽고 업데이트하는 과정을 하나의 단일 연산으로 만들어줘야한다. 3. CompleableFutureCounter에 대한 테스트도 작성 완료 --- .../counter/CompletableFutureCounter.java | 12 +++-- .../thread/concurrency/AtomicCounterTest.java | 49 ++++++++++++++++++ .../CompletableFutureCounterTest.java | 51 +++++++++++-------- .../concurrency/SynchronizedCounterTest.java | 8 +-- 4 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 src/test/java/com/thread/concurrency/AtomicCounterTest.java diff --git a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java index 008b94f..00e97bb 100644 --- a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java +++ b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java @@ -7,13 +7,17 @@ @Component public class CompletableFutureCounter implements Counter{ - private CompletableFuture counter = new CompletableFuture<>(); - + private CompletableFuture counter; + public CompletableFutureCounter(){ + this.counter = new CompletableFuture<>(); + counter.complete(100); + } @Override public void add(int value) { - counter = counter.thenApply((c) -> c + value); + synchronized (this){ + counter = counter.thenApply((c) -> c + value); + } } - @Override public int show() { try { diff --git a/src/test/java/com/thread/concurrency/AtomicCounterTest.java b/src/test/java/com/thread/concurrency/AtomicCounterTest.java new file mode 100644 index 0000000..d104356 --- /dev/null +++ b/src/test/java/com/thread/concurrency/AtomicCounterTest.java @@ -0,0 +1,49 @@ +package com.thread.concurrency; + +import com.thread.concurrency.counter.AtomicCounter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@SpringBootTest +public class AtomicCounterTest { + private final int counteNumber = 1; + private final int totalCount = 5000000; + private final int maxThreadNumber = 15; + private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); + @Autowired + AtomicCounter counter; + + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") + public void 여러_더하기_수행_Executor() throws InterruptedException { + + LocalTime lt1 = LocalTime.now(); + int initalCount = counter.show(); + + ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); + CountDownLatch latch = new CountDownLatch(totalCount); + for (int i = 0; i < totalCount; i++) { + service.submit(() -> { + counter.add(counteNumber); + latch.countDown(); + }); + } + latch.await(); + int finalCount = counter.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); + } +} diff --git a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java index 979dcce..7cb0890 100644 --- a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java +++ b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java @@ -8,48 +8,57 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.time.Duration; import java.time.LocalTime; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; @SpringBootTest public class CompletableFutureCounterTest { private final int counteNumber = 1; - private final int totalCount = 5000000; + private final int totalCount = 5000; private final int maxThreadNumber = 15; private static final Logger logger = LoggerFactory.getLogger(CompletableFutureCounterTest.class); @Autowired CompletableFutureCounter counter; - /** - * 실행 완료까지 871ms 정도 소요 - * - * @throws InterruptedException - */ - @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") - public void 여러_더하기_수행_Executor() throws InterruptedException { + @Test + @DisplayName("CompletableFuture로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") + public void 여러_더하기_수행_Executor() { + ExecutorService executorService = Executors.newFixedThreadPool(maxThreadNumber); LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); + int initialCount = counter.show(); - ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); - CountDownLatch latch = new CountDownLatch(totalCount); - for (int i = 0; i < totalCount; i++) { - service.submit(() -> { + List> tasks = new ArrayList<>(); + for(int i=0; i { counter.add(counteNumber); - latch.countDown(); + }, executorService)); + } + + CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); + for (CompletableFuture future : tasks) { + aggregate = aggregate.thenCompose(list -> { + try { + list.add(future.get()); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return CompletableFuture.completedFuture(list); }); } - latch.await(); + aggregate.join(); // 전체 비동기 결과 집계 int finalCount = counter.show(); + LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); - Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); + logger.info("여러_더하기_수행_CompletableFuture 테스트가 걸린 시간 : "+dif/1000000+"ms"); + Assertions.assertEquals(initialCount+totalCount*counteNumber, finalCount); } } diff --git a/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java index 0d5b633..7b073b5 100644 --- a/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java +++ b/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java @@ -25,13 +25,9 @@ public class SynchronizedCounterTest { @Autowired SynchronizedCounter counter; - /** - * 실행 완료까지 871ms 정도 소요 - * - * @throws InterruptedException - */ + @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") public void 여러_더하기_수행_Executor() throws InterruptedException { LocalTime lt1 = LocalTime.now(); From d7e8a86e5bb664ae3949349e25662e39e9de4100 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 3 Apr 2024 14:14:19 +0900 Subject: [PATCH 22/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=E2=9C=85=20?= =?UTF-8?q?Test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. CompletableFutureCounter의 테스트를 다른 카운터와 동일하게 작성 --- .../CompletableFutureCounterTest.java | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java index 7cb0890..3999676 100644 --- a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java +++ b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java @@ -27,38 +27,25 @@ public class CompletableFutureCounterTest { @Autowired CompletableFutureCounter counter; - @Test @DisplayName("CompletableFuture로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") - public void 여러_더하기_수행_Executor() { - ExecutorService executorService = Executors.newFixedThreadPool(maxThreadNumber); + public void 여러_더하기_수행_Executor() throws InterruptedException { LocalTime lt1 = LocalTime.now(); - int initialCount = counter.show(); + int initalCount = counter.show(); - List> tasks = new ArrayList<>(); - for(int i=0; i { + ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); + CountDownLatch latch = new CountDownLatch(totalCount); + for (int i = 0; i < totalCount; i++) { + service.submit(() -> { counter.add(counteNumber); - }, executorService)); - } - - CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); - for (CompletableFuture future : tasks) { - aggregate = aggregate.thenCompose(list -> { - try { - list.add(future.get()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - return CompletableFuture.completedFuture(list); + latch.countDown(); }); } - aggregate.join(); // 전체 비동기 결과 집계 + latch.await(); int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_CompletableFuture 테스트가 걸린 시간 : "+dif/1000000+"ms"); - Assertions.assertEquals(initialCount+totalCount*counteNumber, finalCount); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); } } From a6870ac1bebf60784925f0346931a625acb0fb95 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 3 Apr 2024 14:42:41 +0900 Subject: [PATCH 23/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=E2=9C=85=20?= =?UTF-8?q?Test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 보기 쉽게 각 테스트들 정리 --- .../concurrency/counter/AtomicCounter.java | 18 +++++++ .../counter/AtomicCounterTest.java | 48 +++++++++++++++++++ .../counter/CompletableFutureCounterTest.java | 21 ++++---- .../counter/SynchronizedCounterTest.java | 25 +++++----- 4 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/thread/concurrency/counter/AtomicCounter.java create mode 100644 src/test/java/com/thread/concurrency/counter/AtomicCounterTest.java diff --git a/src/main/java/com/thread/concurrency/counter/AtomicCounter.java b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java new file mode 100644 index 0000000..1fdf3bd --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java @@ -0,0 +1,18 @@ +package com.thread.concurrency.counter; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class AtomicCounter implements Counter{ + private final AtomicInteger count = new AtomicInteger(100); + @Override + public void add(int value) { + count.addAndGet(value); + } + @Override + public int show() { + return count.get(); + } +} diff --git a/src/test/java/com/thread/concurrency/counter/AtomicCounterTest.java b/src/test/java/com/thread/concurrency/counter/AtomicCounterTest.java new file mode 100644 index 0000000..1a55a18 --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/AtomicCounterTest.java @@ -0,0 +1,48 @@ +package com.thread.concurrency.counter; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@SpringBootTest +public class AtomicCounterTest { + private final int counteNumber = 1; + private final int totalCount = 5000000; + private final int maxThreadNumber = 15; + private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); + @Autowired + AtomicCounter counter; + + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") + public void 여러_더하기_수행_Executor() throws InterruptedException { + + LocalTime lt1 = LocalTime.now(); + int initalCount = counter.show(); + + ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); + CountDownLatch latch = new CountDownLatch(totalCount); + for (int i = 0; i < totalCount; i++) { + service.submit(() -> { + counter.add(counteNumber); + latch.countDown(); + }); + } + latch.await(); + int finalCount = counter.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); + } +} diff --git a/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java index eac7fef..86162ae 100644 --- a/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CompletableFutureCounterTest.java @@ -1,9 +1,11 @@ package com.thread.concurrency.counter; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.time.Duration; @@ -14,17 +16,21 @@ @SpringBootTest public class CompletableFutureCounterTest { + private final int counteNumber = 1; - private final int totalCount = 10000; + private final int totalCount = 5000; + private final int maxThreadNumber = 15; private static final Logger logger = LoggerFactory.getLogger(CompletableFutureCounterTest.class); + @Autowired + CompletableFutureCounter counter; @Test - public void 여러_더하기_수행_Compltable() throws InterruptedException { - SynchronizedCounter counter = new SynchronizedCounter(); + @DisplayName("CompletableFuture로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") + public void 여러_더하기_수행_Executor() throws InterruptedException { LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); - ExecutorService service = Executors.newFixedThreadPool(15); + + ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); CountDownLatch latch = new CountDownLatch(totalCount); for (int i = 0; i < totalCount; i++) { service.submit(() -> { @@ -34,10 +40,9 @@ public class CompletableFutureCounterTest { } latch.await(); int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Compltable 테스트가 걸린 시간 : "+dif/1000000+"ms"); - Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); } } diff --git a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java index 2cab7da..9630e2f 100644 --- a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java @@ -20,21 +20,21 @@ public class SynchronizedCounterTest { private final int counteNumber = 1; - private final int totalCount = 10000; + private final int totalCount = 5000000; + private final int maxThreadNumber = 15; private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); - /** - * 실행 완료까지 871ms 정도 소요 - * @throws InterruptedException - */ + @Autowired + SynchronizedCounter counter; + @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") public void 여러_더하기_수행_Executor() throws InterruptedException { - SynchronizedCounter counter = new SynchronizedCounter(); + LocalTime lt1 = LocalTime.now(); int initalCount = counter.show(); - int numberOfThreads = 15; - ExecutorService service = Executors.newFixedThreadPool(15); + + ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); CountDownLatch latch = new CountDownLatch(totalCount); for (int i = 0; i < totalCount; i++) { service.submit(() -> { @@ -46,13 +46,10 @@ public class SynchronizedCounterTest { int finalCount = counter.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : "+dif/1000000+"ms"); - Assertions.assertEquals(initalCount+totalCount*counteNumber, finalCount); + logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); } - /** - * 실행 완료까지 1061ms 소요 - */ @Test @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") public void 여러_더하기_수행_CompletableFuture() { From fd6771133321bef26854806c0ee8c165f7e5839f Mon Sep 17 00:00:00 2001 From: ohchansol Date: Thu, 4 Apr 2024 22:37:35 +0900 Subject: [PATCH 24/62] =?UTF-8?q?=F0=9F=93=9D=20Docs=20:=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 밴치마크 결과 Readme에 작성 --- README.md | 17 ++++++++++++++++- .../thread/concurrency/AtomicCounterTest.java | 7 +++++-- .../CompletableFutureCounterTest.java | 7 +++++-- .../concurrency/SynchronizedCounterTest.java | 5 ++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 86633b3..e2b66a9 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ -# spring-thread-concurrency \ No newline at end of file +# spring-thread-concurrency + +## 카운터 밴치 마크 + +**실험 환경** +칩 : Apple M2 8코어 +메모리 : 16GB + +| 5회 평균 | 최대 스레드 개수 | 전체 요청 수 | 테스트 시간(ms) | 메모리 사용량(MB) | +| --- | --- | --- | --- | --- | +| AtomicCounter | 9 | 5,000,000 | 321.15 | 12.82 | +| AtomicCounter | 15 | 5,000,000 | 419.33 | 12.16 | +| CompletableFutureCounter | 9 | 5,000,000 | 885.95 | 11.78 | +| CompletableFutureCounter | 15 | 5,000,000 | 939.16 | 11.78 | +| SynchronizedCounter | 9 | 5,000,000 | 398.63 | 12.32 | +| SynchronizedCounter | 15 | 5,000,000 | 495.99 | 11.86 | diff --git a/src/test/java/com/thread/concurrency/AtomicCounterTest.java b/src/test/java/com/thread/concurrency/AtomicCounterTest.java index d104356..a9a4e45 100644 --- a/src/test/java/com/thread/concurrency/AtomicCounterTest.java +++ b/src/test/java/com/thread/concurrency/AtomicCounterTest.java @@ -19,7 +19,7 @@ public class AtomicCounterTest { private final int counteNumber = 1; private final int totalCount = 5000000; - private final int maxThreadNumber = 15; + private final int maxThreadNumber = 9; private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); @Autowired AtomicCounter counter; @@ -27,7 +27,7 @@ public class AtomicCounterTest { @Test @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") public void 여러_더하기_수행_Executor() throws InterruptedException { - + Runtime.getRuntime().gc(); LocalTime lt1 = LocalTime.now(); int initalCount = counter.show(); @@ -44,6 +44,9 @@ public class AtomicCounterTest { LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Runtime.getRuntime().gc(); + long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + logger.info("메모리 사용량 "+(double)usedMemory/1048576 + " MB"); Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); } } diff --git a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java index 3999676..d0ebd57 100644 --- a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java +++ b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java @@ -21,8 +21,8 @@ public class CompletableFutureCounterTest { private final int counteNumber = 1; - private final int totalCount = 5000; - private final int maxThreadNumber = 15; + private final int totalCount = 5000000; + private final int maxThreadNumber = 9; private static final Logger logger = LoggerFactory.getLogger(CompletableFutureCounterTest.class); @Autowired @@ -46,6 +46,9 @@ public class CompletableFutureCounterTest { LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Runtime.getRuntime().gc(); + long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + logger.info("메모리 사용량 "+(double)usedMemory/1048576 + " MB"); Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); } } diff --git a/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java index 7b073b5..074e966 100644 --- a/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java +++ b/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java @@ -20,7 +20,7 @@ public class SynchronizedCounterTest { private final int counteNumber = 1; private final int totalCount = 5000000; - private final int maxThreadNumber = 15; + private final int maxThreadNumber = 9; private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); @Autowired @@ -46,6 +46,9 @@ public class SynchronizedCounterTest { LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + Runtime.getRuntime().gc(); + long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + logger.info("메모리 사용량 "+(double)usedMemory/1048576 + " MB"); Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); } } From 1100a8571a67f3667ba61ad61ac5d9791c280208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=B0=AC=EC=86=94?= <53365713+haxr369@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:40:48 +0000 Subject: [PATCH 25/62] =?UTF-8?q?@haxr369=20=EC=B9=B4=EC=9A=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B0=B4=EC=B9=98=20=EB=A7=88=ED=81=AC=20=EC=B8=A1=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 3303d8c..6aac20b 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,19 @@ # 정보 - [동시성 기본 조건과 관심사](https://github.com/spring-templates/spring-concurrency-thread/discussions/2) + +## 카운터 밴치 마크 + +**실험 환경** + +칩 : Apple M2 8코어, +메모리 : 16GB + +| 5회 평균 | 최대 스레드 개수 | 전체 요청 수 | 테스트 시간(ms) | 메모리 사용량(MB) | +| --- | --- | --- | --- | --- | +| AtomicCounter | 9 | 5,000,000 | 321.15 | 12.82 | +| AtomicCounter | 15 | 5,000,000 | 419.33 | 12.16 | +| CompletableFutureCounter | 9 | 5,000,000 | 885.95 | 11.78 | +| CompletableFutureCounter | 15 | 5,000,000 | 939.16 | 11.78 | +| SynchronizedCounter | 9 | 5,000,000 | 398.63 | 12.32 | +| SynchronizedCounter | 15 | 5,000,000 | 495.99 | 11.86 | \ No newline at end of file From d9ec163c803d7644d87c89ea2db5f83b8530c026 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Fri, 5 Apr 2024 08:59:09 +0900 Subject: [PATCH 26/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20`ConcurrentBatchCo?= =?UTF-8?q?unter`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - YAGNI 원칙에 따른 기능 최적화 Signed-off-by: Hyeon-hak Kim --- .../concurrency/counter/BatchingCounter.java | 38 --------------- .../counter/batch/BatchCounter.java | 7 +++ .../batch/ConcurrentBatchingCounter.java | 39 +++++++++++++++ ...oncurrentParameterizedBatchingCounter.java | 48 +++++++++++++++++++ 4 files changed, 94 insertions(+), 38 deletions(-) delete mode 100644 src/main/java/com/thread/concurrency/counter/BatchingCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/batch/BatchCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java create mode 100644 src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java diff --git a/src/main/java/com/thread/concurrency/counter/BatchingCounter.java b/src/main/java/com/thread/concurrency/counter/BatchingCounter.java deleted file mode 100644 index f2396ef..0000000 --- a/src/main/java/com/thread/concurrency/counter/BatchingCounter.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.thread.concurrency.counter; - -import org.springframework.stereotype.Component; - -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -@Component -public class BatchingCounter implements Counter { - private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); - private static final ConcurrentLinkedQueue jobQueue = new ConcurrentLinkedQueue<>(); - private static volatile int count = 100; - - public BatchingCounter() { - Runnable runnableTask = () -> { - while (!jobQueue.isEmpty()) { - synchronized (this) { - var value = jobQueue.poll(); - count += value == null ? 0 : value; - } - } - }; - // context switching을 최소화하는 최소한의 시간마다 실행하여 성능 향상 - scheduledExecutorService.scheduleAtFixedRate(runnableTask, 4, 5, TimeUnit.MILLISECONDS); - } - - @Override - public void add(int value) { - jobQueue.add(value); - } - - @Override - public int show() { - return count; - } -} diff --git a/src/main/java/com/thread/concurrency/counter/batch/BatchCounter.java b/src/main/java/com/thread/concurrency/counter/batch/BatchCounter.java new file mode 100644 index 0000000..bfd3a77 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/batch/BatchCounter.java @@ -0,0 +1,7 @@ +package com.thread.concurrency.counter.batch; + +import com.thread.concurrency.counter.Counter; + +public interface BatchCounter extends Counter { + void flush(); +} diff --git a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java new file mode 100644 index 0000000..07b70be --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java @@ -0,0 +1,39 @@ +package com.thread.concurrency.counter.batch; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +@Component +public class ConcurrentBatchingCounter implements BatchCounter { + + private final AtomicLong counter = new AtomicLong(); + private final ConcurrentMap batch = new ConcurrentHashMap<>(); + + @Override + public void add(int value) { + var threadId = Thread.currentThread().threadId(); + batch.computeIfAbsent(threadId, k -> new LongAdder()).add(value); + } + + @Override + public int show() { + return counter.intValue(); + } + + private void flush(long threadId) { + var value = batch.remove(threadId); + if (value != null) { + counter.addAndGet(value.longValue()); + } + } + + @Override + public void flush() { + var threadId = Thread.currentThread().threadId(); + flush(threadId); + } +} diff --git a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java new file mode 100644 index 0000000..cbd6ccd --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java @@ -0,0 +1,48 @@ +package com.thread.concurrency.counter.batch; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +@Component +@Profile("dev") +public class ConcurrentParameterizedBatchingCounter implements BatchCounter { + + private static final int BATCH_SIZE = 100; + + private final AtomicLong counter = new AtomicLong(); + private final ConcurrentMap> batch = new ConcurrentHashMap<>(); + + @Override + public void add(int value) { + var threadId = Thread.currentThread().threadId(); + batch.computeIfAbsent(threadId, k -> new ArrayList<>()).add(value); + if (batch.get(threadId).size() >= BATCH_SIZE) { + flush(threadId); + } + } + + @Override + public int show() { + return counter.intValue(); + } + + private void flush(long threadId) { + var list = batch.getOrDefault(threadId, null); + if (list != null && !list.isEmpty()) { + counter.addAndGet(list.stream().mapToLong(Integer::longValue).sum()); + batch.remove(threadId); + } + } + + @Override + public void flush() { + var threadId = Thread.currentThread().threadId(); + flush(threadId); + } +} From 7b074acddab8ace7223f0dc961553c5e340a7dd3 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Fri, 5 Apr 2024 09:01:11 +0900 Subject: [PATCH 27/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20`BatchCounter`=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Async/Virtual: `newVirtualThreadPerTaskExecutor` - Sync/OS: `newFixedThreadPool` - Async/OS: `newCachedThreadPool` (삭제) Signed-off-by: Hyeon-hak Kim --- .../counter/batch/BatchCounterTest.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java diff --git a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java new file mode 100644 index 0000000..0c68150 --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java @@ -0,0 +1,110 @@ +package com.thread.concurrency.counter.batch; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BatchCounterTest { + private final List numbers; + private final Runnable defaultTask; + private final int partialSum; + private BatchCounter counter; + + public BatchCounterTest() { + this.numbers = range(); + this.defaultTask = () -> { + for (Integer number : numbers) { + counter.add(number); + } + counter.flush(); + }; + this.partialSum = numbers.stream().reduce(0, Integer::sum); + } + + private static List range() { + return IntStream.range(0, 1000).boxed().collect(Collectors.toList()); + } + + private static List range(int numberOfThreads, int expected) { + int baseValue = expected / numberOfThreads; + int remainder = expected % numberOfThreads; + + List result = new ArrayList<>(); + for (int i = 0; i < numberOfThreads; i++) { + if (i < remainder) { + result.add(baseValue + 1); + } else { + result.add(baseValue); + } + } + return result; + } + + @BeforeEach + void setUp() { + this.counter = new ConcurrentBatchingCounter(); + } + + + @Test + void singleThreading() { + defaultTask.run(); + assertEquals(partialSum, counter.show()); + } + + + @Test + void conditionalMultiThreading() { + // given + int numberOfThreads = 2; + int expected = Integer.MAX_VALUE / 1024; + List iterPerThread = range(numberOfThreads, expected); + Consumer task = (Integer number) -> { + for (int i = 0; i < number; i++) { + counter.add(1); + } + counter.flush(); + }; + // when + try (ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads)) { + for (int num : iterPerThread) { + executor.submit(() -> task.accept(num)); + } + } + // then + assertEquals(expected, counter.show()); + } + + @Test + void conditionalAsyncVirtualMultiThreading() { + // given + int numberOfThreads = 2; + int expected = Integer.MAX_VALUE / 1024; + List iterPerThread = range(numberOfThreads, expected); + Consumer task = (Integer number) -> { + for (int i = 0; i < number; i++) { + counter.add(1); + } + counter.flush(); + }; + // when + try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + List> futures = iterPerThread.stream() + .map(number -> CompletableFuture.runAsync(() -> task.accept(number), executor)) + .toList(); + futures.forEach(CompletableFuture::join); + } + // then + assertEquals(expected, counter.show()); + } +} From e4e68ededbfe0a9b077e2bc374a5b4bdd560d44a Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Fri, 5 Apr 2024 09:06:06 +0900 Subject: [PATCH 28/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20Simplify=20?= =?UTF-8?q?logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../concurrency/counter/CounterTest.java | 62 +++++++------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java index 3493483..2958804 100644 --- a/src/test/java/com/thread/concurrency/counter/CounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -5,60 +5,44 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.test.context.SpringBootTest; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Stream; -import static java.lang.Thread.sleep; - @SpringBootTest public class CounterTest { public static Stream counterProvider() { - return Stream.of(new BatchingCounter(), new LockCounter(), new PollingCounter(), new BasicCounter()); + return Stream.of(new LockCounter(), new PollingCounter()); } - private static void assertThen(Counter counter, int expectedValue, int actualValue) { - System.out.println("Expected value: " + expectedValue); - System.out.println("Actual value: " + actualValue); - if (counter instanceof BasicCounter) { - System.out.println("BasicCounter is not thread-safe"); - Assertions.assertNotEquals(expectedValue, actualValue); - } else { - System.out.println("Counter is thread-safe"); - Assertions.assertEquals(expectedValue, actualValue); + private static void whenAdd(Counter counter, int nThreads, int addPerThread) { + try (ExecutorService executor = Executors.newFixedThreadPool(nThreads)) { + for (int i = 0; i < nThreads; i++) { + executor.submit(() -> { + for (int j = 0; j < addPerThread; j++) { + counter.add(1); + } + }); + } } } @ParameterizedTest @MethodSource("counterProvider") - public void stressTest(Counter counter) throws InterruptedException { - int initialValue = counter.show(); + public void stressTest(Counter counter) { + // given int nThreads = 100; - int nAddsPerThread = 1000; - int valueToAdd = 1; - int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; - - - // define runnable job - CountDownLatch latch = new CountDownLatch(nThreads); - Runnable job = () -> { - try { - latch.countDown(); // decrease the count - latch.await(); // wait until the count reaches 0 - for (int i = 0; i < nAddsPerThread; i++) { - counter.add(valueToAdd); - } - } catch (InterruptedException ignored) { - } - }; - - // start nThreads threads - for (int i = 0; i < nThreads; i++) { - Thread.ofVirtual().start(job); - } + int addPerThread = 1000; + int expectedValue = counter.show() + nThreads * addPerThread; - sleep(300); // wait for all threads to finish + // when + long start = System.currentTimeMillis(); + whenAdd(counter, nThreads, addPerThread); + long end = System.currentTimeMillis(); - assertThen(counter, expectedValue, counter.show()); + // then + Assertions.assertEquals(expectedValue, counter.show()); + System.out.println("Time elapsed: " + (end - start) + "ms"); } } From a6ab12928d70445b2a204df00abc3966d5c08da9 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Fri, 5 Apr 2024 09:06:19 +0900 Subject: [PATCH 29/62] =?UTF-8?q?=F0=9F=9A=9A=20Chore=20:=20set=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- src/main/resources/application.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e92628c..4cc2930 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=spring-thread-concurrency +spring.profiles.active=default From 61eb93928cfdcabedec0b2bc56d14f0ea744f310 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Fri, 5 Apr 2024 09:06:33 +0900 Subject: [PATCH 30/62] =?UTF-8?q?=F0=9F=8E=A8=20Style=20:=20polishing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .github/ISSUE_TEMPLATE/bug_report.md | 17 ++++++++++------- ...SpringThreadConcurrencyApplicationTests.java | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) rename src/test/java/com/thread/concurrency/{ => counter}/SpringThreadConcurrencyApplicationTests.java (77%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..3205926 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,6 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/src/test/java/com/thread/concurrency/SpringThreadConcurrencyApplicationTests.java b/src/test/java/com/thread/concurrency/counter/SpringThreadConcurrencyApplicationTests.java similarity index 77% rename from src/test/java/com/thread/concurrency/SpringThreadConcurrencyApplicationTests.java rename to src/test/java/com/thread/concurrency/counter/SpringThreadConcurrencyApplicationTests.java index 675a607..60cb352 100644 --- a/src/test/java/com/thread/concurrency/SpringThreadConcurrencyApplicationTests.java +++ b/src/test/java/com/thread/concurrency/counter/SpringThreadConcurrencyApplicationTests.java @@ -1,15 +1,15 @@ -package com.thread.concurrency; +package com.thread.concurrency.counter; +import com.thread.concurrency.SpringThreadConcurrencyApplication; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @SpringBootTest class SpringThreadConcurrencyApplicationTests { - @Test void contextLoads() { assertDoesNotThrow(() -> SpringThreadConcurrencyApplication.main(new String[]{})); } - } From f98cdbdc94ac8311d2070d171d4cb9e6d27776e2 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Fri, 5 Apr 2024 10:01:09 +0900 Subject: [PATCH 31/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. CounterConsumer의 System.out.println을 주석하면 데드락이 걸리는 건지 테스트가 진행이 안되는 문제.,. --- .../producerCustomer/CounterConsumer.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java index c80d515..b665af0 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java @@ -14,19 +14,23 @@ public CounterConsumer(BlockingQueue> queue) { } public void consumeEvent() throws InterruptedException { - while (!queue.isEmpty()) { - System.out.println("현재 큐 사이즈 : "+queue.size()); - Function event = queue.take(); - IntUnaryOperator operator = event::apply; - System.out.println("결과 카운트 : "+count.updateAndGet(operator)); + synchronized (count){ + while (!queue.isEmpty()) { +// System.out.println("현재 큐 사이즈 : "+queue.size()); + Function event = queue.take(); + IntUnaryOperator operator = event::apply; + count.updateAndGet(operator); +// System.out.println("결과 카운트 : "+); + } + } } public int show(){ // 큐가 비어지는 마지막 순간에 if문이 true가 되어 count를 출력해버린다... while(true){ - if(queue.isEmpty()){ - int ret = count.get(); - System.out.println("정답은 ? : "+ret); - return ret; + synchronized (count){ + if(queue.isEmpty()){ + return count.get(); + } } } } From 7860d1d847c7e466db58f7dc6b7900e0c14c1f5b Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Fri, 5 Apr 2024 20:08:05 +0900 Subject: [PATCH 32/62] =?UTF-8?q?=F0=9F=93=9D=20Docs=20:=20`ConcurrentBatc?= =?UTF-8?q?hingCounter`=20=EB=B2=A4=EC=B9=98=EB=A7=88=ED=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- README.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6aac20b..4cf4039 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,32 @@ **실험 환경** -칩 : Apple M2 8코어, +칩 : Apple M2 8코어, 메모리 : 16GB -| 5회 평균 | 최대 스레드 개수 | 전체 요청 수 | 테스트 시간(ms) | 메모리 사용량(MB) | -| --- | --- | --- | --- | --- | -| AtomicCounter | 9 | 5,000,000 | 321.15 | 12.82 | -| AtomicCounter | 15 | 5,000,000 | 419.33 | 12.16 | -| CompletableFutureCounter | 9 | 5,000,000 | 885.95 | 11.78 | -| CompletableFutureCounter | 15 | 5,000,000 | 939.16 | 11.78 | -| SynchronizedCounter | 9 | 5,000,000 | 398.63 | 12.32 | -| SynchronizedCounter | 15 | 5,000,000 | 495.99 | 11.86 | \ No newline at end of file +| 5회 평균 | 최대 스레드 개수 | 전체 요청 수 | 테스트 시간(ms) | 메모리 사용량(MB) | +|--------------------------|-----------|-----------|------------|-------------| +| AtomicCounter | 9 | 5,000,000 | 321.15 | 12.82 | +| AtomicCounter | 15 | 5,000,000 | 419.33 | 12.16 | +| CompletableFutureCounter | 9 | 5,000,000 | 885.95 | 11.78 | +| CompletableFutureCounter | 15 | 5,000,000 | 939.16 | 11.78 | +| SynchronizedCounter | 9 | 5,000,000 | 398.63 | 12.32 | +| SynchronizedCounter | 15 | 5,000,000 | 495.99 | 11.86 | + +- **프로세서** 12th Gen Intel(R) Core(TM) i7-12650H, 2300Mhz, 10 코어, 16 논리 프로세서 + +- **구현체** ConcurrentBatchingCounter + +- **전체 요청 수** Integer.MAX_VALUE + +| 스레드 개수 | 테스트 시간(s) | 메모리 사용량(MB) | +|--------|-----------|-------------| +| 1 | 26-28 | 4 | +| 2 | 14 | 4 | +| 4 | 9 | 4 | +| 8 | 7 | 4 | +| 16 | 5 | 4 | +| 32 | 5-6 | 4 | +| 64 | 6 | 4 | +| 128 | 9 | 337 | +| 1024 | 10 | 373 | From 1bb0cc0d04aabe33df9b8ec0aaf8c63c23779575 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Fri, 5 Apr 2024 20:11:40 +0900 Subject: [PATCH 33/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20'ConcurrentBatchin?= =?UTF-8?q?gCounter'=20=EB=B2=A4=EC=B9=98=EB=A7=88=ED=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../SpringThreadConcurrencyApplication.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java index 42b9717..43c5be1 100644 --- a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java +++ b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java @@ -1,13 +1,81 @@ package com.thread.concurrency; +import com.thread.concurrency.counter.batch.BatchCounter; +import com.thread.concurrency.counter.batch.ConcurrentBatchingCounter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + @SpringBootApplication public class SpringThreadConcurrencyApplication { public static void main(String[] args) { SpringApplication.run(SpringThreadConcurrencyApplication.class, args); + + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage initialMemoryUsage = memoryMXBean.getHeapMemoryUsage(); + long initialTime = System.currentTimeMillis(); + + // Run the test + int totalRequest = Integer.MAX_VALUE; + conditionalMultiThreading(totalRequest); + + MemoryUsage finalMemoryUsage = memoryMXBean.getHeapMemoryUsage(); + long finalTime = System.currentTimeMillis(); + long elapsedTime = finalTime - initialTime; + long usedMemory = finalMemoryUsage.getUsed() - initialMemoryUsage.getUsed(); + + // request with comma + System.out.println("Total request: " + String.format("%,d", totalRequest)); + // seconds + System.out.println("Elapsed time: " + elapsedTime / 1000 + " s"); + // megabytes + System.out.println("Used memory: " + usedMemory / 1024 / 1024 + " MB"); + } + + private static void conditionalMultiThreading(int expected) { + BatchCounter counter = new ConcurrentBatchingCounter(); + + // given + int numberOfThreads = 128; + List iterPerThread = range(numberOfThreads, expected); + Consumer task = (Integer number) -> { + for (int i = 0; i < number; i++) { + counter.add(1); + } + counter.flush(); + }; + // when + try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + List> futures = iterPerThread.stream().map(number -> CompletableFuture.runAsync(() -> task.accept(number), executor)).toList(); + futures.forEach(CompletableFuture::join); + } + // then + assert expected == counter.show(); + } + + private static List range(int numberOfThreads, int expected) { + int baseValue = expected / numberOfThreads; + int remainder = expected % numberOfThreads; + + List result = new ArrayList<>(); + for (int i = 0; i < numberOfThreads; i++) { + if (i < remainder) { + result.add(baseValue + 1); + } else { + result.add(baseValue); + } + } + return result; } } From 8eba8ee7457f809a8a17b55d278115280ab739e9 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sat, 6 Apr 2024 23:18:10 +0900 Subject: [PATCH 34/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=E2=9C=85=20?= =?UTF-8?q?Test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 테스트가 진행되지 않는 이유를 찾음 프로듀서가 큐에 작업을 넣는 속도 보다 컨슈머가 작업을 처리하는 속도가 더 빠름 그렇기 때문에 전체 작업을 하기도 전에 큐가 다 비어버림 큐가 빈것을 인지한 컨슈머가 작업을 끝냄. 더 이상 작업이 진행되지 않음 To Do 프로듀서의 작업 추가 속도를 올리기 컨슈머의 작업 처리 속도를 늦추기 혹은 다 처리해도 대기하기 --- .../producerCustomer/CounterConsumer.java | 29 +++---- .../producerCustomer/CounterProducer.java | 8 +- .../concurrency/counter/QueueCounterTest.java | 82 +++++++++++++++++-- 3 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java index b665af0..2108ebd 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java @@ -2,35 +2,32 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.function.IntUnaryOperator; +import java.util.function.LongUnaryOperator; public class CounterConsumer { - private final BlockingQueue> queue; - private final AtomicInteger count = new AtomicInteger(100); // 스레드 안전성은 synchronized에게 맞기기 때문에 int로 변경. + private final BlockingQueue queue; + private final AtomicLong count = new AtomicLong(0); // 스레드 안전성은 synchronized에게 맞기기 때문에 int로 변경. - public CounterConsumer(BlockingQueue> queue) { + public CounterConsumer(BlockingQueue queue) { this.queue = queue; } public void consumeEvent() throws InterruptedException { - synchronized (count){ - while (!queue.isEmpty()) { -// System.out.println("현재 큐 사이즈 : "+queue.size()); - Function event = queue.take(); - IntUnaryOperator operator = event::apply; - count.updateAndGet(operator); -// System.out.println("결과 카운트 : "+); - } + while (!queue.isEmpty()) { + System.out.println(Thread.currentThread().getName()+"은 현재 큐 사이즈 : "+queue.size()); + Long value = queue.take(); +// count.addAndGet(value); + System.out.println("결과 카운트 : "+count.addAndGet(value)); } } - public int show(){ // 큐가 비어지는 마지막 순간에 if문이 true가 되어 count를 출력해버린다... + public Long show(){ while(true){ - synchronized (count){ - if(queue.isEmpty()){ - return count.get(); - } + if(queue.isEmpty()){ + return count.get(); } } } diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java index e068f5b..74df1d7 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java @@ -4,13 +4,13 @@ import java.util.function.Function; public class CounterProducer { - private final BlockingQueue> queue; + private final BlockingQueue queue; - public CounterProducer(BlockingQueue> queue) { + public CounterProducer(BlockingQueue queue) { this.queue = queue; } - public void add(int value) throws InterruptedException { - queue.put((c) -> c + value); + public void add(long value) throws InterruptedException { + queue.put(value); } } diff --git a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java index 4903c7b..5c0d14c 100644 --- a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java @@ -10,29 +10,34 @@ import java.time.Duration; import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.*; import java.util.function.Function; public class QueueCounterTest { private final int counteNumber = 1; - private final int totalCount = 10000; + private final int totalCount = 50000000; // Integer.MAX_VALUE; + private final int nThread = 15; private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); @Test @DisplayName("producer consumer 패턴을 이용해서 더하기 이벤트 발생 스레드와 더하기 이벤트 처리 스레드를 분리하자") public void 프로듀서_컨슈며_더하기_멀티_프로듀서_멀티_컨슈머() throws InterruptedException { - BlockingQueue> queue = new LinkedBlockingQueue<>(1000); + BlockingQueue queue = new LinkedBlockingQueue<>(1000); CounterConsumer consumer = new CounterConsumer(queue); CounterProducer producer = new CounterProducer(queue); LocalTime lt1 = LocalTime.now(); - int initalCount = consumer.show(); - ExecutorService service = Executors.newFixedThreadPool(15); + Long initalCount = consumer.show(); + ExecutorService service = Executors.newFixedThreadPool(nThread); CountDownLatch latch = new CountDownLatch(totalCount); // 프로듀서 스레드 생성 - for (int i = 0; i < totalCount; i++) { + for (int i = 0; i < nThread; i++) { service.submit(() -> { try { - producer.add(counteNumber); + for(int j=0; j{ try{ consumer.consumeEvent(); @@ -49,10 +54,69 @@ public class QueueCounterTest { } }); } - int finalCount = consumer.show(); + latch.await(); +// Long finalCount = consumer.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); +// Assertions.assertEquals(initalCount + totalCount*counteNumber, finalCount); + } + + @Test + @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") + public void 여러_더하기_수행_CompletableFuture() throws InterruptedException { + BlockingQueue queue = new LinkedBlockingQueue<>(1000); + CounterConsumer consumer = new CounterConsumer(queue); + CounterProducer producer = new CounterProducer(queue); + LocalTime lt1 = LocalTime.now(); + Long initalCount = consumer.show(); + ExecutorService service = Executors.newFixedThreadPool(nThread); + CountDownLatch latch = new CountDownLatch(nThread); + + // 프로듀서 스레드 생성 + for (int i = 0; i < nThread; i++) { + service.submit(() -> { + try { + for(int j=0; j> tasks = new ArrayList<>(); + for(int i=0; i<3; i++){ + tasks.add(CompletableFuture.runAsync(()->{ + try{ + consumer.consumeEvent(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + } + + CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); + for (CompletableFuture future : tasks) { + aggregate = aggregate.thenCompose(list -> { + try { + list.add(future.get()); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return CompletableFuture.completedFuture(list); + }); + } + aggregate.join(); // 전체 비동기 결과 집계 + System.out.println("컨슈머 작업 끝남"); + latch.await(); + System.out.println("프로듀서 작업 끝남"); + Long finalCount = consumer.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); - Assertions.assertEquals(initalCount + totalCount*counteNumber, finalCount); + Assertions.assertEquals(initalCount + (totalCount/nThread)*nThread*counteNumber, finalCount); } } From e0edb4fd10480e7a3b79fdafc03f73cc2b0e05a3 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Sun, 7 Apr 2024 08:47:45 +0900 Subject: [PATCH 35/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20clean-up=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../thread/concurrency/AtomicCounterTest.java | 49 ------------- .../CompletableFutureCounterTest.java | 51 -------------- .../concurrency/SynchronizedCounterTest.java | 51 -------------- .../concurrency/counter/CounterTest.java | 2 +- .../counter/batch/BatchCounterTest.java | 69 +++++++++---------- 5 files changed, 33 insertions(+), 189 deletions(-) delete mode 100644 src/test/java/com/thread/concurrency/AtomicCounterTest.java delete mode 100644 src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java delete mode 100644 src/test/java/com/thread/concurrency/SynchronizedCounterTest.java diff --git a/src/test/java/com/thread/concurrency/AtomicCounterTest.java b/src/test/java/com/thread/concurrency/AtomicCounterTest.java deleted file mode 100644 index d104356..0000000 --- a/src/test/java/com/thread/concurrency/AtomicCounterTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.thread.concurrency; - -import com.thread.concurrency.counter.AtomicCounter; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@SpringBootTest -public class AtomicCounterTest { - private final int counteNumber = 1; - private final int totalCount = 5000000; - private final int maxThreadNumber = 15; - private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); - @Autowired - AtomicCounter counter; - - @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") - public void 여러_더하기_수행_Executor() throws InterruptedException { - - LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); - - ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); - CountDownLatch latch = new CountDownLatch(totalCount); - for (int i = 0; i < totalCount; i++) { - service.submit(() -> { - counter.add(counteNumber); - latch.countDown(); - }); - } - latch.await(); - int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); - Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); - } -} diff --git a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java deleted file mode 100644 index 3999676..0000000 --- a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.thread.concurrency; - -import com.thread.concurrency.counter.CompletableFutureCounter; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.*; - -@SpringBootTest -public class CompletableFutureCounterTest { - - private final int counteNumber = 1; - private final int totalCount = 5000; - private final int maxThreadNumber = 15; - private static final Logger logger = LoggerFactory.getLogger(CompletableFutureCounterTest.class); - - @Autowired - CompletableFutureCounter counter; - @Test - @DisplayName("CompletableFuture로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") - public void 여러_더하기_수행_Executor() throws InterruptedException { - LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); - - ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); - CountDownLatch latch = new CountDownLatch(totalCount); - for (int i = 0; i < totalCount; i++) { - service.submit(() -> { - counter.add(counteNumber); - latch.countDown(); - }); - } - latch.await(); - int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); - Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); - } -} diff --git a/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java deleted file mode 100644 index 7b073b5..0000000 --- a/src/test/java/com/thread/concurrency/SynchronizedCounterTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.thread.concurrency; - -import com.thread.concurrency.counter.SynchronizedCounter; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@SpringBootTest -public class SynchronizedCounterTest { - - private final int counteNumber = 1; - private final int totalCount = 5000000; - private final int maxThreadNumber = 15; - private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); - - @Autowired - SynchronizedCounter counter; - - @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") - public void 여러_더하기_수행_Executor() throws InterruptedException { - - LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); - - ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); - CountDownLatch latch = new CountDownLatch(totalCount); - for (int i = 0; i < totalCount; i++) { - service.submit(() -> { - counter.add(counteNumber); - latch.countDown(); - }); - } - latch.await(); - int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); - Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); - } -} diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java index 2958804..95222b8 100644 --- a/src/test/java/com/thread/concurrency/counter/CounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -13,7 +13,7 @@ public class CounterTest { public static Stream counterProvider() { - return Stream.of(new LockCounter(), new PollingCounter()); + return Stream.of(new LockCounter(), new PollingCounter(), new SynchronizedCounter(), new AtomicCounter(), new CompletableFutureCounter()); } private static void whenAdd(Counter counter, int nThreads, int addPerThread) { diff --git a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java index 0c68150..0924777 100644 --- a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java @@ -1,7 +1,9 @@ package com.thread.concurrency.counter.batch; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import com.thread.concurrency.counter.Counter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.ArrayList; import java.util.List; @@ -11,25 +13,9 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.stream.Stream; class BatchCounterTest { - private final List numbers; - private final Runnable defaultTask; - private final int partialSum; - private BatchCounter counter; - - public BatchCounterTest() { - this.numbers = range(); - this.defaultTask = () -> { - for (Integer number : numbers) { - counter.add(number); - } - counter.flush(); - }; - this.partialSum = numbers.stream().reduce(0, Integer::sum); - } private static List range() { return IntStream.range(0, 1000).boxed().collect(Collectors.toList()); @@ -50,21 +36,31 @@ private static List range(int numberOfThreads, int expected) { return result; } - @BeforeEach - void setUp() { - this.counter = new ConcurrentBatchingCounter(); + public static Stream batchCounterProvider() { + return Stream.of(new ConcurrentBatchingCounter(), new ConcurrentParameterizedBatchingCounter()); } - - @Test - void singleThreading() { - defaultTask.run(); - assertEquals(partialSum, counter.show()); + @ParameterizedTest + @MethodSource("batchCounterProvider") + void singleThreading(BatchCounter counter) { + // given + var numbers = range(); + var partialSum = numbers.stream().reduce(0, Integer::sum); + Runnable task = () -> { + for (Integer number : numbers) { + counter.add(number); + } + counter.flush(); + }; + // when + task.run(); + // then + Assertions.assertEquals(partialSum, counter.show()); } - - @Test - void conditionalMultiThreading() { + @ParameterizedTest + @MethodSource("batchCounterProvider") + void conditionalMultiThreading(BatchCounter counter) { // given int numberOfThreads = 2; int expected = Integer.MAX_VALUE / 1024; @@ -82,11 +78,12 @@ void conditionalMultiThreading() { } } // then - assertEquals(expected, counter.show()); + Assertions.assertEquals(expected, counter.show()); } - @Test - void conditionalAsyncVirtualMultiThreading() { + @ParameterizedTest + @MethodSource("batchCounterProvider") + void conditionalAsyncVirtualMultiThreading(BatchCounter counter) { // given int numberOfThreads = 2; int expected = Integer.MAX_VALUE / 1024; @@ -99,12 +96,10 @@ void conditionalAsyncVirtualMultiThreading() { }; // when try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { - List> futures = iterPerThread.stream() - .map(number -> CompletableFuture.runAsync(() -> task.accept(number), executor)) - .toList(); + List> futures = iterPerThread.stream().map(number -> CompletableFuture.runAsync(() -> task.accept(number), executor)).toList(); futures.forEach(CompletableFuture::join); } // then - assertEquals(expected, counter.show()); + Assertions.assertEquals(expected, counter.show()); } } From 90567f9598a9440061383200621dea33fafb7f56 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Sun, 7 Apr 2024 08:48:55 +0900 Subject: [PATCH 36/62] =?UTF-8?q?=F0=9F=8E=A8=20Style=20:=20polishing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../com/thread/concurrency/counter/AtomicCounter.java | 4 +++- .../concurrency/counter/CompletableFutureCounter.java | 10 +++++++--- .../concurrency/counter/SynchronizedCounter.java | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/thread/concurrency/counter/AtomicCounter.java b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java index 1fdf3bd..cc90632 100644 --- a/src/main/java/com/thread/concurrency/counter/AtomicCounter.java +++ b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java @@ -5,12 +5,14 @@ import java.util.concurrent.atomic.AtomicInteger; @Component -public class AtomicCounter implements Counter{ +public class AtomicCounter implements Counter { private final AtomicInteger count = new AtomicInteger(100); + @Override public void add(int value) { count.addAndGet(value); } + @Override public int show() { return count.get(); diff --git a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java index 00e97bb..dad5b72 100644 --- a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java +++ b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java @@ -1,23 +1,27 @@ package com.thread.concurrency.counter; import org.springframework.stereotype.Component; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @Component -public class CompletableFutureCounter implements Counter{ +public class CompletableFutureCounter implements Counter { private CompletableFuture counter; - public CompletableFutureCounter(){ + + public CompletableFutureCounter() { this.counter = new CompletableFuture<>(); counter.complete(100); } + @Override public void add(int value) { - synchronized (this){ + synchronized (this) { counter = counter.thenApply((c) -> c + value); } } + @Override public int show() { try { diff --git a/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java index aa70b45..4c29b8f 100644 --- a/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java +++ b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java @@ -3,7 +3,7 @@ import org.springframework.stereotype.Component; @Component -public class SynchronizedCounter implements Counter{ +public class SynchronizedCounter implements Counter { private int counter = 100; From 2b1356c3c1a4311c7d37cf632dbcb428c15eedf9 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Sun, 7 Apr 2024 08:49:13 +0900 Subject: [PATCH 37/62] =?UTF-8?q?=F0=9F=94=A5=20Remove=20:=20`BasicCounter?= =?UTF-8?q?`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../concurrency/counter/BasicCounter.java | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/main/java/com/thread/concurrency/counter/BasicCounter.java diff --git a/src/main/java/com/thread/concurrency/counter/BasicCounter.java b/src/main/java/com/thread/concurrency/counter/BasicCounter.java deleted file mode 100644 index a331799..0000000 --- a/src/main/java/com/thread/concurrency/counter/BasicCounter.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.thread.concurrency.counter; - -import org.springframework.stereotype.Component; - -@Component -public class BasicCounter implements Counter { - private static int count = 100; - - @Override - public void add(int value) { - count += value; - } - - @Override - public int show() { - return count; - } -} From 867a7b879b8df92d282afe8db6193e36bdefcba1 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Sun, 7 Apr 2024 08:51:37 +0900 Subject: [PATCH 38/62] =?UTF-8?q?=F0=9F=9A=9A=20Chore=20:=20simplify=20`ma?= =?UTF-8?q?in`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../thread/concurrency/SpringThreadConcurrencyApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java index 43c5be1..6e9cb2c 100644 --- a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java +++ b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java @@ -26,7 +26,7 @@ public static void main(String[] args) { long initialTime = System.currentTimeMillis(); // Run the test - int totalRequest = Integer.MAX_VALUE; + int totalRequest = Integer.MAX_VALUE / 1024; conditionalMultiThreading(totalRequest); MemoryUsage finalMemoryUsage = memoryMXBean.getHeapMemoryUsage(); From c763bb4cc47411c728ebcf29e9cfaa919b714d66 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 11:57:36 +0900 Subject: [PATCH 39/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=E2=9C=85=20?= =?UTF-8?q?Test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 프로듀서-컨슈머 패턴 구현 완료 2. 요구사항인 Integer.MAX_VALUE만큼 더하기 완료 To Do 메모리 누수 때문에 메모리 덤프가 생성됨 메모리 누수의 원인을 찾고 해결해야함 --- .../producerCustomer/CounterConsumer.java | 21 +++--- .../concurrency/counter/QueueCounterTest.java | 72 ++++++++++--------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java index 2108ebd..61f7c5c 100644 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java +++ b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java @@ -1,6 +1,8 @@ package com.thread.concurrency.counter.producerCustomer; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; @@ -9,19 +11,22 @@ public class CounterConsumer { private final BlockingQueue queue; - private final AtomicLong count = new AtomicLong(0); // 스레드 안전성은 synchronized에게 맞기기 때문에 int로 변경. + private final AtomicLong count = new AtomicLong(0); public CounterConsumer(BlockingQueue queue) { this.queue = queue; } - public void consumeEvent() throws InterruptedException { - - while (!queue.isEmpty()) { - System.out.println(Thread.currentThread().getName()+"은 현재 큐 사이즈 : "+queue.size()); - Long value = queue.take(); -// count.addAndGet(value); - System.out.println("결과 카운트 : "+count.addAndGet(value)); + public void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException { + while (true) { +// System.out.println(Thread.currentThread().getName()+"은 현재 큐 사이즈 : "+queue.size()); + Long value = queue.poll(timeout, unit); + if(value == null){ +// System.out.println(Thread.currentThread().getName()+" 끝났으!!"); + break; + } + count.addAndGet(value); +// System.out.println("결s과 카운트 : "+count.addAndGet(value)); } } public Long show(){ diff --git a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java index 5c0d14c..0d170b0 100644 --- a/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/QueueCounterTest.java @@ -17,25 +17,28 @@ public class QueueCounterTest { private final int counteNumber = 1; - private final int totalCount = 50000000; // Integer.MAX_VALUE; + private final int totalCount = Integer.MAX_VALUE; private final int nThread = 15; private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); + @Test - @DisplayName("producer consumer 패턴을 이용해서 더하기 이벤트 발생 스레드와 더하기 이벤트 처리 스레드를 분리하자") - public void 프로듀서_컨슈며_더하기_멀티_프로듀서_멀티_컨슈머() throws InterruptedException { + @DisplayName("멀티 프로듀서 싱글 컨슈머") + public void 멀티_프로듀서_싱글_컨슈머() throws InterruptedException { BlockingQueue queue = new LinkedBlockingQueue<>(1000); CounterConsumer consumer = new CounterConsumer(queue); CounterProducer producer = new CounterProducer(queue); LocalTime lt1 = LocalTime.now(); Long initalCount = consumer.show(); ExecutorService service = Executors.newFixedThreadPool(nThread); - CountDownLatch latch = new CountDownLatch(totalCount); + CountDownLatch latch = new CountDownLatch(nThread); // 프로듀서 스레드 생성 for (int i = 0; i < nThread; i++) { +// int finalI = i; service.submit(() -> { try { - for(int j=0; j{ - try{ - consumer.consumeEvent(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }); + CompletableFuture task = CompletableFuture.runAsync(()->{ + try{ + consumer.consumeEvent(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + try { + task.get(); + } catch (ExecutionException e) { + throw new RuntimeException(e); } + System.out.println("컨슈머 작업 끝남"); latch.await(); -// Long finalCount = consumer.show(); + System.out.println("프로듀서 작업 끝남"); + + Long finalCount = consumer.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); -// Assertions.assertEquals(initalCount + totalCount*counteNumber, finalCount); + Assertions.assertEquals(initalCount + (totalCount/nThread)*nThread*counteNumber, finalCount); } - @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기. 활동성 문제 예상") - public void 여러_더하기_수행_CompletableFuture() throws InterruptedException { + @DisplayName("멀티 프로듀서 멀티 컨슈머") + public void 멀티_프로듀서_멀티_컨슈머() throws InterruptedException { BlockingQueue queue = new LinkedBlockingQueue<>(1000); CounterConsumer consumer = new CounterConsumer(queue); CounterProducer producer = new CounterProducer(queue); @@ -73,11 +81,10 @@ public class QueueCounterTest { ExecutorService service = Executors.newFixedThreadPool(nThread); CountDownLatch latch = new CountDownLatch(nThread); - // 프로듀서 스레드 생성 for (int i = 0; i < nThread; i++) { service.submit(() -> { try { - for(int j=0; j> tasks = new ArrayList<>(); for(int i=0; i<3; i++){ - tasks.add(CompletableFuture.runAsync(()->{ + tasks.add( CompletableFuture.runAsync(()->{ try{ - consumer.consumeEvent(); + consumer.consumeEvent(1, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); } })); } - - CompletableFuture> aggregate = CompletableFuture.completedFuture(new ArrayList<>()); - for (CompletableFuture future : tasks) { - aggregate = aggregate.thenCompose(list -> { - try { - list.add(future.get()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - return CompletableFuture.completedFuture(list); - }); - } - aggregate.join(); // 전체 비동기 결과 집계 + tasks.forEach((t) -> { + try { +// System.out.println("적당히 돌아가는거 확인합닛다."); + t.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + }); System.out.println("컨슈머 작업 끝남"); latch.await(); System.out.println("프로듀서 작업 끝남"); From ada5ae2df53739aab96f8edfce6a060a4636dd79 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 17:27:18 +0900 Subject: [PATCH 40/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=E2=9C=85=20?= =?UTF-8?q?Test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=201.=20=ED=94=84=EB=A1=9C=EB=93=80=EC=84=9C?= =?UTF-8?q?=20=EC=BB=A8=EC=8A=88=EB=A8=B8=20pr=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../counter/queueCounter/CounterConsumer.java | 34 ++++++ .../counter/queueCounter/CounterProducer.java | 15 +++ .../thread/concurrency/AtomicCounterTest.java | 52 -------- .../CompletableFutureCounterTest.java | 54 -------- .../com/thread/concurrency/CounterTest.java | 54 ++++++++ .../concurrency/SynchronizedCounterTest.java | 54 -------- .../concurrency/counter/CounterTest.java | 2 +- .../queueCounter/QueueCounterTest.java | 115 ++++++++++++++++++ 8 files changed, 219 insertions(+), 161 deletions(-) create mode 100644 src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java create mode 100644 src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java delete mode 100644 src/test/java/com/thread/concurrency/AtomicCounterTest.java delete mode 100644 src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java create mode 100644 src/test/java/com/thread/concurrency/CounterTest.java delete mode 100644 src/test/java/com/thread/concurrency/SynchronizedCounterTest.java create mode 100644 src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java new file mode 100644 index 0000000..c12edf1 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java @@ -0,0 +1,34 @@ +package com.thread.concurrency.counter.queueCounter; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class CounterConsumer { + private final BlockingQueue queue; + private final AtomicLong count = new AtomicLong(0); + + public CounterConsumer(BlockingQueue queue) { + this.queue = queue; + } + + public void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException { + while (true) { +// System.out.println(Thread.currentThread().getName()+"은 현재 큐 사이즈 : "+queue.size()); + Long value = queue.poll(timeout, unit); + if(value == null){ +// System.out.println(Thread.currentThread().getName()+" 끝났으!!"); + break; + } + count.addAndGet(value); +// System.out.println("결s과 카운트 : "+count.addAndGet(value)); + } + } + public Long show(){ + while(true){ + if(queue.isEmpty()){ + return count.get(); + } + } + } +} diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java new file mode 100644 index 0000000..126e608 --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java @@ -0,0 +1,15 @@ +package com.thread.concurrency.counter.queueCounter; + +import java.util.concurrent.BlockingQueue; + +public class CounterProducer { + private final BlockingQueue queue; + + public CounterProducer(BlockingQueue queue) { + this.queue = queue; + } + + public void add(long value) throws InterruptedException { + queue.put(value); + } +} diff --git a/src/test/java/com/thread/concurrency/AtomicCounterTest.java b/src/test/java/com/thread/concurrency/AtomicCounterTest.java deleted file mode 100644 index a9a4e45..0000000 --- a/src/test/java/com/thread/concurrency/AtomicCounterTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.thread.concurrency; - -import com.thread.concurrency.counter.AtomicCounter; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@SpringBootTest -public class AtomicCounterTest { - private final int counteNumber = 1; - private final int totalCount = 5000000; - private final int maxThreadNumber = 9; - private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); - @Autowired - AtomicCounter counter; - - @Test - @DisplayName("synchronized로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") - public void 여러_더하기_수행_Executor() throws InterruptedException { - Runtime.getRuntime().gc(); - LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); - - ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); - CountDownLatch latch = new CountDownLatch(totalCount); - for (int i = 0; i < totalCount; i++) { - service.submit(() -> { - counter.add(counteNumber); - latch.countDown(); - }); - } - latch.await(); - int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); - Runtime.getRuntime().gc(); - long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - logger.info("메모리 사용량 "+(double)usedMemory/1048576 + " MB"); - Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); - } -} diff --git a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java b/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java deleted file mode 100644 index d0ebd57..0000000 --- a/src/test/java/com/thread/concurrency/CompletableFutureCounterTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.thread.concurrency; - -import com.thread.concurrency.counter.CompletableFutureCounter; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.*; - -@SpringBootTest -public class CompletableFutureCounterTest { - - private final int counteNumber = 1; - private final int totalCount = 5000000; - private final int maxThreadNumber = 9; - private static final Logger logger = LoggerFactory.getLogger(CompletableFutureCounterTest.class); - - @Autowired - CompletableFutureCounter counter; - @Test - @DisplayName("CompletableFuture로 스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") - public void 여러_더하기_수행_Executor() throws InterruptedException { - LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); - - ExecutorService service = Executors.newFixedThreadPool(maxThreadNumber); - CountDownLatch latch = new CountDownLatch(totalCount); - for (int i = 0; i < totalCount; i++) { - service.submit(() -> { - counter.add(counteNumber); - latch.countDown(); - }); - } - latch.await(); - int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); - Runtime.getRuntime().gc(); - long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - logger.info("메모리 사용량 "+(double)usedMemory/1048576 + " MB"); - Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); - } -} diff --git a/src/test/java/com/thread/concurrency/CounterTest.java b/src/test/java/com/thread/concurrency/CounterTest.java new file mode 100644 index 0000000..d88727d --- /dev/null +++ b/src/test/java/com/thread/concurrency/CounterTest.java @@ -0,0 +1,54 @@ +package com.thread.concurrency; + +import com.thread.concurrency.counter.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Stream; + +@SpringBootTest +public class CounterTest { + private final int valueToAdd = 1; + private final int nAddsPerThread = 50000000; + private final int nThreads = 9; + + public static Stream counterProvider() { + return Stream.of(new AtomicCounter(), new CompletableFutureCounter(), new SynchronizedCounter()); + } + + @ParameterizedTest + @MethodSource("counterProvider") + @DisplayName("스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") + public void 여러_더하기_수행_Executor(Counter counter) throws InterruptedException { + LocalTime lt1 = LocalTime.now(); + int initalCount = counter.show(); + + ExecutorService service = Executors.newFixedThreadPool(nThreads); + CountDownLatch latch = new CountDownLatch(nThreads); + for (int i = 0; i < nThreads; i++) { + service.submit(() -> { + for(int j=0; j { - counter.add(counteNumber); - latch.countDown(); - }); - } - latch.await(); - int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); - Runtime.getRuntime().gc(); - long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - logger.info("메모리 사용량 "+(double)usedMemory/1048576 + " MB"); - Assertions.assertEquals(initalCount + totalCount * counteNumber, finalCount); - } -} diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java index 3493483..19a8a40 100644 --- a/src/test/java/com/thread/concurrency/counter/CounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -34,7 +34,7 @@ private static void assertThen(Counter counter, int expectedValue, int actualVal public void stressTest(Counter counter) throws InterruptedException { int initialValue = counter.show(); int nThreads = 100; - int nAddsPerThread = 1000; + int nAddsPerThread = 100000; int valueToAdd = 1; int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd; diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java new file mode 100644 index 0000000..a6f231a --- /dev/null +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -0,0 +1,115 @@ +package com.thread.concurrency.queueCounter; + +import com.thread.concurrency.counter.queueCounter.CounterConsumer; +import com.thread.concurrency.counter.queueCounter.CounterProducer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.*; + +public class QueueCounterTest { + private final int valueToAdd = 1; + private final int nAddsPerThread = 50000000; + private final int nThreads = 9; + private final long timeout = 1; + private final TimeUnit unit = TimeUnit.SECONDS; + + @Test + @DisplayName("멀티 프로듀서 싱글 컨슈머") + public void 멀티_프로듀서_싱글_컨슈머() throws InterruptedException { + BlockingQueue queue = new LinkedBlockingQueue<>(1000); + CounterConsumer consumer = new CounterConsumer(queue); + CounterProducer producer = new CounterProducer(queue); + LocalTime lt1 = LocalTime.now(); + Long initalCount = consumer.show(); + ExecutorService producerService = Executors.newFixedThreadPool(nThreads); + ExecutorService consumerService = Executors.newFixedThreadPool(nThreads); + CountDownLatch producerLatch = new CountDownLatch(nThreads); + CountDownLatch consumerLatch = new CountDownLatch(1); + + // 프로듀서 스레드 생성 + for (int i = 0; i < nThreads; i++) { + producerService.submit(() -> { + try { + for(int j=0; j { + try { + consumer.consumeEvent(timeout, unit); + consumerLatch.countDown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + consumerLatch.await(); + System.out.println("컨슈머 작업 끝남"); + producerLatch.await(); + System.out.println("프로듀서 작업 끝남"); + + Long finalCount = consumer.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); + Assertions.assertEquals(initalCount + nAddsPerThread*nThreads*valueToAdd, finalCount); + } + @Test + @DisplayName("멀티 프로듀서 멀티 컨슈머") + public void 멀티_프로듀서_멀티_컨슈머() throws InterruptedException { + BlockingQueue queue = new LinkedBlockingQueue<>(1000); + CounterConsumer consumer = new CounterConsumer(queue); + CounterProducer producer = new CounterProducer(queue); + LocalTime lt1 = LocalTime.now(); + Long initalCount = consumer.show(); + ExecutorService producerService = Executors.newFixedThreadPool(nThreads); + ExecutorService consumerService = Executors.newFixedThreadPool(nThreads); + CountDownLatch producerLatch = new CountDownLatch(nThreads); + CountDownLatch consumerLatch = new CountDownLatch(nThreads); + + // 프로듀서 스레드 생성 + for (int i = 0; i < nThreads; i++) { + producerService.submit(() -> { + try { + for(int j=0; j { + try { + consumer.consumeEvent(timeout, unit); + consumerLatch.countDown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + consumerLatch.await(); + System.out.println("컨슈머 작업 끝남"); + producerLatch.await(); + System.out.println("프로듀서 작업 끝남"); + + Long finalCount = consumer.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); + Assertions.assertEquals(initalCount + nAddsPerThread*nThreads*valueToAdd, finalCount); + } +} From b286a26ebc524756649d1596b008b40256b6b601 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 17:32:09 +0900 Subject: [PATCH 41/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 프로듀서 컨슈머 패턴 테스트 정리 --- .../com/thread/concurrency/CounterTest.java | 57 +++++++++ .../{ => async}/AsyncControllerTest.java | 2 +- .../{ => async}/AsyncServiceTest.java | 2 +- ...est.java => BlockingQueueCounterTest.java} | 5 +- .../counter/SynchronizedCounterTest.java | 2 - .../queueCounter/QueueCounterTest.java | 116 ++++++++++++++++++ 6 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/thread/concurrency/CounterTest.java rename src/test/java/com/thread/concurrency/{ => async}/AsyncControllerTest.java (96%) rename src/test/java/com/thread/concurrency/{ => async}/AsyncServiceTest.java (98%) rename src/test/java/com/thread/concurrency/counter/{QueueCounterTest.java => BlockingQueueCounterTest.java} (95%) create mode 100644 src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java diff --git a/src/test/java/com/thread/concurrency/CounterTest.java b/src/test/java/com/thread/concurrency/CounterTest.java new file mode 100644 index 0000000..bb35d59 --- /dev/null +++ b/src/test/java/com/thread/concurrency/CounterTest.java @@ -0,0 +1,57 @@ +package com.thread.concurrency; + +import com.thread.concurrency.counter.AtomicCounter; +import com.thread.concurrency.counter.CompletableFutureCounter; +import com.thread.concurrency.counter.Counter; +import com.thread.concurrency.counter.SynchronizedCounter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Stream; + +@SpringBootTest +public class CounterTest { + private final int valueToAdd = 1; + private final int nAddsPerThread = 50000000; + private final int nThreads = 9; + + public static Stream counterProvider() { + return Stream.of(new AtomicCounter(), new CompletableFutureCounter(), new SynchronizedCounter()); + } + + @ParameterizedTest + @MethodSource("counterProvider") + @DisplayName("스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") + public void 여러_더하기_수행_Executor(Counter counter) throws InterruptedException { + LocalTime lt1 = LocalTime.now(); + int initalCount = counter.show(); + + ExecutorService service = Executors.newFixedThreadPool(nThreads); + CountDownLatch latch = new CountDownLatch(nThreads); + for (int i = 0; i < nThreads; i++) { + service.submit(() -> { + for(int j=0; j { try { for(int j=0; j<(totalCount/nThread); j++){ -// System.out.println(Thread.currentThread().getName()+"은 작업 추가 "+ finalI +" "+j); producer.add(counteNumber); } } catch (InterruptedException e) { diff --git a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java index 9630e2f..31137e0 100644 --- a/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/SynchronizedCounterTest.java @@ -1,6 +1,5 @@ package com.thread.concurrency.counter; -import com.thread.concurrency.AsyncServiceTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,7 +11,6 @@ import java.time.Duration; import java.time.LocalTime; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.*; diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java new file mode 100644 index 0000000..89ab1d0 --- /dev/null +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -0,0 +1,116 @@ +package com.thread.concurrency.queueCounter; + + +import com.thread.concurrency.counter.producerCustomer.CounterConsumer; +import com.thread.concurrency.counter.producerCustomer.CounterProducer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import java.time.Duration; +import java.time.LocalTime; +import java.util.concurrent.*; + +public class QueueCounterTest { + private final int valueToAdd = 1; + private final int nAddsPerThread = 50000000; + private final int nThreads = 9; + private final long timeout = 1; + private final int queueCapacity = 1000; + private final TimeUnit unit = TimeUnit.SECONDS; + + @Test + @DisplayName("멀티 프로듀서 싱글 컨슈머") + public void 멀티_프로듀서_싱글_컨슈머() throws InterruptedException { + BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); + CounterConsumer consumer = new CounterConsumer(queue); + CounterProducer producer = new CounterProducer(queue); + LocalTime lt1 = LocalTime.now(); + Long initalCount = consumer.show(); + ExecutorService producerService = Executors.newFixedThreadPool(nThreads); + ExecutorService consumerService = Executors.newFixedThreadPool(nThreads); + CountDownLatch producerLatch = new CountDownLatch(nThreads); + CountDownLatch consumerLatch = new CountDownLatch(1); + + // 프로듀서 스레드 생성 + for (int i = 0; i < nThreads; i++) { + producerService.submit(() -> { + try { + for(int j=0; j { + try { + consumer.consumeEvent(timeout, unit); + consumerLatch.countDown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + consumerLatch.await(); + System.out.println("컨슈머 작업 끝남"); + producerLatch.await(); + System.out.println("프로듀서 작업 끝남"); + + Long finalCount = consumer.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); + Assertions.assertEquals(initalCount + nAddsPerThread*nThreads*valueToAdd, finalCount); + } + @Test + @DisplayName("멀티 프로듀서 멀티 컨슈머") + public void 멀티_프로듀서_멀티_컨슈머() throws InterruptedException { + BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); + CounterConsumer consumer = new CounterConsumer(queue); + CounterProducer producer = new CounterProducer(queue); + LocalTime lt1 = LocalTime.now(); + Long initalCount = consumer.show(); + ExecutorService producerService = Executors.newFixedThreadPool(nThreads); + ExecutorService consumerService = Executors.newFixedThreadPool(nThreads); + CountDownLatch producerLatch = new CountDownLatch(nThreads); + CountDownLatch consumerLatch = new CountDownLatch(nThreads); + + // 프로듀서 스레드 생성 + for (int i = 0; i < nThreads; i++) { + producerService.submit(() -> { + try { + for(int j=0; j { + try { + consumer.consumeEvent(timeout, unit); + consumerLatch.countDown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + consumerLatch.await(); + System.out.println("컨슈머 작업 끝남"); + producerLatch.await(); + System.out.println("프로듀서 작업 끝남"); + + Long finalCount = consumer.show(); + LocalTime lt2 = LocalTime.now(); + long dif = Duration.between(lt1, lt2).getNano(); + System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); + Assertions.assertEquals(initalCount + nAddsPerThread*nThreads*valueToAdd, finalCount); + } +} From eb1c7117bd7e45530b90b9cb9deea969f8cdeb99 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 17:36:19 +0900 Subject: [PATCH 42/62] =?UTF-8?q?refacto/=EC=8A=A4=EB=A0=88=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=20=EC=A0=95=EB=8F=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../queueCounter/QueueCounterTest.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index 89ab1d0..ec66456 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -13,7 +13,8 @@ public class QueueCounterTest { private final int valueToAdd = 1; private final int nAddsPerThread = 50000000; - private final int nThreads = 9; + private final int producerNThreads = 9; + private final int consumerNThreads = 9; private final long timeout = 1; private final int queueCapacity = 1000; private final TimeUnit unit = TimeUnit.SECONDS; @@ -26,13 +27,13 @@ public class QueueCounterTest { CounterProducer producer = new CounterProducer(queue); LocalTime lt1 = LocalTime.now(); Long initalCount = consumer.show(); - ExecutorService producerService = Executors.newFixedThreadPool(nThreads); - ExecutorService consumerService = Executors.newFixedThreadPool(nThreads); - CountDownLatch producerLatch = new CountDownLatch(nThreads); + ExecutorService producerService = Executors.newFixedThreadPool(producerNThreads); + ExecutorService consumerService = Executors.newFixedThreadPool(consumerNThreads); + CountDownLatch producerLatch = new CountDownLatch(producerNThreads); CountDownLatch consumerLatch = new CountDownLatch(1); // 프로듀서 스레드 생성 - for (int i = 0; i < nThreads; i++) { + for (int i = 0; i < producerNThreads; i++) { producerService.submit(() -> { try { for(int j=0; j { try { for(int j=0; j { try { consumer.consumeEvent(timeout, unit); @@ -111,6 +112,6 @@ public class QueueCounterTest { LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); - Assertions.assertEquals(initalCount + nAddsPerThread*nThreads*valueToAdd, finalCount); + Assertions.assertEquals(initalCount + nAddsPerThread*producerNThreads*valueToAdd, finalCount); } } From 86554d681492e7d61d67437e9bd290e3e96de799 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 17:41:14 +0900 Subject: [PATCH 43/62] =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=EA=B3=BC=20=EB=B3=91=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/thread/concurrency/queueCounter/QueueCounterTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index e45ade9..4362f53 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -64,11 +64,7 @@ public class QueueCounterTest { LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); -<<<<<<< HEAD Assertions.assertEquals(initalCount + nAddsPerThread*producerNThreads*valueToAdd, finalCount); -======= - Assertions.assertEquals(initalCount + nAddsPerThread*nThreads*valueToAdd, finalCount); ->>>>>>> integrated_develop } @Test @DisplayName("멀티 프로듀서 멀티 컨슈머") From 55a797d563deba763df580dfe9747f02a46a7202 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 17:46:55 +0900 Subject: [PATCH 44/62] =?UTF-8?q?sol=20=EC=BB=A4=EB=B0=8B=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 - src/test/java/com/thread/concurrency/package-info.java | 1 - .../thread/concurrency/queueCounter/QueueCounterTest.java | 6 ++---- 3 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 src/test/java/com/thread/concurrency/package-info.java diff --git a/build.gradle.kts b/build.gradle.kts index 85502fe..dfe6a2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,6 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter") - implementation("net.jcip:jcip-annotations:1.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.reactivestreams:reactive-streams") diff --git a/src/test/java/com/thread/concurrency/package-info.java b/src/test/java/com/thread/concurrency/package-info.java deleted file mode 100644 index ae29573..0000000 --- a/src/test/java/com/thread/concurrency/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.thread.concurrency; diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index 4362f53..12a5593 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -63,7 +63,7 @@ public class QueueCounterTest { Long finalCount = consumer.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); + System.out.println("멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); Assertions.assertEquals(initalCount + nAddsPerThread*producerNThreads*valueToAdd, finalCount); } @Test @@ -104,14 +104,12 @@ public class QueueCounterTest { }); } consumerLatch.await(); - System.out.println("컨슈머 작업 끝남"); producerLatch.await(); - System.out.println("프로듀서 작업 끝남"); Long finalCount = consumer.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - System.out.println("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); + System.out.println("멀티_프로듀서_멀티_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); Assertions.assertEquals(initalCount + nAddsPerThread*producerNThreads*valueToAdd, finalCount); } } From 84da0f49b92051ea71c54c54d46b42b75940db70 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 17:49:39 +0900 Subject: [PATCH 45/62] =?UTF-8?q?queueCounter=EB=8A=94=20counter=EC=99=80?= =?UTF-8?q?=20=EB=8B=A4=EB=A5=B8=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EA=B3=B5=EC=9C=A0=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EB=95=8C=EB=AC=B8=EC=97=90=20counter=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../counter/BlockingQueueCounterTest.java | 121 ------------------ 1 file changed, 121 deletions(-) delete mode 100644 src/test/java/com/thread/concurrency/counter/BlockingQueueCounterTest.java diff --git a/src/test/java/com/thread/concurrency/counter/BlockingQueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/BlockingQueueCounterTest.java deleted file mode 100644 index fdb61c9..0000000 --- a/src/test/java/com/thread/concurrency/counter/BlockingQueueCounterTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.thread.concurrency.counter; - -import com.thread.concurrency.counter.producerCustomer.CounterConsumer; -import com.thread.concurrency.counter.producerCustomer.CounterProducer; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.*; - -public class BlockingQueueCounterTest { - private final int counteNumber = 1; - private final int totalCount = Integer.MAX_VALUE; - private final int nThread = 15; - private static final Logger logger = LoggerFactory.getLogger(SynchronizedCounterTest.class); - - @Test - @DisplayName("멀티 프로듀서 싱글 컨슈머") - public void 멀티_프로듀서_싱글_컨슈머() throws InterruptedException { - BlockingQueue queue = new LinkedBlockingQueue<>(1000); - CounterConsumer consumer = new CounterConsumer(queue); - CounterProducer producer = new CounterProducer(queue); - LocalTime lt1 = LocalTime.now(); - Long initalCount = consumer.show(); - ExecutorService service = Executors.newFixedThreadPool(nThread); - CountDownLatch latch = new CountDownLatch(nThread); - - // 프로듀서 스레드 생성 - for (int i = 0; i < nThread; i++) { - service.submit(() -> { - try { - for(int j=0; j<(totalCount/nThread); j++){ - producer.add(counteNumber); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - latch.countDown(); - }); - } - // CounterCustomer 스레드 생성 및 비동기로 처리 시작 - CompletableFuture task = CompletableFuture.runAsync(()->{ - try{ - consumer.consumeEvent(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }); - try { - task.get(); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - System.out.println("컨슈머 작업 끝남"); - latch.await(); - System.out.println("프로듀서 작업 끝남"); - - Long finalCount = consumer.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); - Assertions.assertEquals(initalCount + (totalCount/nThread)*nThread*counteNumber, finalCount); - } - @Test - @DisplayName("멀티 프로듀서 멀티 컨슈머") - public void 멀티_프로듀서_멀티_컨슈머() throws InterruptedException { - BlockingQueue queue = new LinkedBlockingQueue<>(1000); - CounterConsumer consumer = new CounterConsumer(queue); - CounterProducer producer = new CounterProducer(queue); - LocalTime lt1 = LocalTime.now(); - Long initalCount = consumer.show(); - ExecutorService service = Executors.newFixedThreadPool(nThread); - CountDownLatch latch = new CountDownLatch(nThread); - - for (int i = 0; i < nThread; i++) { - service.submit(() -> { - try { - for(int j=0; j<(totalCount/nThread); j++){ - producer.add(counteNumber); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - latch.countDown(); - }); - } - // CounterCustomer 스레드 생성 및 비동기로 처리 시작 - List> tasks = new ArrayList<>(); - for(int i=0; i<3; i++){ - tasks.add( CompletableFuture.runAsync(()->{ - try{ - consumer.consumeEvent(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - })); - } - tasks.forEach((t) -> { - try { -// System.out.println("적당히 돌아가는거 확인합닛다."); - t.get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - }); - System.out.println("컨슈머 작업 끝남"); - latch.await(); - System.out.println("프로듀서 작업 끝남"); - Long finalCount = consumer.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - logger.info("프로듀서_컨슈며_더하기_멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); - Assertions.assertEquals(initalCount + (totalCount/nThread)*nThread*counteNumber, finalCount); - } -} From e783deb54156277e5dbfa08c4e3fadecd42155e9 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 17:55:06 +0900 Subject: [PATCH 46/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=E2=9C=85=20?= =?UTF-8?q?Test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 프로듀서 컨슈머의 인터페이스 작성 2. 인터페이스를 이용해 테스트 리팩토링 --- .../producerCustomer/CounterConsumer.java | 39 ------------------- .../producerCustomer/CounterProducer.java | 16 -------- .../counter/queueCounter/Consumer.java | 8 ++++ .../counter/queueCounter/CounterConsumer.java | 5 +-- .../counter/queueCounter/CounterProducer.java | 2 +- .../counter/queueCounter/Producer.java | 5 +++ .../queueCounter/QueueCounterTest.java | 10 +++-- 7 files changed, 21 insertions(+), 64 deletions(-) delete mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java delete mode 100644 src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java create mode 100644 src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java create mode 100644 src/main/java/com/thread/concurrency/counter/queueCounter/Producer.java diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java deleted file mode 100644 index 61f7c5c..0000000 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterConsumer.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.thread.concurrency.counter.producerCustomer; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; -import java.util.function.IntUnaryOperator; -import java.util.function.LongUnaryOperator; - -public class CounterConsumer { - private final BlockingQueue queue; - private final AtomicLong count = new AtomicLong(0); - - public CounterConsumer(BlockingQueue queue) { - this.queue = queue; - } - - public void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException { - while (true) { -// System.out.println(Thread.currentThread().getName()+"은 현재 큐 사이즈 : "+queue.size()); - Long value = queue.poll(timeout, unit); - if(value == null){ -// System.out.println(Thread.currentThread().getName()+" 끝났으!!"); - break; - } - count.addAndGet(value); -// System.out.println("결s과 카운트 : "+count.addAndGet(value)); - } - } - public Long show(){ - while(true){ - if(queue.isEmpty()){ - return count.get(); - } - } - } -} diff --git a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java deleted file mode 100644 index 74df1d7..0000000 --- a/src/main/java/com/thread/concurrency/counter/producerCustomer/CounterProducer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.thread.concurrency.counter.producerCustomer; - -import java.util.concurrent.BlockingQueue; -import java.util.function.Function; - -public class CounterProducer { - private final BlockingQueue queue; - - public CounterProducer(BlockingQueue queue) { - this.queue = queue; - } - - public void add(long value) throws InterruptedException { - queue.put(value); - } -} diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java new file mode 100644 index 0000000..11930df --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java @@ -0,0 +1,8 @@ +package com.thread.concurrency.counter.queueCounter; + +import java.util.concurrent.TimeUnit; + +public interface Consumer { + void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException; + Long show(); +} diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java index c12edf1..964d86c 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java @@ -4,7 +4,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -public class CounterConsumer { +public class CounterConsumer implements Consumer { private final BlockingQueue queue; private final AtomicLong count = new AtomicLong(0); @@ -14,14 +14,11 @@ public CounterConsumer(BlockingQueue queue) { public void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException { while (true) { -// System.out.println(Thread.currentThread().getName()+"은 현재 큐 사이즈 : "+queue.size()); Long value = queue.poll(timeout, unit); if(value == null){ -// System.out.println(Thread.currentThread().getName()+" 끝났으!!"); break; } count.addAndGet(value); -// System.out.println("결s과 카운트 : "+count.addAndGet(value)); } } public Long show(){ diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java index 126e608..24afef8 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java @@ -2,7 +2,7 @@ import java.util.concurrent.BlockingQueue; -public class CounterProducer { +public class CounterProducer implements Producer{ private final BlockingQueue queue; public CounterProducer(BlockingQueue queue) { diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/Producer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/Producer.java new file mode 100644 index 0000000..71d943d --- /dev/null +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/Producer.java @@ -0,0 +1,5 @@ +package com.thread.concurrency.counter.queueCounter; + +public interface Producer { + void add(long value) throws InterruptedException; +} diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index 12a5593..8180d7d 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -1,7 +1,9 @@ package com.thread.concurrency.queueCounter; +import com.thread.concurrency.counter.queueCounter.Consumer; import com.thread.concurrency.counter.queueCounter.CounterConsumer; import com.thread.concurrency.counter.queueCounter.CounterProducer; +import com.thread.concurrency.counter.queueCounter.Producer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,8 +25,8 @@ public class QueueCounterTest { @DisplayName("멀티 프로듀서 싱글 컨슈머") public void 멀티_프로듀서_싱글_컨슈머() throws InterruptedException { BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); - CounterConsumer consumer = new CounterConsumer(queue); - CounterProducer producer = new CounterProducer(queue); + Consumer consumer = new CounterConsumer(queue); + Producer producer = new CounterProducer(queue); LocalTime lt1 = LocalTime.now(); Long initalCount = consumer.show(); ExecutorService producerService = Executors.newFixedThreadPool(producerNThreads); @@ -70,8 +72,8 @@ public class QueueCounterTest { @DisplayName("멀티 프로듀서 멀티 컨슈머") public void 멀티_프로듀서_멀티_컨슈머() throws InterruptedException { BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); - CounterConsumer consumer = new CounterConsumer(queue); - CounterProducer producer = new CounterProducer(queue); + Consumer consumer = new CounterConsumer(queue); + Producer producer = new CounterProducer(queue); LocalTime lt1 = LocalTime.now(); Long initalCount = consumer.show(); ExecutorService producerService = Executors.newFixedThreadPool(producerNThreads); From 75b0f90dbeec99f17d2f0cbe9fde5e4f5f182652 Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 18:01:26 +0900 Subject: [PATCH 47/62] =?UTF-8?q?=EC=A1=B4=EC=9E=AC=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EA=B5=AC=ED=98=84=EC=A0=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concurrency/counter/BasicCounterTest.java | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 src/test/java/com/thread/concurrency/counter/BasicCounterTest.java diff --git a/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java b/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java deleted file mode 100644 index ffc7e7f..0000000 --- a/src/test/java/com/thread/concurrency/counter/BasicCounterTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.thread.concurrency.counter; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import java.util.concurrent.CompletableFuture; - -@SpringBootTest -public class BasicCounterTest { - - private final BasicCounter basicCounter; - private final int counteNumber = 1; - private final int totalCount = 100; - - @Autowired - public BasicCounterTest(BasicCounter basicCounter) { - this.basicCounter = basicCounter; - } - - @Test - @DisplayName("스레드 안전하지 않는 카운터로 동시에 여러 더하기 수행하기. 실패 예상") - public void 여러_더하기_수행(){ - int initalCount = basicCounter.show(); - - for(int i=0; i { - basicCounter.add(counteNumber); - }); - } - int finalCount = basicCounter.show(); - Assertions.assertNotEquals(initalCount+totalCount*counteNumber, finalCount); - } -} From e962db2cd0b3652bb232c2a9dde9f54c15765dbf Mon Sep 17 00:00:00 2001 From: ohchansol Date: Sun, 7 Apr 2024 18:05:08 +0900 Subject: [PATCH 48/62] =?UTF-8?q?=ED=95=84=EC=9A=94=20=EC=97=86=EB=8A=94?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/thread/concurrency/queueCounter/QueueCounterTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index 8180d7d..56eb89d 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -58,9 +58,7 @@ public class QueueCounterTest { } }); consumerLatch.await(); - System.out.println("컨슈머 작업 끝남"); producerLatch.await(); - System.out.println("프로듀서 작업 끝남"); Long finalCount = consumer.show(); LocalTime lt2 = LocalTime.now(); From 3b3f5cd1659d451c0361e066186669b5445d6d3d Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Mon, 8 Apr 2024 00:06:30 +0900 Subject: [PATCH 49/62] =?UTF-8?q?=F0=9F=8E=A8=20Style=20:=20polishing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../SpringThreadConcurrencyApplication.java | 4 ++-- .../async/controller/AsyncController.java | 6 +++--- .../concurrency/async/service/AsyncService.java | 4 ++-- .../counter/queueCounter/Consumer.java | 1 + .../counter/queueCounter/CounterConsumer.java | 9 +++++---- .../counter/queueCounter/CounterProducer.java | 2 +- .../java/com/thread/concurrency/CounterTest.java | 13 ++++++++----- .../concurrency/async/AsyncControllerTest.java | 2 +- .../concurrency/async/AsyncServiceTest.java | 16 +++++++++------- .../queueCounter/QueueCounterTest.java | 9 +++++---- 10 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java index 1b02bec..87ec2a2 100644 --- a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java +++ b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java @@ -8,14 +8,13 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import java.util.concurrent.Executor; - import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; @@ -83,6 +82,7 @@ private static List range(int numberOfThreads, int expected) { } return result; } + @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // Spring에서 사용하는 스레드를 제어한느 설정 diff --git a/src/main/java/com/thread/concurrency/async/controller/AsyncController.java b/src/main/java/com/thread/concurrency/async/controller/AsyncController.java index 6be4cdf..e72346d 100644 --- a/src/main/java/com/thread/concurrency/async/controller/AsyncController.java +++ b/src/main/java/com/thread/concurrency/async/controller/AsyncController.java @@ -22,14 +22,14 @@ public AsyncController(AsyncService asyncService) { public CompletableFuture calculateRunTime(int cnt, int waitTime) throws InterruptedException { LocalTime lt1 = LocalTime.now(); List> hellos = new ArrayList<>(); - for(int i=0; i results = hellos.stream().map(CompletableFuture::join) .toList(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).toMillis(); - return CompletableFuture.completedFuture(dif+"가 걸렸습니다."); + return CompletableFuture.completedFuture(dif + "가 걸렸습니다."); } } diff --git a/src/main/java/com/thread/concurrency/async/service/AsyncService.java b/src/main/java/com/thread/concurrency/async/service/AsyncService.java index 9cb2a00..aa3f261 100644 --- a/src/main/java/com/thread/concurrency/async/service/AsyncService.java +++ b/src/main/java/com/thread/concurrency/async/service/AsyncService.java @@ -9,8 +9,8 @@ @Service public class AsyncService { @Async - public CompletableFuture voidParamStringReturn (long waitTime, String message) throws InterruptedException{ - System.out.println("비동기적으로 실행 - "+ + public CompletableFuture voidParamStringReturn(long waitTime, String message) throws InterruptedException { + System.out.println("비동기적으로 실행 - " + Thread.currentThread().getName()); Thread.sleep(waitTime); return CompletableFuture.completedFuture(message); diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java index 11930df..3a2fada 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java @@ -4,5 +4,6 @@ public interface Consumer { void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException; + Long show(); } diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java index 964d86c..4fa4c3b 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java @@ -15,15 +15,16 @@ public CounterConsumer(BlockingQueue queue) { public void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException { while (true) { Long value = queue.poll(timeout, unit); - if(value == null){ + if (value == null) { break; } count.addAndGet(value); } } - public Long show(){ - while(true){ - if(queue.isEmpty()){ + + public Long show() { + while (true) { + if (queue.isEmpty()) { return count.get(); } } diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java index 24afef8..eea7bcc 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java +++ b/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java @@ -2,7 +2,7 @@ import java.util.concurrent.BlockingQueue; -public class CounterProducer implements Producer{ +public class CounterProducer implements Producer { private final BlockingQueue queue; public CounterProducer(BlockingQueue queue) { diff --git a/src/test/java/com/thread/concurrency/CounterTest.java b/src/test/java/com/thread/concurrency/CounterTest.java index d88727d..831ce57 100644 --- a/src/test/java/com/thread/concurrency/CounterTest.java +++ b/src/test/java/com/thread/concurrency/CounterTest.java @@ -1,6 +1,9 @@ package com.thread.concurrency; -import com.thread.concurrency.counter.*; +import com.thread.concurrency.counter.AtomicCounter; +import com.thread.concurrency.counter.CompletableFutureCounter; +import com.thread.concurrency.counter.Counter; +import com.thread.concurrency.counter.SynchronizedCounter; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -35,7 +38,7 @@ public static Stream counterProvider() { CountDownLatch latch = new CountDownLatch(nThreads); for (int i = 0; i < nThreads; i++) { service.submit(() -> { - for(int j=0; j counterProvider() { int finalCount = counter.show(); LocalTime lt2 = LocalTime.now(); long dif = Duration.between(lt1, lt2).getNano(); - System.out.println("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float)dif / 1000000) + "ms"); + System.out.println("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float) dif / 1000000) + "ms"); Runtime.getRuntime().gc(); long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - System.out.println("메모리 사용량 "+(double)usedMemory/1048576 + " MB"); - Assertions.assertEquals(initalCount + nThreads*nAddsPerThread*valueToAdd, finalCount); + System.out.println("메모리 사용량 " + (double) usedMemory / 1048576 + " MB"); + Assertions.assertEquals(initalCount + nThreads * nAddsPerThread * valueToAdd, finalCount); } } diff --git a/src/test/java/com/thread/concurrency/async/AsyncControllerTest.java b/src/test/java/com/thread/concurrency/async/AsyncControllerTest.java index d501937..36209fe 100644 --- a/src/test/java/com/thread/concurrency/async/AsyncControllerTest.java +++ b/src/test/java/com/thread/concurrency/async/AsyncControllerTest.java @@ -21,7 +21,7 @@ public class AsyncControllerTest { @Test public void invokeMultiAsyncMethod() throws InterruptedException { List> hellos = new ArrayList<>(); - for(int i=0; i<10; i++){ + for (int i = 0; i < 10; i++) { hellos.add(asyncController.calculateRunTime(10, 1000)); } // 모든 비동기 호출이 완료될 때까지 대기하고 결과를 리스트에 넣기 diff --git a/src/test/java/com/thread/concurrency/async/AsyncServiceTest.java b/src/test/java/com/thread/concurrency/async/AsyncServiceTest.java index b4ea59c..4519feb 100644 --- a/src/test/java/com/thread/concurrency/async/AsyncServiceTest.java +++ b/src/test/java/com/thread/concurrency/async/AsyncServiceTest.java @@ -1,7 +1,9 @@ package com.thread.concurrency.async; import com.thread.concurrency.async.service.AsyncService; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -22,20 +24,20 @@ public class AsyncServiceTest { @Test @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출") public void testGetString() throws ExecutionException, InterruptedException { - CompletableFuture helloWorld = asyncService.voidParamStringReturn(1000, "기본 메세지"); - Assertions.assertEquals("기본 메세지",helloWorld.get()); + CompletableFuture helloWorld = asyncService.voidParamStringReturn(1000, "기본 메세지"); + Assertions.assertEquals("기본 메세지", helloWorld.get()); } @Test @DisplayName("입력은 void 출력은 String인 비동기 함수 다중 호출") public void testGetMultiString() throws InterruptedException { List> hellos = new ArrayList<>(); - for(int i=0; i<100; i++){ - hellos.add(asyncService.voidParamStringReturn(1000,i+"번째 메세지")); + for (int i = 0; i < 100; i++) { + hellos.add(asyncService.voidParamStringReturn(1000, i + "번째 메세지")); } // 모든 비동기 호출이 완료될 때까지 대기하고 결과를 리스트에 넣기 List results = hellos.stream().map(CompletableFuture::join) - .toList(); + .toList(); results.forEach(logger::info); } @@ -47,6 +49,6 @@ public void testGetStringTimeOutIsThisAsync() throws InterruptedException { long timeOutValue = 1; TimeUnit timeUnit = TimeUnit.SECONDS; // 1초가 지난 후 타임아웃 발생 - Assertions.assertThrows(ExecutionException.class, () -> completableFuture.orTimeout(timeOutValue,timeUnit).get()); + Assertions.assertThrows(ExecutionException.class, () -> completableFuture.orTimeout(timeOutValue, timeUnit).get()); } } diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index 56eb89d..9062b2a 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -38,7 +38,7 @@ public class QueueCounterTest { for (int i = 0; i < producerNThreads; i++) { producerService.submit(() -> { try { - for(int j=0; j { try { - for(int j=0; j Date: Mon, 8 Apr 2024 10:14:05 +0900 Subject: [PATCH 50/62] =?UTF-8?q?=F0=9F=A4=96=20Refactor=20:=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../com/thread/concurrency/CounterTest.java | 57 -------- .../queueCounter/QueueCounterTest.java | 124 ++++++++---------- 2 files changed, 53 insertions(+), 128 deletions(-) delete mode 100644 src/test/java/com/thread/concurrency/CounterTest.java diff --git a/src/test/java/com/thread/concurrency/CounterTest.java b/src/test/java/com/thread/concurrency/CounterTest.java deleted file mode 100644 index 831ce57..0000000 --- a/src/test/java/com/thread/concurrency/CounterTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.thread.concurrency; - -import com.thread.concurrency.counter.AtomicCounter; -import com.thread.concurrency.counter.CompletableFutureCounter; -import com.thread.concurrency.counter.Counter; -import com.thread.concurrency.counter.SynchronizedCounter; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Stream; - -@SpringBootTest -public class CounterTest { - private final int valueToAdd = 1; - private final int nAddsPerThread = 50000000; - private final int nThreads = 9; - - public static Stream counterProvider() { - return Stream.of(new AtomicCounter(), new CompletableFutureCounter(), new SynchronizedCounter()); - } - - @ParameterizedTest - @MethodSource("counterProvider") - @DisplayName("스레드 안전한 카운터로 동시에 여러 더하기 수행하기.") - public void 여러_더하기_수행_Executor(Counter counter) throws InterruptedException { - LocalTime lt1 = LocalTime.now(); - int initalCount = counter.show(); - - ExecutorService service = Executors.newFixedThreadPool(nThreads); - CountDownLatch latch = new CountDownLatch(nThreads); - for (int i = 0; i < nThreads; i++) { - service.submit(() -> { - for (int j = 0; j < nAddsPerThread; j++) { - counter.add(valueToAdd); - } - latch.countDown(); - }); - } - latch.await(); - int finalCount = counter.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - System.out.println("여러_더하기_수행_Executor 테스트가 걸린 시간 : " + ((float) dif / 1000000) + "ms"); - Runtime.getRuntime().gc(); - long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - System.out.println("메모리 사용량 " + (double) usedMemory / 1048576 + " MB"); - Assertions.assertEquals(initalCount + nThreads * nAddsPerThread * valueToAdd, finalCount); - } -} diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index 9062b2a..e1a0e31 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -4,83 +4,71 @@ import com.thread.concurrency.counter.queueCounter.CounterConsumer; import com.thread.concurrency.counter.queueCounter.CounterProducer; import com.thread.concurrency.counter.queueCounter.Producer; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import java.time.Duration; -import java.time.LocalTime; import java.util.concurrent.*; public class QueueCounterTest { - private final int valueToAdd = 1; - private final int nAddsPerThread = 50000000; - private final int producerNThreads = 9; - private final int consumerNThreads = 9; - private final long timeout = 1; - private final int queueCapacity = 1000; - private final TimeUnit unit = TimeUnit.SECONDS; + private static final int valueToAdd = 1; + private static final int nAddsPerThread = 1000000; + private static final int producerNThreads = 9; + private static final int consumerNThreads = 9; + private Consumer consumer; + private Producer producer; + private ExecutorService consumerService; + private ExecutorService producerService; + + @BeforeEach + public void setup() { + int queueCapacity = 1000; + BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); + consumer = new CounterConsumer(queue); + producer = new CounterProducer(queue); + producerService = Executors.newFixedThreadPool(producerNThreads); + consumerService = Executors.newFixedThreadPool(consumerNThreads); + } + + @AfterEach + public void cleanup() { + producerService.shutdown(); + consumerService.shutdown(); + } @Test + @SuppressWarnings("SpellCheckingInspection") @DisplayName("멀티 프로듀서 싱글 컨슈머") - public void 멀티_프로듀서_싱글_컨슈머() throws InterruptedException { - BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); - Consumer consumer = new CounterConsumer(queue); - Producer producer = new CounterProducer(queue); - LocalTime lt1 = LocalTime.now(); - Long initalCount = consumer.show(); - ExecutorService producerService = Executors.newFixedThreadPool(producerNThreads); - ExecutorService consumerService = Executors.newFixedThreadPool(consumerNThreads); + public void multiProducerSingleConsumer() { + Assertions.assertTimeout(Duration.ofSeconds(10), () -> { + runTest(1); + }); + } + + @Test + @SuppressWarnings("SpellCheckingInspection") + @DisplayName("멀티 프로듀서 멀티 컨슈머") + public void multiProducerMultiConsumer() { + Assertions.assertTimeout(Duration.ofSeconds(10), () -> { + runTest(consumerNThreads); + }); + } + + private void runTest(int consumerCount) throws InterruptedException { + Long initialCount = consumer.show(); CountDownLatch producerLatch = new CountDownLatch(producerNThreads); - CountDownLatch consumerLatch = new CountDownLatch(1); + CountDownLatch consumerLatch = new CountDownLatch(consumerCount); - // 프로듀서 스레드 생성 - for (int i = 0; i < producerNThreads; i++) { - producerService.submit(() -> { - try { - for (int j = 0; j < nAddsPerThread; j++) { - producer.add(valueToAdd); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - producerLatch.countDown(); - }); - } + createProducerThreads(producerLatch); + createConsumerThreads(consumerLatch, consumerCount); - // 컨슈머 스레드 생성 - consumerService.submit(() -> { - try { - consumer.consumeEvent(timeout, unit); - consumerLatch.countDown(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }); consumerLatch.await(); producerLatch.await(); Long finalCount = consumer.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - System.out.println("멀티_프로듀서_단일_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); - Assertions.assertEquals(initalCount + nAddsPerThread * producerNThreads * valueToAdd, finalCount); + Assertions.assertEquals(initialCount + nAddsPerThread * producerNThreads * valueToAdd, finalCount); } - @Test - @DisplayName("멀티 프로듀서 멀티 컨슈머") - public void 멀티_프로듀서_멀티_컨슈머() throws InterruptedException { - BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); - Consumer consumer = new CounterConsumer(queue); - Producer producer = new CounterProducer(queue); - LocalTime lt1 = LocalTime.now(); - Long initalCount = consumer.show(); - ExecutorService producerService = Executors.newFixedThreadPool(producerNThreads); - ExecutorService consumerService = Executors.newFixedThreadPool(consumerNThreads); - CountDownLatch producerLatch = new CountDownLatch(producerNThreads); - CountDownLatch consumerLatch = new CountDownLatch(consumerNThreads); - - // 프로듀서 스레드 생성 + private void createProducerThreads(CountDownLatch producerLatch) { for (int i = 0; i < producerNThreads; i++) { producerService.submit(() -> { try { @@ -93,24 +81,18 @@ public class QueueCounterTest { producerLatch.countDown(); }); } - // 컨슈머 스레드 생성 - for (int i = 0; i < consumerNThreads; i++) { + } + + private void createConsumerThreads(CountDownLatch consumerLatch, int consumerCount) { + for (int i = 0; i < consumerCount; i++) { consumerService.submit(() -> { try { - consumer.consumeEvent(timeout, unit); + consumer.consumeEvent(1, TimeUnit.SECONDS); consumerLatch.countDown(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } - consumerLatch.await(); - producerLatch.await(); - - Long finalCount = consumer.show(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).getNano(); - System.out.println("멀티_프로듀서_멀티_컨슈머 테스트가 걸린 시간 : " + dif / 1000000 + "ms"); - Assertions.assertEquals(initalCount + nAddsPerThread * producerNThreads * valueToAdd, finalCount); } } From ec445909e04af7926f44303f35abe4d89b829e4e Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Mon, 8 Apr 2024 10:17:07 +0900 Subject: [PATCH 51/62] =?UTF-8?q?=F0=9F=94=A5=20Remove=20:=20remove=20asyn?= =?UTF-8?q?c=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../async/controller/AsyncController.java | 35 ------------ .../async/service/AsyncService.java | 18 ------- .../async/AsyncControllerTest.java | 32 ----------- .../concurrency/async/AsyncServiceTest.java | 54 ------------------- 4 files changed, 139 deletions(-) delete mode 100644 src/main/java/com/thread/concurrency/async/controller/AsyncController.java delete mode 100644 src/main/java/com/thread/concurrency/async/service/AsyncService.java delete mode 100644 src/test/java/com/thread/concurrency/async/AsyncControllerTest.java delete mode 100644 src/test/java/com/thread/concurrency/async/AsyncServiceTest.java diff --git a/src/main/java/com/thread/concurrency/async/controller/AsyncController.java b/src/main/java/com/thread/concurrency/async/controller/AsyncController.java deleted file mode 100644 index e72346d..0000000 --- a/src/main/java/com/thread/concurrency/async/controller/AsyncController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.thread.concurrency.async.controller; - -import com.thread.concurrency.async.service.AsyncService; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Controller; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -@Controller -public class AsyncController { - private final AsyncService asyncService; - - public AsyncController(AsyncService asyncService) { - this.asyncService = asyncService; - } - - @Async - public CompletableFuture calculateRunTime(int cnt, int waitTime) throws InterruptedException { - LocalTime lt1 = LocalTime.now(); - List> hellos = new ArrayList<>(); - for (int i = 0; i < cnt; i++) { - hellos.add(asyncService.voidParamStringReturn(waitTime, i + "번째 메세지")); - } - // 모든 비동기 호출이 완료될 때까지 대기하고 결과를 리스트에 넣기 - List results = hellos.stream().map(CompletableFuture::join) - .toList(); - LocalTime lt2 = LocalTime.now(); - long dif = Duration.between(lt1, lt2).toMillis(); - return CompletableFuture.completedFuture(dif + "가 걸렸습니다."); - } -} diff --git a/src/main/java/com/thread/concurrency/async/service/AsyncService.java b/src/main/java/com/thread/concurrency/async/service/AsyncService.java deleted file mode 100644 index aa3f261..0000000 --- a/src/main/java/com/thread/concurrency/async/service/AsyncService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.thread.concurrency.async.service; - -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.util.concurrent.CompletableFuture; - - -@Service -public class AsyncService { - @Async - public CompletableFuture voidParamStringReturn(long waitTime, String message) throws InterruptedException { - System.out.println("비동기적으로 실행 - " + - Thread.currentThread().getName()); - Thread.sleep(waitTime); - return CompletableFuture.completedFuture(message); - } -} diff --git a/src/test/java/com/thread/concurrency/async/AsyncControllerTest.java b/src/test/java/com/thread/concurrency/async/AsyncControllerTest.java deleted file mode 100644 index 36209fe..0000000 --- a/src/test/java/com/thread/concurrency/async/AsyncControllerTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.thread.concurrency.async; - -import com.thread.concurrency.async.controller.AsyncController; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -@SpringBootTest -public class AsyncControllerTest { - private static final Logger logger = LoggerFactory.getLogger(AsyncServiceTest.class); - - @Autowired - private AsyncController asyncController; - - @Test - public void invokeMultiAsyncMethod() throws InterruptedException { - List> hellos = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - hellos.add(asyncController.calculateRunTime(10, 1000)); - } - // 모든 비동기 호출이 완료될 때까지 대기하고 결과를 리스트에 넣기 - List results = hellos.stream().map(CompletableFuture::join) - .toList(); - results.forEach(logger::info); - } -} diff --git a/src/test/java/com/thread/concurrency/async/AsyncServiceTest.java b/src/test/java/com/thread/concurrency/async/AsyncServiceTest.java deleted file mode 100644 index 4519feb..0000000 --- a/src/test/java/com/thread/concurrency/async/AsyncServiceTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.thread.concurrency.async; - -import com.thread.concurrency.async.service.AsyncService; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -@SpringBootTest -public class AsyncServiceTest { - private static final Logger logger = LoggerFactory.getLogger(AsyncServiceTest.class); - @Autowired - private AsyncService asyncService; - - @Test - @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출") - public void testGetString() throws ExecutionException, InterruptedException { - CompletableFuture helloWorld = asyncService.voidParamStringReturn(1000, "기본 메세지"); - Assertions.assertEquals("기본 메세지", helloWorld.get()); - } - - @Test - @DisplayName("입력은 void 출력은 String인 비동기 함수 다중 호출") - public void testGetMultiString() throws InterruptedException { - List> hellos = new ArrayList<>(); - for (int i = 0; i < 100; i++) { - hellos.add(asyncService.voidParamStringReturn(1000, i + "번째 메세지")); - } - // 모든 비동기 호출이 완료될 때까지 대기하고 결과를 리스트에 넣기 - List results = hellos.stream().map(CompletableFuture::join) - .toList(); - results.forEach(logger::info); - } - - @Test - @DisplayName("입력은 void 출력은 String인 비동기 함수 단일 호출 타임아웃 발생.") - public void testGetStringTimeOutIsThisAsync() throws InterruptedException { - // voidParamStringReturn가 비동기 메서드인지 의문이 생김. - CompletableFuture completableFuture = asyncService.voidParamStringReturn(4000, "타임아웃 발생 안 함!"); - long timeOutValue = 1; - TimeUnit timeUnit = TimeUnit.SECONDS; - // 1초가 지난 후 타임아웃 발생 - Assertions.assertThrows(ExecutionException.class, () -> completableFuture.orTimeout(timeOutValue, timeUnit).get()); - } -} From d2eed84c82b329301a423beb11d0b3b838d5c1ad Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Mon, 8 Apr 2024 10:36:54 +0900 Subject: [PATCH 52/62] =?UTF-8?q?=F0=9F=8E=A8=20Style=20:=20polishing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../batch/ConcurrentBatchingCounter.java | 1 - ...oncurrentParameterizedBatchingCounter.java | 2 - .../queueCounter/QueueCounterTest.java | 52 ++++++++----------- 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java index 07b70be..4732395 100644 --- a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java +++ b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java @@ -9,7 +9,6 @@ @Component public class ConcurrentBatchingCounter implements BatchCounter { - private final AtomicLong counter = new AtomicLong(); private final ConcurrentMap batch = new ConcurrentHashMap<>(); diff --git a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java index cbd6ccd..8be5497 100644 --- a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java +++ b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java @@ -12,9 +12,7 @@ @Component @Profile("dev") public class ConcurrentParameterizedBatchingCounter implements BatchCounter { - private static final int BATCH_SIZE = 100; - private final AtomicLong counter = new AtomicLong(); private final ConcurrentMap> batch = new ConcurrentHashMap<>(); diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java index e1a0e31..5587737 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java @@ -16,8 +16,8 @@ public class QueueCounterTest { private static final int consumerNThreads = 9; private Consumer consumer; private Producer producer; - private ExecutorService consumerService; - private ExecutorService producerService; + private ExecutorService consumerExecutor; + private ExecutorService producerExecutor; @BeforeEach public void setup() { @@ -25,32 +25,28 @@ public void setup() { BlockingQueue queue = new LinkedBlockingQueue<>(queueCapacity); consumer = new CounterConsumer(queue); producer = new CounterProducer(queue); - producerService = Executors.newFixedThreadPool(producerNThreads); - consumerService = Executors.newFixedThreadPool(consumerNThreads); + producerExecutor = Executors.newFixedThreadPool(producerNThreads); + consumerExecutor = Executors.newFixedThreadPool(consumerNThreads); } @AfterEach public void cleanup() { - producerService.shutdown(); - consumerService.shutdown(); + producerExecutor.shutdown(); + consumerExecutor.shutdown(); } @Test @SuppressWarnings("SpellCheckingInspection") @DisplayName("멀티 프로듀서 싱글 컨슈머") public void multiProducerSingleConsumer() { - Assertions.assertTimeout(Duration.ofSeconds(10), () -> { - runTest(1); - }); + Assertions.assertTimeout(Duration.ofSeconds(10), () -> runTest(1)); } @Test @SuppressWarnings("SpellCheckingInspection") @DisplayName("멀티 프로듀서 멀티 컨슈머") public void multiProducerMultiConsumer() { - Assertions.assertTimeout(Duration.ofSeconds(10), () -> { - runTest(consumerNThreads); - }); + Assertions.assertTimeout(Duration.ofSeconds(10), () -> runTest(consumerNThreads)); } private void runTest(int consumerCount) throws InterruptedException { @@ -69,30 +65,26 @@ private void runTest(int consumerCount) throws InterruptedException { } private void createProducerThreads(CountDownLatch producerLatch) { + Callable task = () -> { + for (int j = 0; j < nAddsPerThread; j++) { + producer.add(valueToAdd); + } + producerLatch.countDown(); + return null; + }; for (int i = 0; i < producerNThreads; i++) { - producerService.submit(() -> { - try { - for (int j = 0; j < nAddsPerThread; j++) { - producer.add(valueToAdd); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - producerLatch.countDown(); - }); + producerExecutor.submit(task); } } private void createConsumerThreads(CountDownLatch consumerLatch, int consumerCount) { + Callable task = () -> { + consumer.consumeEvent(1, TimeUnit.SECONDS); + consumerLatch.countDown(); + return null; + }; for (int i = 0; i < consumerCount; i++) { - consumerService.submit(() -> { - try { - consumer.consumeEvent(1, TimeUnit.SECONDS); - consumerLatch.countDown(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }); + consumerExecutor.submit(task); } } } From 2b55fc4fd5f4d2d06e9368e19e8a3cbe8ad96237 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Mon, 8 Apr 2024 10:40:49 +0900 Subject: [PATCH 53/62] =?UTF-8?q?=F0=9F=94=A7=20Modify=20:=20rename=20pack?= =?UTF-8?q?age?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../counter/{queueCounter => queue}/Consumer.java | 2 +- .../counter/{queueCounter => queue}/CounterConsumer.java | 2 +- .../counter/{queueCounter => queue}/CounterProducer.java | 2 +- .../counter/{queueCounter => queue}/Producer.java | 2 +- .../{queueCounter => counter/queue}/QueueCounterTest.java | 6 +----- 5 files changed, 5 insertions(+), 9 deletions(-) rename src/main/java/com/thread/concurrency/counter/{queueCounter => queue}/Consumer.java (76%) rename src/main/java/com/thread/concurrency/counter/{queueCounter => queue}/CounterConsumer.java (93%) rename src/main/java/com/thread/concurrency/counter/{queueCounter => queue}/CounterProducer.java (86%) rename src/main/java/com/thread/concurrency/counter/{queueCounter => queue}/Producer.java (61%) rename src/test/java/com/thread/concurrency/{queueCounter => counter/queue}/QueueCounterTest.java (90%) diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java b/src/main/java/com/thread/concurrency/counter/queue/Consumer.java similarity index 76% rename from src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java rename to src/main/java/com/thread/concurrency/counter/queue/Consumer.java index 3a2fada..06492a9 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/Consumer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/Consumer.java @@ -1,4 +1,4 @@ -package com.thread.concurrency.counter.queueCounter; +package com.thread.concurrency.counter.queue; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/queue/CounterConsumer.java similarity index 93% rename from src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java rename to src/main/java/com/thread/concurrency/counter/queue/CounterConsumer.java index 4fa4c3b..f64c945 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterConsumer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/CounterConsumer.java @@ -1,4 +1,4 @@ -package com.thread.concurrency.counter.queueCounter; +package com.thread.concurrency.counter.queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/queue/CounterProducer.java similarity index 86% rename from src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java rename to src/main/java/com/thread/concurrency/counter/queue/CounterProducer.java index eea7bcc..5ad6c4f 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/CounterProducer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/CounterProducer.java @@ -1,4 +1,4 @@ -package com.thread.concurrency.counter.queueCounter; +package com.thread.concurrency.counter.queue; import java.util.concurrent.BlockingQueue; diff --git a/src/main/java/com/thread/concurrency/counter/queueCounter/Producer.java b/src/main/java/com/thread/concurrency/counter/queue/Producer.java similarity index 61% rename from src/main/java/com/thread/concurrency/counter/queueCounter/Producer.java rename to src/main/java/com/thread/concurrency/counter/queue/Producer.java index 71d943d..979b044 100644 --- a/src/main/java/com/thread/concurrency/counter/queueCounter/Producer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/Producer.java @@ -1,4 +1,4 @@ -package com.thread.concurrency.counter.queueCounter; +package com.thread.concurrency.counter.queue; public interface Producer { void add(long value) throws InterruptedException; diff --git a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java similarity index 90% rename from src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java rename to src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java index 5587737..95da2d9 100644 --- a/src/test/java/com/thread/concurrency/queueCounter/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java @@ -1,9 +1,5 @@ -package com.thread.concurrency.queueCounter; +package com.thread.concurrency.counter.queue; -import com.thread.concurrency.counter.queueCounter.Consumer; -import com.thread.concurrency.counter.queueCounter.CounterConsumer; -import com.thread.concurrency.counter.queueCounter.CounterProducer; -import com.thread.concurrency.counter.queueCounter.Producer; import org.junit.jupiter.api.*; import java.time.Duration; From 00a591de4f2dac72ec100f956e8a0335bbb4dae4 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Tue, 9 Apr 2024 10:47:58 +0900 Subject: [PATCH 54/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20implements=20`clea?= =?UTF-8?q?r()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../concurrency/counter/AtomicCounter.java | 5 +++++ .../counter/CompletableFutureCounter.java | 8 +++++++- .../com/thread/concurrency/counter/Counter.java | 2 ++ .../thread/concurrency/counter/LockCounter.java | 10 ++++++++++ .../concurrency/counter/PollingCounter.java | 16 ++++++++++++++-- .../concurrency/counter/SynchronizedCounter.java | 5 +++++ ...chingCounter.java => AtomicBatchCounter.java} | 8 +++++++- .../ConcurrentParameterizedBatchingCounter.java | 6 ++++++ .../counter/batch/BatchCounterTest.java | 2 +- 9 files changed, 57 insertions(+), 5 deletions(-) rename src/main/java/com/thread/concurrency/counter/batch/{ConcurrentBatchingCounter.java => AtomicBatchCounter.java} (87%) diff --git a/src/main/java/com/thread/concurrency/counter/AtomicCounter.java b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java index cc90632..0834634 100644 --- a/src/main/java/com/thread/concurrency/counter/AtomicCounter.java +++ b/src/main/java/com/thread/concurrency/counter/AtomicCounter.java @@ -17,4 +17,9 @@ public void add(int value) { public int show() { return count.get(); } + + @Override + public void clear() { + count.set(0); + } } diff --git a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java index dad5b72..2a60a1d 100644 --- a/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java +++ b/src/main/java/com/thread/concurrency/counter/CompletableFutureCounter.java @@ -7,7 +7,6 @@ @Component public class CompletableFutureCounter implements Counter { - private CompletableFuture counter; public CompletableFutureCounter() { @@ -30,4 +29,11 @@ public int show() { throw new RuntimeException(e); } } + + @Override + public void clear() { + synchronized (this) { + counter = CompletableFuture.completedFuture(0); + } + } } diff --git a/src/main/java/com/thread/concurrency/counter/Counter.java b/src/main/java/com/thread/concurrency/counter/Counter.java index 908294f..3b057ab 100644 --- a/src/main/java/com/thread/concurrency/counter/Counter.java +++ b/src/main/java/com/thread/concurrency/counter/Counter.java @@ -4,4 +4,6 @@ public interface Counter { void add(int value); int show(); + + void clear(); } diff --git a/src/main/java/com/thread/concurrency/counter/LockCounter.java b/src/main/java/com/thread/concurrency/counter/LockCounter.java index e0eec2b..13607b3 100644 --- a/src/main/java/com/thread/concurrency/counter/LockCounter.java +++ b/src/main/java/com/thread/concurrency/counter/LockCounter.java @@ -23,4 +23,14 @@ public void add(int value) { public int show() { return count; } + + @Override + public void clear() { + lock.lock(); + try { + count = 0; + } finally { + lock.unlock(); + } + } } diff --git a/src/main/java/com/thread/concurrency/counter/PollingCounter.java b/src/main/java/com/thread/concurrency/counter/PollingCounter.java index a11ad44..79d0f60 100644 --- a/src/main/java/com/thread/concurrency/counter/PollingCounter.java +++ b/src/main/java/com/thread/concurrency/counter/PollingCounter.java @@ -3,8 +3,6 @@ import org.springframework.stereotype.Component; @Component -// use technique spin-lock(busy waiting) -// this approach can lead to high CPU usage if the lock is heavily contended public class PollingCounter implements Counter { private static int count = 100; private static volatile boolean lock = false; @@ -31,4 +29,18 @@ public void add(int value) { public int show() { return count; } + + @Override + public void clear() { + while (true) { + if (!lock) { + synchronized (PollingCounter.class) { + lock = true; + count = 0; + lock = false; + break; + } + } + } + } } diff --git a/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java index 4c29b8f..3fc0395 100644 --- a/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java +++ b/src/main/java/com/thread/concurrency/counter/SynchronizedCounter.java @@ -16,4 +16,9 @@ public synchronized void add(int value) { public synchronized int show() { return counter; } + + @Override + public synchronized void clear() { + counter = 0; + } } diff --git a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java b/src/main/java/com/thread/concurrency/counter/batch/AtomicBatchCounter.java similarity index 87% rename from src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java rename to src/main/java/com/thread/concurrency/counter/batch/AtomicBatchCounter.java index 4732395..43ef28f 100644 --- a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentBatchingCounter.java +++ b/src/main/java/com/thread/concurrency/counter/batch/AtomicBatchCounter.java @@ -8,7 +8,7 @@ import java.util.concurrent.atomic.LongAdder; @Component -public class ConcurrentBatchingCounter implements BatchCounter { +public class AtomicBatchCounter implements BatchCounter { private final AtomicLong counter = new AtomicLong(); private final ConcurrentMap batch = new ConcurrentHashMap<>(); @@ -35,4 +35,10 @@ public void flush() { var threadId = Thread.currentThread().threadId(); flush(threadId); } + + @Override + public void clear() { + counter.set(0); + batch.clear(); + } } diff --git a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java index 8be5497..be66936 100644 --- a/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java +++ b/src/main/java/com/thread/concurrency/counter/batch/ConcurrentParameterizedBatchingCounter.java @@ -30,6 +30,12 @@ public int show() { return counter.intValue(); } + @Override + public void clear() { + counter.set(0); + batch.clear(); + } + private void flush(long threadId) { var list = batch.getOrDefault(threadId, null); if (list != null && !list.isEmpty()) { diff --git a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java index 0924777..68dbe87 100644 --- a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java @@ -37,7 +37,7 @@ private static List range(int numberOfThreads, int expected) { } public static Stream batchCounterProvider() { - return Stream.of(new ConcurrentBatchingCounter(), new ConcurrentParameterizedBatchingCounter()); + return Stream.of(new AtomicBatchCounter(), new ConcurrentParameterizedBatchingCounter()); } @ParameterizedTest From 26429bad103e5207bb78f9a7ab9fbf05247434c3 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Tue, 9 Apr 2024 10:49:54 +0900 Subject: [PATCH 55/62] =?UTF-8?q?=F0=9F=9A=9A=20Chore=20(QueueCounter)=20:?= =?UTF-8?q?=20set=20profile=20`dev`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../java/com/thread/concurrency/counter/queue/Consumer.java | 3 +++ .../com/thread/concurrency/counter/queue/CounterConsumer.java | 3 +++ .../com/thread/concurrency/counter/queue/CounterProducer.java | 3 +++ .../java/com/thread/concurrency/counter/queue/Producer.java | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/main/java/com/thread/concurrency/counter/queue/Consumer.java b/src/main/java/com/thread/concurrency/counter/queue/Consumer.java index 06492a9..af19793 100644 --- a/src/main/java/com/thread/concurrency/counter/queue/Consumer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/Consumer.java @@ -1,7 +1,10 @@ package com.thread.concurrency.counter.queue; +import org.springframework.context.annotation.Profile; + import java.util.concurrent.TimeUnit; +@Profile("dev") public interface Consumer { void consumeEvent(long timeout, TimeUnit unit) throws InterruptedException; diff --git a/src/main/java/com/thread/concurrency/counter/queue/CounterConsumer.java b/src/main/java/com/thread/concurrency/counter/queue/CounterConsumer.java index f64c945..2fc1716 100644 --- a/src/main/java/com/thread/concurrency/counter/queue/CounterConsumer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/CounterConsumer.java @@ -1,9 +1,12 @@ package com.thread.concurrency.counter.queue; +import org.springframework.context.annotation.Profile; + import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +@Profile("dev") public class CounterConsumer implements Consumer { private final BlockingQueue queue; private final AtomicLong count = new AtomicLong(0); diff --git a/src/main/java/com/thread/concurrency/counter/queue/CounterProducer.java b/src/main/java/com/thread/concurrency/counter/queue/CounterProducer.java index 5ad6c4f..66c243a 100644 --- a/src/main/java/com/thread/concurrency/counter/queue/CounterProducer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/CounterProducer.java @@ -1,7 +1,10 @@ package com.thread.concurrency.counter.queue; +import org.springframework.context.annotation.Profile; + import java.util.concurrent.BlockingQueue; +@Profile("dev") public class CounterProducer implements Producer { private final BlockingQueue queue; diff --git a/src/main/java/com/thread/concurrency/counter/queue/Producer.java b/src/main/java/com/thread/concurrency/counter/queue/Producer.java index 979b044..f36b4d6 100644 --- a/src/main/java/com/thread/concurrency/counter/queue/Producer.java +++ b/src/main/java/com/thread/concurrency/counter/queue/Producer.java @@ -1,5 +1,8 @@ package com.thread.concurrency.counter.queue; +import org.springframework.context.annotation.Profile; + +@Profile("dev") public interface Producer { void add(long value) throws InterruptedException; } From bad08f5762db5e0d91a28b80de7c064423d3c552 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Tue, 9 Apr 2024 10:50:55 +0900 Subject: [PATCH 56/62] =?UTF-8?q?=E2=9C=A8=20Feat=20(Counter)=20:=20custom?= =?UTF-8?q?=20test=20executor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../SpringThreadConcurrencyApplication.java | 91 +-------------- .../executor/CounterBenchmark.java | 105 ++++++++++++++++++ .../executor/CounterConfiguration.java | 38 +++++++ .../concurrency/executor/Performance.java | 8 ++ 4 files changed, 157 insertions(+), 85 deletions(-) create mode 100644 src/main/java/com/thread/concurrency/executor/CounterBenchmark.java create mode 100644 src/main/java/com/thread/concurrency/executor/CounterConfiguration.java create mode 100644 src/main/java/com/thread/concurrency/executor/Performance.java diff --git a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java index 87ec2a2..839e4b9 100644 --- a/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java +++ b/src/main/java/com/thread/concurrency/SpringThreadConcurrencyApplication.java @@ -1,96 +1,17 @@ package com.thread.concurrency; -import com.thread.concurrency.counter.batch.BatchCounter; -import com.thread.concurrency.counter.batch.ConcurrentBatchingCounter; +import com.thread.concurrency.executor.CounterBenchmark; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.lang.management.MemoryUsage; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.Consumer; +import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication -@EnableAsync public class SpringThreadConcurrencyApplication { public static void main(String[] args) { - SpringApplication.run(SpringThreadConcurrencyApplication.class, args); - - MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - MemoryUsage initialMemoryUsage = memoryMXBean.getHeapMemoryUsage(); - long initialTime = System.currentTimeMillis(); - - // Run the test - int totalRequest = Integer.MAX_VALUE / 1024; - conditionalMultiThreading(totalRequest); - - MemoryUsage finalMemoryUsage = memoryMXBean.getHeapMemoryUsage(); - long finalTime = System.currentTimeMillis(); - long elapsedTime = finalTime - initialTime; - long usedMemory = finalMemoryUsage.getUsed() - initialMemoryUsage.getUsed(); - - // request with comma - System.out.println("Total request: " + String.format("%,d", totalRequest)); - // seconds - System.out.println("Elapsed time: " + elapsedTime / 1000 + " s"); - // megabytes - System.out.println("Used memory: " + usedMemory / 1024 / 1024 + " MB"); - } - - private static void conditionalMultiThreading(int expected) { - BatchCounter counter = new ConcurrentBatchingCounter(); - - // given - int numberOfThreads = 128; - List iterPerThread = range(numberOfThreads, expected); - Consumer task = (Integer number) -> { - for (int i = 0; i < number; i++) { - counter.add(1); - } - counter.flush(); - }; - // when - try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { - List> futures = iterPerThread.stream().map(number -> CompletableFuture.runAsync(() -> task.accept(number), executor)).toList(); - futures.forEach(CompletableFuture::join); - } - // then - assert expected == counter.show(); - } - - private static List range(int numberOfThreads, int expected) { - int baseValue = expected / numberOfThreads; - int remainder = expected % numberOfThreads; - - List result = new ArrayList<>(); - for (int i = 0; i < numberOfThreads; i++) { - if (i < remainder) { - result.add(baseValue + 1); - } else { - result.add(baseValue); - } - } - return result; - } - - @Bean - public Executor taskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // Spring에서 사용하는 스레드를 제어한느 설정 - executor.setCorePoolSize(50); // thread-pool에 살아있는 thread의 최소 개수 - executor.setMaxPoolSize(50); // thread-pool에서 사용할 수 있는 최대 개수 - executor.setQueueCapacity(500); //thread-pool에 최대 queue 크기 - executor.setThreadNamePrefix("AsyncApp-"); - executor.initialize(); - return executor; + ConfigurableApplicationContext context = SpringApplication.run(SpringThreadConcurrencyApplication.class, args); + var performance = context.getBean(CounterBenchmark.class).benchmark(); + System.out.println("|----------------------|---------------|---------------|---------------|"); + System.out.println(performance); } } diff --git a/src/main/java/com/thread/concurrency/executor/CounterBenchmark.java b/src/main/java/com/thread/concurrency/executor/CounterBenchmark.java new file mode 100644 index 0000000..5ad69cc --- /dev/null +++ b/src/main/java/com/thread/concurrency/executor/CounterBenchmark.java @@ -0,0 +1,105 @@ +package com.thread.concurrency.executor; + +import com.thread.concurrency.counter.Counter; +import com.thread.concurrency.counter.batch.BatchCounter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + + +@Component +public class CounterBenchmark { + private final Counter counter; + private final String counterName; + private final int iterations, nThreads, totalRequests; + + @Autowired + public CounterBenchmark(CounterConfiguration.CounterConfig config) { + System.out.println(config); + this.counter = config.counter(); + this.counterName = counter.getClass().getSimpleName(); + this.iterations = config.iterations(); + this.nThreads = config.nThreads(); + this.totalRequests = config.totalRequests(); + } + + private void doAdd(List params, Consumer task) { + try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + counter.clear(); + List> futures = new ArrayList<>(); + for (int i = 0; i < nThreads; i++) { + int nRequest = params.get(i); + futures.add(CompletableFuture.runAsync(() -> task.accept(nRequest), executor)); + } + futures.forEach(CompletableFuture::join); + } + } + + public Performance benchmark() { + System.out.printf("| %-20s | %13s | %13s | %13s |%n", "Name", "Time", "Threads", "Memory"); + System.out.println("|----------------------|---------------|---------------|---------------|"); + List performances = new ArrayList<>(); + for (int i = 0; i < iterations; i++) { + var performance = calculateEach(); + performances.add(performance); + System.out.println(performance); + } + return reduce(performances); + } + + private Performance calculateEach() { + List iterPerThread = range(); + Consumer task = (Integer nRequest) -> { + for (int i = 0; i < nRequest; i++) { + counter.add(1); + } + if (counter instanceof BatchCounter batchCounter) { + batchCounter.flush(); + } + }; + System.gc(); + long timeOnStart = System.currentTimeMillis(); + long memoryOnStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + + doAdd(iterPerThread, task); + if (counter.show() != totalRequests) { + System.out.printf("Counter: %d, Total: %d%n", counter.show(), totalRequests); + } + + + long timeOnEnd = System.currentTimeMillis(); + long memoryOnEnd = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + + + long timeElapsed = timeOnEnd - timeOnStart; + long memoryUsed = memoryOnEnd - memoryOnStart; + return new Performance(counterName, timeElapsed, iterPerThread.size(), memoryUsed); + } + + private List range() { + int baseValue = totalRequests / nThreads; + int remainder = totalRequests % nThreads; + + List result = new ArrayList<>(); + for (int i = 0; i < nThreads; i++) { + var remainderValue = i < remainder ? 1 : 0; + result.add(baseValue + remainderValue); + } + assert result.stream().mapToInt(Integer::intValue).sum() == totalRequests; + return result; + } + + private Performance reduce(List performances) { + performances.sort((a, b) -> (int) (a.time() - b.time())); + long time = performances.get(performances.size() / 2).time(); + performances.sort((a, b) -> (int) (a.memory() - b.memory())); + long memory = performances.get(performances.size() / 2).memory(); + return new Performance(counterName, time, nThreads, memory); + } +} diff --git a/src/main/java/com/thread/concurrency/executor/CounterConfiguration.java b/src/main/java/com/thread/concurrency/executor/CounterConfiguration.java new file mode 100644 index 0000000..21979d8 --- /dev/null +++ b/src/main/java/com/thread/concurrency/executor/CounterConfiguration.java @@ -0,0 +1,38 @@ +package com.thread.concurrency.executor; + +import com.thread.concurrency.counter.AtomicCounter; +import com.thread.concurrency.counter.Counter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class CounterConfiguration { + + @Bean + public Counter counter() { + return new AtomicCounter(); + } + + @Bean + public CounterConfig counterConfig(Counter counter) { + int iterations = 25; + int totalRequest = Integer.MAX_VALUE; + int nThreads = 1; + return new CounterConfig(counter, iterations, totalRequest, nThreads); + } + + public record CounterConfig(Counter counter, int iterations, int totalRequests, int nThreads) { + @Override + public String toString() { + // multiple-lines format + return """ + CounterConfig { + counter=%s, + iterations=%d, + totalRequests=%d, + nThreads=%d + }""".formatted(counter.getClass().getSimpleName(), iterations, totalRequests, nThreads).stripIndent(); + } + } +} diff --git a/src/main/java/com/thread/concurrency/executor/Performance.java b/src/main/java/com/thread/concurrency/executor/Performance.java new file mode 100644 index 0000000..b215102 --- /dev/null +++ b/src/main/java/com/thread/concurrency/executor/Performance.java @@ -0,0 +1,8 @@ +package com.thread.concurrency.executor; + +public record Performance(String name, long time, int threads, long memory) { + @Override + public String toString() { + return String.format("| %-20s | %10d ms | %5d threads | %10d KB |", name, time, threads, memory / 1024); + } +} From 130eaf8a91ebec22fc2c3e6c1f4743f6915cc814 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Tue, 9 Apr 2024 10:51:53 +0900 Subject: [PATCH 57/62] =?UTF-8?q?=F0=9F=90=9B=20Fix=20:=20remove=20redunda?= =?UTF-8?q?ncy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- src/test/java/com/thread/concurrency/counter/CounterTest.java | 2 -- .../counter/SpringThreadConcurrencyApplicationTests.java | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java index 95222b8..2ec45ab 100644 --- a/src/test/java/com/thread/concurrency/counter/CounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -3,13 +3,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.test.context.SpringBootTest; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Stream; -@SpringBootTest public class CounterTest { public static Stream counterProvider() { diff --git a/src/test/java/com/thread/concurrency/counter/SpringThreadConcurrencyApplicationTests.java b/src/test/java/com/thread/concurrency/counter/SpringThreadConcurrencyApplicationTests.java index 60cb352..75d7009 100644 --- a/src/test/java/com/thread/concurrency/counter/SpringThreadConcurrencyApplicationTests.java +++ b/src/test/java/com/thread/concurrency/counter/SpringThreadConcurrencyApplicationTests.java @@ -1,15 +1,11 @@ package com.thread.concurrency.counter; -import com.thread.concurrency.SpringThreadConcurrencyApplication; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - @SpringBootTest class SpringThreadConcurrencyApplicationTests { @Test void contextLoads() { - assertDoesNotThrow(() -> SpringThreadConcurrencyApplication.main(new String[]{})); } } From 4f5af6c5d2fed511834f63770f92f8586aead3ff Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Tue, 9 Apr 2024 10:52:05 +0900 Subject: [PATCH 58/62] =?UTF-8?q?=F0=9F=93=9D=20Docs=20:=20benchmark=20res?= =?UTF-8?q?ults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- README.md | 64 +++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 4cf4039..9249f1d 100644 --- a/README.md +++ b/README.md @@ -17,36 +17,34 @@ - [동시성 기본 조건과 관심사](https://github.com/spring-templates/spring-concurrency-thread/discussions/2) -## 카운터 밴치 마크 - -**실험 환경** - -칩 : Apple M2 8코어, -메모리 : 16GB - -| 5회 평균 | 최대 스레드 개수 | 전체 요청 수 | 테스트 시간(ms) | 메모리 사용량(MB) | -|--------------------------|-----------|-----------|------------|-------------| -| AtomicCounter | 9 | 5,000,000 | 321.15 | 12.82 | -| AtomicCounter | 15 | 5,000,000 | 419.33 | 12.16 | -| CompletableFutureCounter | 9 | 5,000,000 | 885.95 | 11.78 | -| CompletableFutureCounter | 15 | 5,000,000 | 939.16 | 11.78 | -| SynchronizedCounter | 9 | 5,000,000 | 398.63 | 12.32 | -| SynchronizedCounter | 15 | 5,000,000 | 495.99 | 11.86 | - -- **프로세서** 12th Gen Intel(R) Core(TM) i7-12650H, 2300Mhz, 10 코어, 16 논리 프로세서 - -- **구현체** ConcurrentBatchingCounter - -- **전체 요청 수** Integer.MAX_VALUE - -| 스레드 개수 | 테스트 시간(s) | 메모리 사용량(MB) | -|--------|-----------|-------------| -| 1 | 26-28 | 4 | -| 2 | 14 | 4 | -| 4 | 9 | 4 | -| 8 | 7 | 4 | -| 16 | 5 | 4 | -| 32 | 5-6 | 4 | -| 64 | 6 | 4 | -| 128 | 9 | 337 | -| 1024 | 10 | 373 | +# [Counter-implementation Benchmark](https://www.notion.so/softsquared/f314375356b54381a8878cf2dabd381b) + +> - median of 25 iterations +> - nRequests: 2^21 - 1 + +| name | nThreads | time (ms) | memory (KB) | +|-------------------|----------|-----------|-------------| +| AtomicBatch | 4 | 12 | 480 | +| Atomic | 1 | 14 | 318 | +| AtomicBatch | 1 | 30 | 240 | +| Lock | 1 | 61 | 241 | +| Synchronized | 1 | 61 | 241 | +| Polling | 1 | 78 | 463 | +| CompletableFuture | 1 | 158 | 25710 | + +### AtomicBatch vs Atomic + +> - nThreads: AtomicBatch=4, Atomic=1 + +| name | nRequests | time (ms) | memory (KB) | +|-------------|-----------|-----------|-------------| +| AtomicBatch | 2^21 - 1 | 12 | 480 | +| AtomicBatch | 2^22 - 1 | 24 | 538 | +| AtomicBatch | 2^23 - 1 | 42 | 572 | +| AtomicBatch | 2^30 - 1 | 5695 | 511 | +| AtomicBatch | 2^31 - 1 | 11621 | 294 | +| Atomic | 2^21 - 1 | 14 | 318 | +| Atomic | 2^22 - 1 | 27 | 244 | +| Atomic | 2^23 - 1 | 55 | 344 | +| Atomic | 2^30 - 1 | 7178 | 103 | +| Atomic | 2^31 - 1 | 14377 | 266 | From c2faebc02bd08143605b7842877efe261ae99d7c Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Wed, 10 Apr 2024 08:40:09 +0900 Subject: [PATCH 59/62] =?UTF-8?q?=F0=9F=8E=A8=20Style=20:=20polishing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메서드 순서 변경 (public, 깊이 우선) Signed-off-by: Hyeon-hak Kim --- .../concurrency/counter/PollingCounter.java | 6 +-- .../counter/batch/AtomicBatchCounter.java | 18 ++++----- .../executor/CounterBenchmark.java | 24 ++++++------ .../concurrency/counter/CounterTest.java | 24 ++++++------ .../counter/batch/BatchCounterTest.java | 38 +++++++++---------- .../counter/queue/QueueCounterTest.java | 14 +++---- 6 files changed, 60 insertions(+), 64 deletions(-) diff --git a/src/main/java/com/thread/concurrency/counter/PollingCounter.java b/src/main/java/com/thread/concurrency/counter/PollingCounter.java index 79d0f60..b3cc8fa 100644 --- a/src/main/java/com/thread/concurrency/counter/PollingCounter.java +++ b/src/main/java/com/thread/concurrency/counter/PollingCounter.java @@ -7,17 +7,13 @@ public class PollingCounter implements Counter { private static int count = 100; private static volatile boolean lock = false; - private static void doAdd(int value) { - count += value; - } - @Override public void add(int value) { while (true) { if (!lock) { synchronized (PollingCounter.class) { lock = true; - doAdd(value); + count += value; lock = false; break; } diff --git a/src/main/java/com/thread/concurrency/counter/batch/AtomicBatchCounter.java b/src/main/java/com/thread/concurrency/counter/batch/AtomicBatchCounter.java index 43ef28f..98db876 100644 --- a/src/main/java/com/thread/concurrency/counter/batch/AtomicBatchCounter.java +++ b/src/main/java/com/thread/concurrency/counter/batch/AtomicBatchCounter.java @@ -23,11 +23,10 @@ public int show() { return counter.intValue(); } - private void flush(long threadId) { - var value = batch.remove(threadId); - if (value != null) { - counter.addAndGet(value.longValue()); - } + @Override + public void clear() { + counter.set(0); + batch.clear(); } @Override @@ -36,9 +35,10 @@ public void flush() { flush(threadId); } - @Override - public void clear() { - counter.set(0); - batch.clear(); + private void flush(long threadId) { + var value = batch.remove(threadId); + if (value != null) { + counter.addAndGet(value.longValue()); + } } } diff --git a/src/main/java/com/thread/concurrency/executor/CounterBenchmark.java b/src/main/java/com/thread/concurrency/executor/CounterBenchmark.java index 5ad69cc..398d15e 100644 --- a/src/main/java/com/thread/concurrency/executor/CounterBenchmark.java +++ b/src/main/java/com/thread/concurrency/executor/CounterBenchmark.java @@ -29,18 +29,6 @@ public CounterBenchmark(CounterConfiguration.CounterConfig config) { this.totalRequests = config.totalRequests(); } - private void doAdd(List params, Consumer task) { - try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { - counter.clear(); - List> futures = new ArrayList<>(); - for (int i = 0; i < nThreads; i++) { - int nRequest = params.get(i); - futures.add(CompletableFuture.runAsync(() -> task.accept(nRequest), executor)); - } - futures.forEach(CompletableFuture::join); - } - } - public Performance benchmark() { System.out.printf("| %-20s | %13s | %13s | %13s |%n", "Name", "Time", "Threads", "Memory"); System.out.println("|----------------------|---------------|---------------|---------------|"); @@ -82,6 +70,18 @@ private Performance calculateEach() { return new Performance(counterName, timeElapsed, iterPerThread.size(), memoryUsed); } + private void doAdd(List params, Consumer task) { + try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + counter.clear(); + List> futures = new ArrayList<>(); + for (int i = 0; i < nThreads; i++) { + int nRequest = params.get(i); + futures.add(CompletableFuture.runAsync(() -> task.accept(nRequest), executor)); + } + futures.forEach(CompletableFuture::join); + } + } + private List range() { int baseValue = totalRequests / nThreads; int remainder = totalRequests % nThreads; diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java index 2ec45ab..a22f2ab 100644 --- a/src/test/java/com/thread/concurrency/counter/CounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -14,18 +14,6 @@ public static Stream counterProvider() { return Stream.of(new LockCounter(), new PollingCounter(), new SynchronizedCounter(), new AtomicCounter(), new CompletableFutureCounter()); } - private static void whenAdd(Counter counter, int nThreads, int addPerThread) { - try (ExecutorService executor = Executors.newFixedThreadPool(nThreads)) { - for (int i = 0; i < nThreads; i++) { - executor.submit(() -> { - for (int j = 0; j < addPerThread; j++) { - counter.add(1); - } - }); - } - } - } - @ParameterizedTest @MethodSource("counterProvider") public void stressTest(Counter counter) { @@ -43,4 +31,16 @@ public void stressTest(Counter counter) { Assertions.assertEquals(expectedValue, counter.show()); System.out.println("Time elapsed: " + (end - start) + "ms"); } + + private static void whenAdd(Counter counter, int nThreads, int addPerThread) { + try (ExecutorService executor = Executors.newFixedThreadPool(nThreads)) { + for (int i = 0; i < nThreads; i++) { + executor.submit(() -> { + for (int j = 0; j < addPerThread; j++) { + counter.add(1); + } + }); + } + } + } } diff --git a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java index 68dbe87..a05f002 100644 --- a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java @@ -17,25 +17,6 @@ class BatchCounterTest { - private static List range() { - return IntStream.range(0, 1000).boxed().collect(Collectors.toList()); - } - - private static List range(int numberOfThreads, int expected) { - int baseValue = expected / numberOfThreads; - int remainder = expected % numberOfThreads; - - List result = new ArrayList<>(); - for (int i = 0; i < numberOfThreads; i++) { - if (i < remainder) { - result.add(baseValue + 1); - } else { - result.add(baseValue); - } - } - return result; - } - public static Stream batchCounterProvider() { return Stream.of(new AtomicBatchCounter(), new ConcurrentParameterizedBatchingCounter()); } @@ -58,6 +39,10 @@ void singleThreading(BatchCounter counter) { Assertions.assertEquals(partialSum, counter.show()); } + private static List range() { + return IntStream.range(0, 1000).boxed().collect(Collectors.toList()); + } + @ParameterizedTest @MethodSource("batchCounterProvider") void conditionalMultiThreading(BatchCounter counter) { @@ -81,6 +66,21 @@ void conditionalMultiThreading(BatchCounter counter) { Assertions.assertEquals(expected, counter.show()); } + private static List range(int numberOfThreads, int expected) { + int baseValue = expected / numberOfThreads; + int remainder = expected % numberOfThreads; + + List result = new ArrayList<>(); + for (int i = 0; i < numberOfThreads; i++) { + if (i < remainder) { + result.add(baseValue + 1); + } else { + result.add(baseValue); + } + } + return result; + } + @ParameterizedTest @MethodSource("batchCounterProvider") void conditionalAsyncVirtualMultiThreading(BatchCounter counter) { diff --git a/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java index 95da2d9..79f2ded 100644 --- a/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java @@ -38,13 +38,6 @@ public void multiProducerSingleConsumer() { Assertions.assertTimeout(Duration.ofSeconds(10), () -> runTest(1)); } - @Test - @SuppressWarnings("SpellCheckingInspection") - @DisplayName("멀티 프로듀서 멀티 컨슈머") - public void multiProducerMultiConsumer() { - Assertions.assertTimeout(Duration.ofSeconds(10), () -> runTest(consumerNThreads)); - } - private void runTest(int consumerCount) throws InterruptedException { Long initialCount = consumer.show(); CountDownLatch producerLatch = new CountDownLatch(producerNThreads); @@ -83,4 +76,11 @@ private void createConsumerThreads(CountDownLatch consumerLatch, int consumerCou consumerExecutor.submit(task); } } + + @Test + @SuppressWarnings("SpellCheckingInspection") + @DisplayName("멀티 프로듀서 멀티 컨슈머") + public void multiProducerMultiConsumer() { + Assertions.assertTimeout(Duration.ofSeconds(10), () -> runTest(consumerNThreads)); + } } From 056e1449a5d8b86c0ae1a92715bc4fae36e77c28 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Wed, 10 Apr 2024 09:20:30 +0900 Subject: [PATCH 60/62] =?UTF-8?q?=E2=9C=85=20Test=20:=20simply=20check=20b?= =?UTF-8?q?enchmark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../executor/CounterBenchmarkTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/test/java/com/thread/concurrency/counter/executor/CounterBenchmarkTest.java diff --git a/src/test/java/com/thread/concurrency/counter/executor/CounterBenchmarkTest.java b/src/test/java/com/thread/concurrency/counter/executor/CounterBenchmarkTest.java new file mode 100644 index 0000000..231596c --- /dev/null +++ b/src/test/java/com/thread/concurrency/counter/executor/CounterBenchmarkTest.java @@ -0,0 +1,24 @@ +package com.thread.concurrency.counter.executor; + +import com.thread.concurrency.executor.CounterBenchmark; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class CounterBenchmarkTest { + + private final CounterBenchmark counterBenchmark; + + @Autowired + public CounterBenchmarkTest(CounterBenchmark counterBenchmark) { + this.counterBenchmark = counterBenchmark; + } + + @Test + void test() { + var performance = counterBenchmark.benchmark(); + Assertions.assertNotNull(performance); + } +} From d2f9facb8b47090b7cfff111aef44b28006b5a53 Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Wed, 10 Apr 2024 09:21:45 +0900 Subject: [PATCH 61/62] =?UTF-8?q?=F0=9F=9A=9A=20Chore=20:=20make=20test=20?= =?UTF-8?q?runtime=20shorter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../com/thread/concurrency/executor/CounterConfiguration.java | 2 +- .../com/thread/concurrency/counter/queue/QueueCounterTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/thread/concurrency/executor/CounterConfiguration.java b/src/main/java/com/thread/concurrency/executor/CounterConfiguration.java index 21979d8..a739431 100644 --- a/src/main/java/com/thread/concurrency/executor/CounterConfiguration.java +++ b/src/main/java/com/thread/concurrency/executor/CounterConfiguration.java @@ -17,7 +17,7 @@ public Counter counter() { @Bean public CounterConfig counterConfig(Counter counter) { int iterations = 25; - int totalRequest = Integer.MAX_VALUE; + int totalRequest = Integer.MAX_VALUE / 1024; int nThreads = 1; return new CounterConfig(counter, iterations, totalRequest, nThreads); } diff --git a/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java b/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java index 79f2ded..d55ab28 100644 --- a/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/queue/QueueCounterTest.java @@ -7,7 +7,7 @@ public class QueueCounterTest { private static final int valueToAdd = 1; - private static final int nAddsPerThread = 1000000; + private static final int nAddsPerThread = 100000; private static final int producerNThreads = 9; private static final int consumerNThreads = 9; private Consumer consumer; From 1fe90f4def968a3fcf136a3dbc1f1a18640375fa Mon Sep 17 00:00:00 2001 From: Hyeon-hak Kim Date: Wed, 10 Apr 2024 09:22:18 +0900 Subject: [PATCH 62/62] =?UTF-8?q?=E2=9C=A8=20Feat=20:=20test=20`clear()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hyeon-hak Kim --- .../com/thread/concurrency/counter/CounterTest.java | 10 +++++++++- .../concurrency/counter/batch/BatchCounterTest.java | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/thread/concurrency/counter/CounterTest.java b/src/test/java/com/thread/concurrency/counter/CounterTest.java index a22f2ab..d2c3732 100644 --- a/src/test/java/com/thread/concurrency/counter/CounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/CounterTest.java @@ -32,7 +32,7 @@ public void stressTest(Counter counter) { System.out.println("Time elapsed: " + (end - start) + "ms"); } - private static void whenAdd(Counter counter, int nThreads, int addPerThread) { + private void whenAdd(Counter counter, int nThreads, int addPerThread) { try (ExecutorService executor = Executors.newFixedThreadPool(nThreads)) { for (int i = 0; i < nThreads; i++) { executor.submit(() -> { @@ -43,4 +43,12 @@ private static void whenAdd(Counter counter, int nThreads, int addPerThread) { } } } + + @ParameterizedTest + @MethodSource("counterProvider") + public void clearTest(Counter counter) { + counter.add(1000); + counter.clear(); + Assertions.assertEquals(0, counter.show()); + } } diff --git a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java index a05f002..8482048 100644 --- a/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java +++ b/src/test/java/com/thread/concurrency/counter/batch/BatchCounterTest.java @@ -21,6 +21,14 @@ public static Stream batchCounterProvider() { return Stream.of(new AtomicBatchCounter(), new ConcurrentParameterizedBatchingCounter()); } + @ParameterizedTest + @MethodSource("batchCounterProvider") + void clearTest(BatchCounter counter) { + counter.add(1000); + counter.clear(); + Assertions.assertEquals(0, counter.show()); + } + @ParameterizedTest @MethodSource("batchCounterProvider") void singleThreading(BatchCounter counter) {