diff --git a/src/language_java/BeforeOrderService.java b/src/language_java/BeforeOrderService.java new file mode 100644 index 0000000..4cce537 --- /dev/null +++ b/src/language_java/BeforeOrderService.java @@ -0,0 +1,42 @@ +package language_java; + +import java.util.HashMap; +import java.util.Map; + +public class BeforeOrderService { + + private final Map productDatabase = new HashMap<>(); + private final Map latestOrderDatabase = new HashMap<>(); + + public BeforeOrderService() { + productDatabase.put("apple", 100); + productDatabase.put("banana", 50); + productDatabase.put("orange", 75); + } + + public void order(String productName, int amount) { + Integer currentStock = getStock(productName); + + simulateDelay(); + + if (currentStock >= amount) { + System.out.println("Current Thread : " + Thread.currentThread().getName() + + " - CurrentStock : " + currentStock + " - Order : " + amount); + productDatabase.put(productName, currentStock - amount); + latestOrderDatabase.put(productName, new OrderInfo(productName, amount, System.currentTimeMillis())); + } + } + + public Integer getStock(String productName) { + return productDatabase.getOrDefault(productName, 0); + } + + private static void simulateDelay() { + try { + Thread.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/language_java/OrderInfo.java b/src/language_java/OrderInfo.java new file mode 100644 index 0000000..2929e63 --- /dev/null +++ b/src/language_java/OrderInfo.java @@ -0,0 +1,14 @@ +package language_java; + +public class OrderInfo { + String productName; + Integer amount; + Long timestamp; + + OrderInfo(String productName, Integer amount, Long timestamp) { + this.productName = productName; + this.amount = amount; + this.timestamp = timestamp; + } + +} diff --git a/src/language_java/OrderService1.java b/src/language_java/OrderService1.java new file mode 100644 index 0000000..d29e617 --- /dev/null +++ b/src/language_java/OrderService1.java @@ -0,0 +1,57 @@ +package language_java; + +import java.util.HashMap; +import java.util.Map; + +public class OrderService1 { + + private final Map productDatabase = new HashMap<>(); + private final Map latestOrderDatabase = new HashMap<>(); + + public OrderService1() { + productDatabase.put("apple", 100); + productDatabase.put("banana", 50); + productDatabase.put("orange", 75); + } + + public synchronized void orderUsingSynchronized(String productName, int amount) { + Integer currentStock = getStock(productName); + + simulateDelay(); + + if (currentStock >= amount) { + System.out.println("Current Thread : " + Thread.currentThread().getName() + + " - CurrentStock : " + currentStock + " - Order : " + amount); + productDatabase.put(productName, currentStock - amount); + latestOrderDatabase.put(productName, new OrderInfo(productName, amount, System.currentTimeMillis())); + } + } + + public void orderUsingSynchronized2(String productName, int amount) { + synchronized (this) { + Integer currentStock = getStock(productName); + + simulateDelay(); + + if (currentStock >= amount) { + System.out.println("Current Thread : " + Thread.currentThread().getName() + + " - CurrentStock : " + currentStock + " - Order : " + amount); + productDatabase.put(productName, currentStock - amount); + latestOrderDatabase.put(productName, new OrderInfo(productName, amount, System.currentTimeMillis())); + } + } + } + + public Integer getStock(String productName) { + return productDatabase.getOrDefault(productName, 0); + } + + private static void simulateDelay() { + try { + Thread.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } +} diff --git a/src/language_java/OrderService2.java b/src/language_java/OrderService2.java new file mode 100644 index 0000000..1e1e96f --- /dev/null +++ b/src/language_java/OrderService2.java @@ -0,0 +1,50 @@ +package language_java; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +public class OrderService2 { + + private final Map productDatabase = new HashMap<>(); + private final Map latestOrderDatabase = new HashMap<>(); + + public OrderService2() { + productDatabase.put("apple", new AtomicInteger(100)); + productDatabase.put("banana", new AtomicInteger(50)); + productDatabase.put("orange", new AtomicInteger(75)); + } + + public void order(String productName, int amount) { + AtomicInteger currentStock = getStock(productName); + + try { + Thread.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + while (true) { + System.out.println("Current Thread : " + Thread.currentThread().getName() + + " - currentStock : " + currentStock + " - Order : " + amount); + int current = currentStock.get(); + if (current < amount) { + return; + } + + boolean success = currentStock.compareAndSet(current, current - amount); + if (success) { + System.out.println("Current Thread : " + Thread.currentThread().getName() + + " - Stock before order: " + current + " - Order: " + amount + + " - Stock after order: " + currentStock.get()); + latestOrderDatabase.put(productName, new OrderInfo(productName, amount, System.currentTimeMillis())); + break; + } + } + } + + public AtomicInteger getStock(String productName) { + return productDatabase.getOrDefault(productName, new AtomicInteger(0)); + } +} diff --git a/src/language_java/OrderService3.java b/src/language_java/OrderService3.java new file mode 100644 index 0000000..87dc30e --- /dev/null +++ b/src/language_java/OrderService3.java @@ -0,0 +1,39 @@ +package language_java; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class OrderService3 { + + private final Map productDatabase = new ConcurrentHashMap<>(); + private final Map latestOrderDatabase = new ConcurrentHashMap<>(); + + public OrderService3() { + productDatabase.put("apple", 100); + productDatabase.put("banana", 50); + productDatabase.put("orange", 75); + } + + public void order(String productName, int amount) { + try { + Thread.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + productDatabase.compute(productName, (key, currentStock) -> { + if (currentStock == null || currentStock < amount) { + return currentStock; + } + System.out.println("Current Thread : " + Thread.currentThread().getName() + + " - CurrentStock : " + currentStock + " - Order : " + amount); + latestOrderDatabase.put(productName, new OrderInfo(productName, amount, System.currentTimeMillis())); + return currentStock - amount; + }); + } + + public int getStock(String productName) { + return productDatabase.getOrDefault(productName, 0); + } +} diff --git a/test/language_java/BeforeOrderServiceTest.java b/test/language_java/BeforeOrderServiceTest.java new file mode 100644 index 0000000..fbbae3e --- /dev/null +++ b/test/language_java/BeforeOrderServiceTest.java @@ -0,0 +1,49 @@ +package language_java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@DisplayName("여러 스레드가 동시에 주문을 요청할 때") +class BeforeOrderServiceTest { + + private final BeforeOrderService service = new BeforeOrderService(); + + @Test + @DisplayName("동시성 이슈로 재고 불일치가 발생한다.") + void givenMultipleThreads_whenConcurrentOrders_thenNotEqualsExpectedStock() throws InterruptedException { + String productName = "apple"; + int initialStock = service.getStock(productName); + + int orderAmount = 8; + int threadCount = 100; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + executor.execute(() -> { + try { + service.order(productName, orderAmount); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executor.shutdown(); + + int expectedStock = initialStock % orderAmount; + int actualStock = service.getStock(productName); + + System.out.println("Expected Stock : " + expectedStock + ", Actual Stock : " + actualStock); + + assertNotEquals(expectedStock, actualStock); + } +} diff --git a/test/language_java/OrderService1Test.java b/test/language_java/OrderService1Test.java new file mode 100644 index 0000000..9ccb212 --- /dev/null +++ b/test/language_java/OrderService1Test.java @@ -0,0 +1,83 @@ +package language_java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("여러 스레드가 동시에 주문을 요청할 때") +public class OrderService1Test { + + private final OrderService1 service = new OrderService1(); + + + @Test + @DisplayName("instance 에 synchronized 키워드를 사용하면, 재고가 일치한다.") + void givenMultipleThreads_whenSynchronizedMultipleThreads_thenEqualsExpectedStock() throws InterruptedException { + String productName = "apple"; + int initialStock = service.getStock(productName); + + int orderAmount = 8; + int threadCount = 100; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + executor.execute(() -> { + try { + service.orderUsingSynchronized(productName, orderAmount); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executor.shutdown(); + + int expectedStock = initialStock % orderAmount; + int actualStock = service.getStock(productName); + + System.out.println("Expected Stock : " + expectedStock + ", Actual Stock : " + actualStock); + + assertEquals(expectedStock, actualStock); + } + + @Test + @DisplayName("block 에 synchronized 키워드를 사용하면, 재고가 일치한다.") + void givenMultipleThreads_whenSynchronizedBlockMultipleThreads_thenEqualsExpectedStock() throws InterruptedException { + String productName = "apple"; + int initialStock = service.getStock(productName); + + int orderAmount = 8; + int threadCount = 100; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + executor.execute(() -> { + try { + service.orderUsingSynchronized2(productName, orderAmount); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executor.shutdown(); + + int expectedStock = initialStock % orderAmount; + int actualStock = service.getStock(productName); + + System.out.println("Expected Stock : " + expectedStock + ", Actual Stock : " + actualStock); + + assertEquals(expectedStock, actualStock); + } +} diff --git a/test/language_java/OrderService2Test.java b/test/language_java/OrderService2Test.java new file mode 100644 index 0000000..4516524 --- /dev/null +++ b/test/language_java/OrderService2Test.java @@ -0,0 +1,49 @@ +package language_java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("여러 스레드가 동시에 주문을 요청할 때") +public class OrderService2Test { + + private final OrderService2 service = new OrderService2(); + + @Test + @DisplayName("AtomicInteger 을 사용하면 재고가 일치한다.") + void givenMultipleThreads_whenUsingAtomicInteger_thenEqualsExpectedStock() throws InterruptedException { + String productName = "apple"; + int initialStock = service.getStock(productName).get(); + + int orderAmount = 8; + int threadCount = 100; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + executor.execute(() -> { + try { + service.order(productName, orderAmount); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executor.shutdown(); + + int expectedStock = initialStock % orderAmount; + int actualStock = service.getStock(productName).get(); + + System.out.println("Expected Stock : " + expectedStock + ", Actual Stock : " + actualStock); + + assertEquals(expectedStock, actualStock); + } +} diff --git a/test/language_java/OrderService3Test.java b/test/language_java/OrderService3Test.java new file mode 100644 index 0000000..8f27823 --- /dev/null +++ b/test/language_java/OrderService3Test.java @@ -0,0 +1,49 @@ +package language_java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("여러 스레드가 동시에 주문을 요청할 때") +public class OrderService3Test { + + private final OrderService3 service = new OrderService3(); + + @Test + @DisplayName("ConcurrentHashMap 을 사용하면, 재고가 일치한다.") + void givenMultipleThreads_whenUsingConcurrentHashMap_thenEqualsExpectedStock() throws InterruptedException { + String productName = "apple"; + int initialStock = service.getStock(productName); + + int orderAmount = 8; + int threadCount = 100; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + executor.execute(() -> { + try { + service.order(productName, orderAmount); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executor.shutdown(); + + int expectedStock = initialStock % orderAmount; + int actualStock = service.getStock(productName); + + System.out.println("Expected Stock : " + expectedStock + ", Actual Stock : " + actualStock); + + assertEquals(expectedStock, actualStock); + } +}