From 6494b667167f66f90077329f2c22b64266a4538e Mon Sep 17 00:00:00 2001 From: ohchansol Date: Wed, 3 Apr 2024 13:05:41 +0900 Subject: [PATCH 1/3] =?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 2/3] =?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 3/3] =?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); } }