반응형

CountDownLatch와 사이드 프로젝트에서 활용-지선학

CountDownLatch란?

CountDownLatch는 지정된 횟수만큼 countDown() 메서드가 호출될 때까지 현재 스레드를 대기 상태로 만들고, 카운트가 0에 도달하면 대기 중인 스레드를 다시 실행 가능하게 만드는 동기화 도구입니다. 이를 주로 여러 개의 작업 스레드가 완료될 때까지 대기하고, 모든 작업이 끝나면 특정 스레드를 다시 실행하게 할 때 사용합니다.

Method Summary

예시코드

쇼핑몰 사이트에서 특정 사용자가 컴퓨터 부품을 주문하는 상황임

사용자가 물건 주문 요청을 하면 서버가 각각 부품의 재고 상황을 파악하고, 구매 가능하지 파악하는 로직임

사용자가 부품의 재고를 순차적으로 0초, 5초 *4(재고없는 물건) 대략 20초의 시간이 아닌, 다중쓰레드를 이용해서 대략 5초 이내로 처리 속도를 개선할 수 있음

import com.google.gson.Gson;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class ComputerPartsPurchaseSimulation {

    public static void main(String[] args) throws InterruptedException {

        List<String> productOrderRequests = Arrays.asList("CPU", "RAM", "Motherboard", "SSD", "HDD");

        int threadCount = productOrderRequests.size();
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        log.info("서버: 구매 요청 대기 중...");

        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        Queue<PurchaseResult> productOrderRequestResults = new ConcurrentLinkedQueue<>();
        for (String product : productOrderRequests) {
            executorService.submit(() -> {
                try {
                    PurchaseResult response = processPurchase(product);
                    productOrderRequestResults.add(response);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();

        log.info("모든 요청 처리 완료!");
        log.info("구매 요청 처리 결과 : ");
        productOrderRequestResults.forEach(result -> log.info(new Gson().toJson(result)));

        executorService.shutdown();
    }

    private static PurchaseResult processPurchase(String product) {
        boolean isAvailable = checkStockAvailability(product);
        return PurchaseResult.builder()
                .product(product)
                .status(isAvailable ? "success" : "failure")
                .message(product + (isAvailable ? " 구매 성공!" : " 재고 부족으로 구매 실패"))
                .build();
    }

    /**
     * CPU만 재고가 있음, 나머지는 재고 없음 재고 없어서 오래 걸림
     * @param product
     * @return
     */
    private static boolean checkStockAvailability(String product) {
        if ("CPU".equals(product)) {
            log.info("CPU 구매 처리 중... 0초 대기-{}", Thread.currentThread().getName());
            return true;
        }

        try {
            log.info("{} 구매 처리 중... 5초 대기-{}", product, Thread.currentThread().getName());
            Thread.sleep(5000);  // 5초 대기
        } catch (InterruptedException interruptedException) {
            interruptedException.printStackTrace();
        }
        return false;
    }

    @Setter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class PurchaseResult {
        private String product;
        private String status;
        private String message;
    }

}
15:45:42.833 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- 서버: 구매 요청 대기 중...
15:45:42.841 [pool-1-thread-1] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- CPU 구매 처리 중... 0초 대기-pool-1-thread-1
15:45:42.841 [pool-1-thread-2] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- RAM 구매 처리 중... 5초 대기-pool-1-thread-2
15:45:42.841 [pool-1-thread-3] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- Motherboard 구매 처리 중... 5초 대기-pool-1-thread-3
15:45:42.842 [pool-1-thread-5] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- HDD 구매 처리 중... 5초 대기-pool-1-thread-5
15:45:42.841 [pool-1-thread-4] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- SSD 구매 처리 중... 5초 대기-pool-1-thread-4
15:45:47.847 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- 서버: 모든 요청 처리 완료!
15:45:47.848 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- 구매 결과 : 
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"CPU","status":"success","message":"CPU 구매 성공!"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"RAM","status":"failure","message":"RAM 재고 부족으로 구매 실패"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"Motherboard","status":"failure","message":"Motherboard 재고 부족으로 구매 실패"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"HDD","status":"failure","message":"HDD 재고 부족으로 구매 실패"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"SSD","status":"failure","message":"SSD 재고 부족으로 구매 실패"}

이 밖에 쓰레드 협업용 동기화 도구

CyclicBarrier

  • 목적: 지정된 수의 스레드가 모두 도달할 때까지 기다린 후, 동시에 작업을 진행합니다.
  • CountDownLatch와 달리, 반복적으로 사용 가능.
  • 사용 사례:
    • 여러 스레드가 서로 독립적인 작업을 완료한 후, 다음 단계로 함께 진행해야 할 때.
    • 스레드풀에서 일정 개수의 스레드가 모였을 때 새로운 작업을 시작해야 하는 경우.
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("All threads reached the barrier. Proceeding...");
});

Runnable task = () -> {
    try {
        System.out.println(Thread.currentThread().getName() + " waiting at barrier");
        barrier.await();
        System.out.println(Thread.currentThread().getName() + " proceeding");
    } catch (Exception e) {
        e.printStackTrace();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
    executor.submit(task);
}
executor.shutdown();

Semaphore

  • 목적: 스레드의 접근을 제어하는 데 사용되며, 허용된 작업의 개수를 제한.
  • 스레드 협업에 사용할 수 있으며, 신호 용도로 활용 가능.
  • 사용 사례:
    • 리소스에 대한 동시 접근을 제한하고 싶을 때.
    • 일정한 스레드 수가 접근해야 작업을 완료하고 신호를 보낼 때.
Semaphore semaphore = new Semaphore(3); // 허용된 동시 작업 수

Runnable task = () -> {
    try {
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName() + " acquired permit");
        Thread.sleep(1000); // 작업 수행
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println(Thread.currentThread().getName() + " releasing permit");
        semaphore.release();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
    executor.submit(task);
}
executor.shutdown();

Phaser

  • 목적: CyclicBarrier와 유사하게 단계별 동기화를 제공하며, 단계마다 참여 스레드 수를 동적으로 조정할 수 있습니다.
  • CountDownLatch와 달리 단계를 명시적으로 관리하고, 재사용 가능.
  • 사용 사례:
    • 여러 단계로 나뉜 스레드 작업을 동기화해야 할 때.
    • 단계별로 스레드의 참여를 동적으로 변경할 수 있어야 할 때.
import java.util.concurrent.Phaser;

public class PhaserTest {

    /**
     * 3개의 작업을 3단계로 걸쳐서 함
     * @param args
     */
    public static void main(String[] args) {
        // Phaser 생성, 3개의 쓰레드를 등록
        Phaser phaser = new Phaser(3);

        // Runnable 작업 정의
        Runnable task = () -> {
            String threadName = Thread.currentThread().getName();

            // 1단계 작업
            System.out.println(threadName + " completed Phase 1");
            phaser.arriveAndAwaitAdvance(); // 1단계 완료 후 대기

            // 2단계 작업
            System.out.println(threadName + " completed Phase 2");
            phaser.arriveAndAwaitAdvance(); // 2단계 완료 후 대기

            // 3단계 작업
            System.out.println(threadName + " completed Phase 3");
            phaser.arriveAndAwaitAdvance(); // 3단계 완료 후 대기*/

            System.out.println(threadName + " finished all phases");
        };

        // 3개의 쓰레드 생성 및 실행
        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");
        Thread t3 = new Thread(task, "Thread-3");

        t1.start();
        t2.start();
        t3.start();
    }
}

Exchanger

  • 목적: 두 스레드가 데이터를 교환하는 데 사용.
  • 사용 사례:
    • 한 스레드가 데이터를 생성하고 다른 스레드가 이를 소비하는 생산자-소비자 패턴.
    • 두 스레드 간의 데이터 교환이 필요한 경우.
Exchanger<String> exchanger = new Exchanger<>();

Runnable producer = () -> {
    try {
        String data = "Data from producer";
        System.out.println("Producer: Sending data");
        String response = exchanger.exchange(data);
        System.out.println("Producer: Received response - " + response);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Runnable consumer = () -> {
    try {
        String data = exchanger.exchange(null);
        System.out.println("Consumer: Received data - " + data);
        exchanger.exchange("Acknowledgement from consumer");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(producer);
executor.submit(consumer);
executor.shutdown();

SynchronousQueue

  • 목적: 한 번에 하나의 작업만 전송하고 수신되도록 동기화된 대기열.
  • 생산자가 데이터를 큐에 넣으려면 소비자가 즉시 데이터를 받아야 함.
  • 사용 사례:
    • 스레드 간의 단방향 데이터 교환.
SynchronousQueue<String> queue = new SynchronousQueue<>();

Runnable producer = () -> {
    try {
        System.out.println("Producer: Sending data");
        queue.put("Data from producer");
        System.out.println("Producer: Data sent");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Runnable consumer = () -> {
    try {
        System.out.println("Consumer: Waiting for data");
        String data = queue.take();
        System.out.println("Consumer: Received data - " + data);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(producer);
executor.submit(consumer);
executor.shutdown();
728x90
반응형

toString을 항상 재정의하라

Object의 기본 toString 메서드가 우리가 작성한 클래스에 적합한 문자열을 반환하는 경우는 거의 없음

toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고, 그 클래스를 사용한 시스템은 디버깅하기 쉬음

실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋음

포맷을 명시하든 아니든 여러분의 의도는 명확히 밝혀야 함

class sample(){

    @Override public String toString(){
        return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
    }

}
class sample(){

    @Override public String toString(){
        ...
    }

}

포맷 명시 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자
PhoneNumber 클래스는 지역 코드, 프리픽스, 가입자 번호용 접근자를 제공해야함
그렇지 않으면 이 정보가 필요한 프로그래머는 toString의 반환값을 파싱할 수 밖에 없음

정적 유틸리티 클래스, 대부분의 열거 타입 이미 toString을 제공함

하지만 하위 클래스들이 공유해야 할 문자열 표현이 있는 추상 클래스라면 toString을 재정의해줘야 함
대다수의 컬렉션 구현체는 추상 컬렉션 클래스들의 toString 메서드를 상속해 씀

구글의 AutoValue 프레임워크는 toString도 생성해줌

정리

모든 구체 클래스에서 Object의 toString을 재정의하자.

toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야함

728x90
반응형

equals를 재정의하려거든 hashCode도 재정의하라

hashCode 일반 규약을 어기에 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제

논리적으로 같은 객체는 같은 해시코드를 반환해야함

class example() {

    public static void main(String[] args) {
        Map<PhoneNumber, String> m = new HashMap<>();
        m.put(new PhoneNumber(707, 867, 5309), "제니");    
    }

}

위 코드에서 m.get(new PhoneNumber(707, 867, 5309))를 실행하면 "제니"가 나와야함
하지만, null을 반환함

PhoneNumber 클래스는 hashCode를 재정의하지 않았기 때문에 논리적 동치인 두 객체가
서로 다른 해시코드를 반환하여 두번째 규약을 지키지 못함

백기선님 강의 example

https://github.com/jshag90/effective-java-study/blob/main/src/main/java/com/ji/effective/java/chapter2/item11/hashtable/HashMapTest.java

최악의 (하지만 적법한) hashCode 구헌 - 사용 금지!

class sample(){

    @Override public int hashCode(){return 42;}
}

위와 같이 정의하면 모든 객체에서 똑같은 해시코드를 반환
해시테이블의 버킷 하나에 담겨 마치 연결 리스트 처럼 동작한다.

그 결과 평균 수행 시간이 O(1)인 해시테이블이 O(n)으로 느려져서 객체가 많아지면 도저기 쓸수 없게 된다.

곱한 숫자를 31로 정한 이유는 31이 홀수이면서 소수(prime)이기 때문임
소수를 곱하는 이유는 명확하지 않지만 정통적으로 그리해왔다.
결과적으로 31을 이용하면, 이 곱셈을 시프트 연산과 뺄셈으로 대체해 최적화할 수 있음
요즘 VM들은 이런 최적화를 자동으로 해줌

31이라는 숫자를 사용했을 때 가장 해시 충돌이 적었다고함

전형적인 hashCode 메서드

class sample(){
    @Override public int hashCode(){
        int result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        return result;
    }
}

한 줄짜리 hashCode 메서드 - 성능이 살짝 아쉽다.

class sample(){
    @Override public int hashCode(){
        return Object.hash(lineNum, prefix, areaCode);
    }
}

hash 메서드는 성능에 민감하지 않은 상황에서만 사용하자.

클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기보다는 캐싱하는 방식으로 고려해야함

해시의 키로 사용되지 않는 경우라면 hashCode가 처음 불릴 때 계산하는 지연 초기화(lazy initialization) 전략을 고려
(생성자에서 미리 계산하는게 아니라 hashCode()가 호출 되었을 때 값을 초기화)

해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 함

class Sample(){
    private int hashCode; // 자동으로 0으로 초기화됨

    @Override public int hashCode(){
        int result = hashCode;
        if(result == 0){
            int result = Short.hashCode(areaCode);
            result = 31 * result + Short.hashCode(prefix);
            result = 31 * result + Short.hashCode(lineNum);
            hashCode = result;
        }

        return result;
    }

}

성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안됨
특히 어떤 필드는 특정 영역에 몰른 인스턴스들의 해시코드를 넓은 범위로 고르게 퍼트려주는 효과가 있을 수 있음
핵심 필드가 없다면 수많은 인스턴스가 단 몇 개의 해시코드로 집중되어 해시테이블의 속도가 선형으로 느려짐

정리

equals를 재정의할 때는 hashCode도 반드시 재정의 해야함

hashCode는 Object의 API 문서에 기술된 일반 규약을 따라야 하며,
서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 규현해야함

AutoValue 프레임워크를 사용하면 멋진 equals와 hashCode를 자동으로 만들어줌

추가 정리

스레드 안전

https://github.com/jshag90/effective-java-study/blob/main/src/main/java/com/ji/effective/java/chapter2/item11/hashcode/PhoneNumber.java

 

728x90

+ Recent posts