728x90
반응형
728x90
반응형
반응형

환경: jdk6, bouncy castle 설치됨, https 통신 필요

https통신 시도 시 아래와 같은 에러 발생

원인

  • JDK 6 환경에서 TLS 1.2 통신을 시도
    • 기본적으로 JDK 6은 TLS 1.2를 지원하지 않음
    • 하지만 Bouncy Castle을 설치하여 강제로 TLS 1.2를 활성화함
  • TLS Handshake 실패
    • 서버가 제공한 인증서를 검증할 때, JDK 6의 TrustStore (신뢰할 수 있는 루트 인증서 저장소)에 해당 루트 인증서가 없었음
    • 즉, 신뢰할 수 있는 CA(인증 기관)의 인증서가 누락되어 있어 통신 실패

해결

  • 루트 인증서를 설치하고 JDK 6 KeyStore에 추가
    • keytool 명령어를 사용하여 루트 인증서를 JDK 6의 TrustStore에 등록 
    • cacerts는 JDK의 기본 TrustStore ($JAVA_HOME/jre/lib/security/cacerts)
keytool -import -trustcacerts -keystore cacerts -storepass changeit -file rootCA.crt -alias myrootca
  • 다시 TLS 1.2 통신을 시도하여 성공
    • 루트 인증서가 추가되었으므로 서버 인증서 검증이 정상적으로 완료됨
    • 최종적으로 TLS 1.2 통신이 정상적으로 수행됨

 

728x90
반응형
반응형

타임셰어링(Time Sharing)과 컨텍스트 스위칭(Context Switching)은 밀접한 관계가 있지만 다른 개념이라 하여 정리해본다.

 

타임셰어링(Time Sharing)

(OS가 관리하는) CPU 시간을 여러 프로세스 또는 스레드에 분배하는 방식

  • 목적: 여러 작업을 동시에 실행하는 것처럼 보이게 함 (멀티태스킹)
  • 방법: 일정한 시간 간격(타임 슬라이스, Time Slice)마다 CPU를 다른 프로세스/스레드에 할당
    • 모든 프로세스가 공평하게 CPU 시간을 나누어 사용(선점 없음)
  • 결과: 사용자는 여러 작업이 동시에 실행되는 것처럼 느끼지만, 실제로는 CPU가 빠르게 번갈아가며 실행하는 것

 

컨텍스트 스위칭(Context Switching)

CPU가 실행 중인 프로세스 또는 스레드를 변경할 때, 현재 상태(Context)를 저장하고 새로운 프로세스 또는 스레드의 상태를 복원하는 과정

  • 목적: 여러 프로세스를 실행하기 위해 이전 실행 상태를 저장하고 새로운 작업을 로드
  • 필요한 이유: OS가 프로세스나 스레드를 교체할 때 이전 작업을 나중에 다시 실행할 수 있도록 하기 위해
  • 오버헤드 발생: 컨텍스트를 저장하고 복원하는 작업은 추가적인 CPU 자원을 소모

컨텍스트 스위칭 과정

  1. 현재 실행 중인 프로세스의 레지스터, 메모리 상태(Context) 저장
  2. 새로운 프로세스의 레지스터, 메모리 상태 복원
  3. CPU가 새로운 프로세스를 실행

즉, 타임셰어링을 하려면 필연적으로 컨텍스트 스위칭이 발생함!


타임셰어링은 CPU 시간을 나누어 여러 프로세스가 실행되도록 하는 방식
타임셰어링을 수행하려면 컨텍스트 스위칭이 발생해야 함
하지만 컨텍스트 스위칭은 타임셰어링이 아닐 수도 있음

  • 예) 우선순위 기반 선점형(Preemptive) 스케줄링에서도 컨텍스트 스위칭 발생
    • 높은 우선순위의 프로세스가 CPU를 빼앗아 사용할 수 있음(선점 가능)

즉, 타임셰어링은 CPU 시간을 나누는 방식이고, 컨텍스트 스위칭은 그 과정에서 발생하는 기술적인 동작!

728x90
반응형
반응형

Call by Value vs Call by Reference

  • Call by Value: 인자로 전달된 변수의 만 복사하여 사용. 원본 변수에는 영향을 주지 않는다.
  • Call by Reference: 인자로 전달된 변수의 메모리 주소(참조값, reference)를 전달. 함수 내에서 값이 변경되면 원본 변수에도 영향을 미친다.

자바는 Call by Value

자바의 메서드 호출 방식은 항상 Call by Value다. 즉, 메서드가 인자를 받을 때 원본 변수의 값을 복사하여 전달한다.

기본 타입 (Primitive Type)

기본 타입(예: int, double, char 등)은 Call by Value로 동작하여 원본 변수에 영향을 주지 않는다. 

참조 타입 (Reference Type)

객체(Object)와 같은 참조 타입의 경우, 객체의 참조값(메모리 주소)이 값으로 전달된다. 따라서 참조하는 객체의 속성을 변경하면 원본 객체에도 영향을 준다. 하지만 참조 자체를 변경해도(새로운 객체를 할당하면) 원본에는 영향을 주지 않는다.

class Person {
    String name;
}

public class Example {
    public static void changePerson(Person p) {
        p = new Person(); // 새로운 객체를 할당
        p.name = "Charlie";
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.name = "Bob";

        changePerson(person);
        System.out.println(person.name); // 여전히 Bob (새로운 객체는 원본에 영향을 주지 않음)
    }
}

 

changePerson 함수 안에서 p가 새로운 객체를 가리키도록 변경되었지만, 이것은 메서드 내부의 p 변수가 가리키는 참조가 바뀐 것일 뿐, 원래 person 변수에는 영향을 주지 않는다. 만약 메서드 내부에서 새로운 객체를 만들어 원본에도 반영하고 싶다면, 리턴 값을 활용하여 원본 변수를 직접 변경해야 한다.

public static Person changePerson(Person p) {
    return new Person("New Person");
}

public static void main(String[] args) {
    Person person = new Person("Original");
    person = changePerson(person); // 리턴 값을 원본 변수에 할당
    System.out.println(person.name); // "New Person"
}

객체의 참조값(메모리 주소)을 복사하여 전달하기 때문에, 객체 내부 값은 변경할 수 있지만, 객체 자체를 변경할 수는 없다.

 

즉, Java는 "Call by Value of Reference"

728x90
반응형
반응형

환경: 자바8+

 

SimpleDateFormat은 멀티스레드 환경에서 안전하지 않다.

SimpleDateFormat은 내부적으로 Calendar 인스턴스를 공유하는데, 이 과정에서 공유 자원 변경이 발생하여 멀티스레드 환경에서 예상치 못한 결과를 초래할 수 있다. SimpleDateFormat은 멀티스레드 환경에서 사용할 경우 각 스레드마다 별도 인스턴스를 생성하거나 ThreadLocal을 이용해야 한다.

 

Java 8부터는 DateTimeFormatter가 제공되며, 이는 불변(immutable) 객체이므로 여러 스레드에서 동시에 안전하게 사용할 수 있다. 따라서 매번 새로 생성할 필요 없이, 재사용하는 것이 성능적으로도 더 유리하다.

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateTimeFormatterExample {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        Runnable task = () -> {
            String formattedDate = LocalDateTime.now().format(FORMATTER);
            System.out.println(Thread.currentThread().getName() + " : " + formattedDate);
        };

        for (int i = 0; i < 5; i++) {
            new Thread(task).start();
        }
    }
}
728x90
반응형
반응형

환경: springframework 4.2, java8

 

자바와 레디스를 연결하기 위한 레디스 클라이언트는 크게 3개가 있다.

1. Jedis

특징

  • 초기 Redis Java 클라이언트 중 하나로, 간단하고 직관적인 API 제공.
  • Redis 명령어 대부분을 지원하며, 설정이 간단함.
  • 싱글스레드 기반으로 설계됨.
  • Jedis는 Redis 클라이언트를 위한 핵심적인 기능만 포함하며, 추가적인 추상화나 고급 기능(분산 락, 세마포어 등)은 제공하지 않음
  • 다른 클라이언트(예: Redisson)에 비해 의존성이 적고, 실행 크기가 작음, 가벼움

장점

  • 사용법이 단순하고 직관적이어서 빠르게 배울 수 있음.
  • Redis와의 네이티브 한 연동 및 모든 명령어를 제공.
  • 간단한 애플리케이션에서 적합.

단점

  • Thread-Safe 하지 않음: 멀티스레드 환경에서 사용하려면 JedisPool을 사용해야 함.
  • 싱글스레드 기반이라 멀티스레드 환경에서는 성능이 저하될 수 있음.

추천 사용 시나리오

  • 단일 스레드 기반 애플리케이션.
  • Redis를 단순 캐싱 또는 데이터 저장소로 사용하는 경우.

2. Lettuce

특징

  • 비동기 및 동기 API를 모두 제공하며, Reactive Streams(Flux/Mono)도 지원.
  • 기본적으로 비동기적으로 실행되고, 결과는 CompletionStage 또는 Future를 통해 반환됨
  • Netty를 기반으로 한 non blocking I/O 모델 사용.
  • Thread-Safe: 단일 커넥션을 여러 스레드에서 공유 가능.

장점

  • 멀티스레드 환경에서 효율적이며 Thread-Safe.
  • 비동기 작업에 유리하며 고성능 제공.
  • Reactive 프로그래밍 환경과의 호환성이 뛰어남.
  • 클러스터와 Sentinel 환경 지원.

단점

  • API가 Jedis보다 다소 복잡할 수 있음.
  • 초심자에게는 학습 곡선이 약간 높음.

추천 사용 시나리오

  • 비동기 작업이 많은 고성능 애플리케이션.
  • 멀티스레드 환경.
  • 클러스터 또는 Sentinel 기반 Redis 설정.
  • Reactive 프로그래밍(Spring WebFlux 등)을 사용하는 경우.

3. Redisson

특징

  • Redis를 기반으로 한 고급 분산 기능 제공(분산 락, 분산 캐시 등).
  • Redis를 Java의 분산 데이터 구조와 유사하게 다룰 수 있는 API 제공.
  • Thread-Safe 하며, 다양한 고급 기능이 포함됨.

장점

  • 분산 락, 분산 세마포어, RMap, RList 등 고급 데이터 구조 지원.
  • 클러스터, Sentinel, 레플리카 환경에서 유연하게 동작.
  • Redis 클라이언트를 단순한 캐싱 도구 이상으로 활용 가능.

단점

  • 다른 클라이언트보다 무겁고 약간의 추가 오버헤드 발생.
  • Jedis나 Lettuce에 비해 더 많은 메모리 사용 가능.

추천 사용 시나리오

  • 분산 락, 세마포어, 큐와 같은 고급 분산 기능이 필요한 환경.
  • Redis를 데이터베이스 이상의 목적으로 사용하는 경우.
  • 복잡한 클러스터 환경.

 

jedis 사용

<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.8.4.RELEASE</version>
</dependency>

<!-- Jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
 public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    log.debug("redisKey: {}, ttl: {}", redisKey, lockSecond);
    try {
      acquireLock(redisKey, lockSecond);
      return logic.get();
    } finally {
      releaseLock(redisKey);
    }
  }

  private void acquireLock(String redisKey, long lockSecond) {
    final RedisCallback<Boolean> redisCallback = connection -> {
      byte[] key = redisTemplate.getStringSerializer().serialize(redisKey);
      byte[] value = redisTemplate.getStringSerializer().serialize("locked");
      return connection.setNX(key, value) && connection.expire(key, lockSecond);
    };

    boolean success = redisTemplate.execute(redisCallback);
    if (!success) {
      throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
    }
  }

  private void releaseLock(String redisKey) {
    redisTemplate.delete(redisKey);
  }

위처럼 분산락을 jedis를 사용하여 개발하면 락을 얻고 ttl을 주고 해제하는 과정을 수동으로 개발해야 한다.

redisson으로 수정

redisson을 사용하면 아래와 같이 라이브러리에서 제공하는 기능을 사용하여 간소화하게 짤 수 있다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    RLock lock = redissonClient.getLock(redisKey); // Redis 분산 락 생성
    log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

    try {
        // 락을 시도하고 성공할 경우 논리 실행
        if (lock.tryLock(0, lockSecond, TimeUnit.SECONDS)) { 
            return logic.get();
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new ApiLockException("락을 획득하는 동안 인터럽트가 발생했습니다. key: " + redisKey, e);
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock(); // 락 해제
            log.debug("Lock released for redisKey: {}", redisKey);
        }
    }
}

lettuce...?

호옥시 lettuce로 바꾸면 어떻게 될까 궁금해서 chat gpt에게 물어봤다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisCommands<String, String> commands = connection.sync();
        log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

        if (acquireLock(commands, redisKey, lockSecond)) {
            try {
                return logic.get();
            } finally {
                releaseLock(commands, redisKey);
            }
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    }
}

private boolean acquireLock(RedisCommands<String, String> commands, String redisKey, long lockSecond) {
    String result = commands.set(redisKey, "locked", io.lettuce.core.SetArgs.Builder.nx().ex(lockSecond));
    return "OK".equals(result); // Redis SET NX PX 결과 확인
}

private void releaseLock(RedisCommands<String, String> commands, String redisKey) {
    commands.del(redisKey); // 락 해제
    log.debug("Lock released for redisKey: {}", redisKey);
}

lettuce는 동기 동작을 위해 .sync()를 꼭 넣어야 하고(비동기는 .async()) 나머지는 jedis와 비슷하게 직접(setnx) 락을 설정하고 ttl도 설정해야 한다.

그리고 lettuce의 StatefulRedisConnection를 사용하는 경우 close 해줘야 한다. AutoCloseable를 implement 했기 때문에 try-with-resources 사용 가능하다.

 

  • 연결 풀을 사용할 경우에는, 연결이 자동으로 풀로 반환되며 그 자체로 명시적인 종료가 필요하지 않지만, StatefulRedisConnection은 연결 풀을 사용하지 않고, 각 연결을 명시적으로 관리하는 방식.
  • 연결 풀을 사용하면 연결을 가져오고 반환하는 방식으로 관리되기 때문에, 풀에서 자동으로 연결을 회수하고 재사용할 수. 하지만 StatefulRedisConnection은 풀을 사용하지 않기 때문에 사용자가 직접 연결을 닫고 관리해야 합니다.

 

shutdown을 수동으로 해야하나..?

참고로 레디스 연결 객체인 RedisClient와 RedisConnectionFactory는 프로그램 종료 시 shutdown이 되어야 하는데, 빈으로 등록되어 있는 경우 Spring Boot에서는 자동으로 연결 종료가 처리된다. 이렇게 되면 명시적인 shutdown() 호출은 필요하지 않는다. Spring Boot는 빈 관리 및 라이프사이클을 자동으로 처리하기 때문에, 애플리케이션 종료 시 리소스를 자동으로 정리한다.

하지만 Spring Framework에서는 명시적인 shutdown() 호출이 필요할 수 있다. 왜냐하면 Spring Framework에서는 자동으로 리소스를 관리하는 기능이 내장되어 있지 않기 때문...?

  • hmmmmmm?? 진짠지 모르겠음.. 레거시에서도 수동으로 shutdown 한건 본적이 없음

 


분산락을 위해 보통 @AOP로 만들 수도 있지만 위와 같은 고차함수 방식을 더 선호한다.

아래와 같은 단점이 있기에..

  • public에만 사용가능
  • 값 넘길 때 함수 argument 첫번째 값 주의 필요
  • @Around( "@within(com.annotation.. <-- 와 같이 문자열로 관리하는 것에 대한 부담
  • 락의 범위가 넓어질 수 있음

 

고차함수 분산락을 사용할 때는 아래와 같이 하면 된다.

final String redisKey = redisKey("invenChangeExpire", String.valueOf(sno));

return this.apiLockService.lockProcess(redisKey, () -> {
	... 로직
    });
728x90
반응형
반응형

환경: java17

 

자바11부터 로컬 변수에 대한 타입 추론이 가능해졌다. 즉 아래와 같이 구체적인 타입을 선언하지 않고 var로 선언이 가능해졌다.

var items = (List<SinyutnoriRanking>) chunk.getItems();

그리고 자바 17을 사용중이다. 그런데 아래와 같은 경고창을 만난다.

Unchecked cast: 'java.util.List<capture<? extends xx.Ranking>>' to 'java.util.List<xx.Ranking>'

위 경고는 컴파일러가 타입 추론을 못해서 발생하는데, 자바 17이면 당연히 자바 11의 내용을 알고 있기에 컴파일러 단에서 타입추론이 되는거 아니야? 하는 생각이 들었다.

제네릭과 타입 추론의 한계

Java 11에서 var를 도입하면서 로컬 변수의 타입 추론이 가능해졌지만, 제네릭 타입의 안전성을 보장하기 위해 여전히 unchecked 경고가 발생할 수 있다.

  • 제네릭 타입을 사용할 때, Java 컴파일러는 타입 소거(Type Erasure)를 사용하여 런타임에 타입 정보를 제거한다. 이로 인해, 컴파일러가 타입 안전성을 완전히 보장할 수 없는 경우 경고를 발생시킨다.
  • 이런 경우 var를 사용하더라도 타입 캐스팅이 명시적이든 암시적이든 타입 안전성을 보장할 수 없으므로 @SuppressWarnings("unchecked")가 필요하다.
List<String> list = (List<String>) new ArrayList(); // Unchecked cast warning

var list = (List<String>) new ArrayList(); // 경고 발생

 

@SuppressWarnings("unchecked")

타입 안정성을 보장할 수 없는 상황에서 컴파일러 경고를 무시하도록 명시적으로 @SuppressWarnings("unchecked")를 추가한다. 이는 컴파일러에게 "나는 이 경고를 알고 있으며, 이 코드가 안전하다는 것을 보장할 수 있다"라는 신호를 주는 것이다.

 

참고로 최신 버전의 IntelliJ IDEA나 다른 IDE를 사용해도, 컴파일러 경고 자체는 여전히 발생할 수 있다는 점..!

728x90
반응형
반응형

2024.09.21 - [개발/java] - [java8+] 함수형 프로그래밍 @FunctionalInterface

 

일급 객체 (First-Class Object)

  1. 변수에 할당할 수 있다: 함수를 변수에 할당할 수 있다.
  2. 인자로 전달할 수 있다: 함수를 다른 함수의 인자로 전달할 수 있다.
  3. 반환값으로 사용할 수 있다: 함수를 다른 함수의 반환값으로 사용할 수 있다.
import java.util.function.Function;

public class FirstClassObjectExample {
    public static void main(String[] args) {
        // 함수가 변수에 할당됨
        Function<String, String> greet = name -> "Hello, " + name + "!";

        // 함수가 매개변수로 전달됨
        sayHello(greet, "Alice");

        // 함수가 반환 값으로 사용됨
        Function<String, String> greetFn = getGreetFunction();
        System.out.println(greetFn.apply("Bob"));
    }

    public static void sayHello(Function<String, String> fn, String name) {
        System.out.println(fn.apply(name));
    }

    public static Function<String, String> getGreetFunction() {
        return name -> "Hi, " + name + "!";
    }
}

고차 함수 (Higher-Order Function)

고차 함수는 다음 중 하나 이상의 조건을 만족하는 함수:

  1. 다른 함수를 인자로 받을 수 있다.
  2. 다른 함수를 반환할 수 있다.

Java 8의 Stream API는 함수형 프로그래밍의 개념을 적극 활용한다. 메서드 체이닝을 통해 고차 함수의 형태로 map, filter, reduce 등의 연산을 수행할 수 있다.

Java에서 콜백을 처리할 때 고차 함수가 자주 사용된다. 특정 작업이 완료되었을 때 실행할 동작을 함수로 전달할 수 있다.

import java.util.function.Consumer;
import java.util.function.Function;

public class HigherOrderFunctionExample {
    public static void main(String[] args) {
        // 함수를 매개변수로 받는 고차 함수
        repeat(3, i -> System.out.println("Hello, " + i));

        // 함수를 반환하는 고차 함수
        Function<Integer, Integer> doubleFn = createMultiplier(2);
        System.out.println(doubleFn.apply(5));  // 10
    }

    public static void repeat(int n, Consumer<Integer> action) {
        for (int i = 0; i < n; i++) {
            action.accept(i);
        }
    }

    public static Function<Integer, Integer> createMultiplier(int multiplier) {
        return value -> value * multiplier;
    }
}

용어가 비슷해서 헷갈리지만 다른....(객체 지향 관련 개념)

일급 컬렉션 (First-class Collection)

일급 컬렉션은 컬렉션을 직접 사용하지 않고 컬렉션을 래핑하는 클래스를 만들어, 해당 클래스를 통해서만 컬렉션을 조작하도록 하는 디자인 패턴이다. 이를 통해 코드의 명확성과 유지 보수성을 높이고, 불변성을 보장할 수 있다.

  1. 불변성 보장: 일급 컬렉션 클래스는 컬렉션에 대한 직접적인 접근을 방지하고, 불변성을 유지하도록 도와준다.
  2. 비즈니스 로직 캡슐화: 컬렉션에 대한 비즈니스 로직을 일급 컬렉션 클래스 내부에 캡슐화하여 코드의 응집도를 높임.
  3. 컬렉션 관련 메서드 제공: 컬렉션을 조작하기 위한 메서드들을 일급 컬렉션 클래스에서 제공하여, 코드의 명확성을 높임.
  4. 특정 타입의 컬렉션 강제: 일급 컬렉션은 특정 타입의 컬렉션만을 다루도록 강제할 수 있다.
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

public class Products {
    private final List<Product> products;

    public Products(List<Product> products) {
        this.products = new ArrayList<>(products);
    }

    // 불변성을 유지하기 위해 컬렉션 반환 시 복사본 제공
    public List<Product> getProducts() {
        return Collections.unmodifiableList(products);
    }

    // 전체 가격 계산 같은 비즈니스 로직 캡슐화
    public double totalPrice() {
        return products.stream()
                       .mapToDouble(Product::getPrice)
                       .sum();
    }

    // 제품 추가 메서드
    public Products addProduct(Product product) {
        List<Product> newProducts = new ArrayList<>(products);
        newProducts.add(product);
        return new Products(newProducts);
    }
}
728x90
반응형
반응형

코루틴 - 코틀린; 비동기

코루틴(Coroutine)은 비동기 프로그래밍동시성 처리를 위한 경량 실행 단위/함수
일반적으로 코루틴은 실행을 일시 중단하고(중단점 제공), 필요한 시점에 다시 시작할 수 있는 기능을 가지고 있음

  • 핵심 아이디어: 사용자 수준의 스케줄링
    • 코루틴은 명시적인 중단 지점을 통해 비동기 작업을 관리
    • 단일 스레드에서도 여러 코루틴을 실행할 수 있음
  • 동작 방식:
    • 비동기: 코루틴은 주로 비동기 작업을 처리하는 데 사용. 코루틴은 일시 중단과 재개가 가능하여, 비동기 네트워크 호출이나 파일 I/O 작업을 쉽게 처리할 수 있음
    • 런타임이 코루틴의 상태를 관리하고, 필요 시 다시 스케줄링
    • 예: suspend 함수 호출 시 작업을 중단하고 다른 작업을 실행

장점:

  • 명시적인 상태 관리로 복잡한 비동기 로직 처리에 강력.
  • 메모리 및 리소스 효율성이 높음.

단점:

  • 프로그래머가 중단 지점을 명시적으로 관리해야
  • 자바에서는 안됨(Kotlin에서 주로 사용)

코루틴의 특징

  1. 경량 스레드:
    • 코루틴은 스레드와 유사하게 보이지만, 실제 스레드를 생성하지 않고 실행되므로 더 적은 리소스를 사용함
    • 코루틴은 스레드보다 가벼운 구조로, 많은 수의 코루틴을 동시에 실행할 수 있어 메모리 사용량을 줄이고 성능을 향상시킴
  2. 비동기 작업 처리:
    • await 또는 yield 같은 키워드를 통해 작업의 흐름을 중단하고, 나중에 재개할 수 있음
    • 코루틴은 비동기적으로 실행되며, 다른 작업과 동시에 진행될 수 있어 CPU 자원을 효율적으로 사용할 수 있게 해줌
  3. 스케줄링 제어:
    • 코루틴은 프로그래머가 명시적으로 실행 순서를 제어할 수 있음
  4. 언제든 중단/재개 가능:
    • 작업의 중간에서 멈췄다가 나중에 다시 이어서 실행할 수 있어 효율적인 비동기 작업 처리가 가능
    • 코루틴은 실행 상태를 유지할 수 있어, 중단된 지점에서 다시 시작할 수 있음

언제 코루틴을 사용하나?

  1. I/O 작업:
    • 네트워크 요청, 파일 읽기/쓰기 등 시간이 오래 걸리는 작업에서 UI 스레드나 메인 스레드를 차단하지 않고 비동기적으로 처리.
  2. 동시성 프로그래밍:
    • 여러 작업을 병렬로 처리할 때 스레드보다 더 효율적으로 관리 가능.
  3. UI 프로그래밍:
    • 애니메이션, 이벤트 처리, 사용자 인터페이스 업데이트 등 비동기 작업을 자연스럽게 구현.
  4. 백그라운드 작업:
    • CPU 집약적인 작업이나 긴 대기 시간이 필요한 작업을 수행하면서 메인 스레드를 차단하지 않음.

 

코루틴 자바 지원 X

자바의 virtual thread랑 비교?

코루틴은 비동기 작업을 간편하게 처리하기 위해 설계된 반면, 버추얼 스레드는 높은 동시성을 요구하는 환경에서 효율적으로 스레드를 관리하기 위한 방법

  • 코루틴: 비동기 작업을 중단하고 재개할 수 있는 경량 구성 요소로, 비동기 작업 처리가 주 용도
  • 버추얼 스레드: 동기적인 프로그래밍 모델을 유지하면서도 높은 동시성을 처리할 수 있는 경량 스레드로, 비동기 코드의 복잡성을 줄임

 

버추얼 스레드 - 자바; 대규모동시성동기

  • 핵심 아이디어: JVM이 직접 관리
    • 전통적인 스레드는 OS 커널에서 관리되지만, 버추얼 스레드는 JVM 내부에서 관리되어 더 적은 자원을 소비
    • 각 작업은 자체적인 스레드처럼 동작하므로 프로그래머가 명시적으로 중단점을 관리할 필요가 없음
    • 버추얼 스레드는 동기적으로 작업을 수행. 전통적인 스레드와 유사한 방식으로 작동하지만, 더 가볍고 효율적
  • 동작 방식:
    • 동기적 코드 작성: 버추얼 스레드는 비동기 작업을 동기적인 코드 스타일로 작성할 수 있게 한다. 이는 비동기 코드의 복잡성을 줄이고, 동기적인 프로그래밍 모델을 유지. (동기 방식도 대규모 동시성 지원 가능! 많은 스레드를 동시에 처리 가능)
    • 차단 호출(예: I/O 작업) 발생 시 자동으로 OS 스레드에서 분리
      • IO 대기 상태에서는 스레드 반환해 대규모 작업 가능
    • 기존 스레드 풀보다 더 많은 수의 동시 작업 가능

장점:

  • 기존 스레드 API와 호환성 높음 (학습 곡선 낮음).
    • synchronized 블록, wait/notify 메서드 등을 그대로 사용 가능
  • 차단 호출도 자동으로 처리하므로 더 간단한 코드 작성 가능.

단점:

  • 자바 19 이상의 JVM에서만 사용 가능.
  • 특정 시나리오에서는 기존 스레드 풀만큼의 효율성 제공 어려움.

전통적인 스레드와의 비교

  • 전통적인 스레드:
    • OS에서 관리되며, 각각의 스레드는 상당한 메모리와 자원을 차지
    • 많은 수의 스레드를 생성하면 성능 문제가 발생할 수
  • 버추얼 스레드:
    • JVM에서 관리되며, 매우 가벼움
    • 대량의 동시 작업을 처리할 때 효율적
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> {
            Thread.sleep(1000);
            return "Task completed!";
        });
    }
}

위 코드에서 10,000개의 Virtual Thread를 동기적으로 처리하지만, 비동기를 사용하지 않아도 효율적으로 실행됨

 

정리

 

2024.11.26 - [개발/java] - [병렬처리] Folkjoinpool & executorService

 

대규모 처리를 동시에 하려면 비동기 작업이 필수 아닌가?

 

그럼 비동기는 언제?

IO작업이 많을 때, 대기 시간을 최소화하고 자원을 효율적으로 활용할 수 있을 때

비동기는 주로 대기 시간이 많은 작업에서 활용됩니다. 이 대기 시간은 CPU가 직접 연산하는 시간이 아니라, 외부 시스템에서 응답을 기다리는 시간으로 인해 발생합니다. 비동기를 선택하는 주요 이유는 자원 효율성대규모 처리 능력입니다.

DB, file, API, 네트워크..

대규모 데이터 처리(ETL, 스트리밍)

작업 성격에 따른 선택

 

  • I/O 바운드 작업:
    대기 시간이 많은 작업에서 비동기를 사용하면 자원 활용이 극대화됩니다.
    • :
      • 외부 API 호출
      • 데이터베이스 쿼리
      • 파일 읽기/쓰기
      • 네트워크 요청
    • 추천 방식:
      • 비동기 프로그래밍: CompletableFuture, Kotlin Coroutines, async/await.
      • Reactive Streams: Project Reactor, RxJava.
      • Event-driven frameworks: Node.js.
  • CPU 바운드 작업:
    복잡한 계산, 데이터 변환 등 CPU가 주로 사용되는 작업에서는 동기나 멀티스레드 기반의 병렬 처리가 적합합니다.
    • :
      • 데이터 처리(대규모 연산)
      • 이미지 변환, 암호화
      • 머신 러닝 모델 실행
    • 추천 방식:
      • 동기 처리: 단일 스레드에서 작업.
      • 멀티스레드 기반 병렬 처리: Java ForkJoinPool, Parallel Streams, ExecutorService.

 

 

  • 멀티스레드는 작업을 물리적으로 병렬로 실행하는 방식이고,
  • 비동기는 작업을 논리적으로 대기 시간을 줄이며 논블로킹으로 처리하는 방식입니다.
  • 비동기는 멀티스레드를 사용하지 않을 수도 있지만, 필요하면 내부적으로 멀티스레드를 활용할 수도 있습니다. (예: Java NIO, Kotlin Coroutines).

성능 비교

https://tech.kakaopay.com/post/coroutine_virtual_thread_wayne/

728x90
반응형
반응형

ForkJoinPool은 Java 7에 도입된 병렬 처리 프레임워크로, 작업을 작은 단위로 분할(fork)하고 병렬로 처리한 후 다시 합치는(join) 방식으로 동작한다. 병렬 프로그래밍작업 스케줄링을 위한 강력한 도구로, 특히 대규모 데이터 처리나 계산 집약적인 작업에 유용하다.

이게 프레임워크?

  • 고수준의 작업 관리: ForkJoinPool은 작업 스케줄링, 워크 스틸링, 병렬 처리 등을 관리하는 메커니즘을 제공한다. 개발자가 세부적인 스레드 관리나 큐 처리 등을 직접 코딩하지 않아도 된다.
    • 제어 역전 (IoC, Inversion of Control): 작업 실행과 스레드 관리는 ForkJoinPool이 수행하며, 개발자는 작업의 논리만 작성
  • 작업 분할 및 병합 전략: RecursiveTaskRecursiveAction이라는 추상 클래스를 기반으로 작업을 설계하며, 내부적으로는 효율적인 작업 분할 및 병합을 자동으로 처리한다.
  • 워크 스틸링 (Work Stealing): 스레드 풀에서 작업 큐를 관리하며, 비활성 스레드가 다른 활성 스레드의 큐에서 작업을 가져와 실행하는 동적 작업 분배를 한다.(처리량을 최적화); 개발자가 구현할 필요 없이 forkjoinpool이 자동으로 처리
  • 표준화된 인터페이스: 개발자가 사용할 수 있는 명확한 API (invoke, submit, fork, join 등)를 제공. 이로 인해 복잡한 병렬 프로그래밍을 간단히 구현할 수 있음.

장점

  • 멀티코어를 활용하여 작업을 병렬로 처리하므로 CPU 사용률이 최적화
  • 워크 스틸링을 통해 비효율적인 작업 분배를 방지

단점

  • 작업 분할 및 병합에 대한 오버헤드가 존재
  • I/O 중심 작업에서는 비효율적이며, CPU 집약적인 작업에 적합

적합한 상황

  • 데이터가 많고, 병렬로 처리할 수 있는 작업
  • 재귀작업/반복적으로 작업을 나눌 수 있을 때 (예: 합계, 정렬)
  • CPU 집약적인 작업에서 최적의 성능을 얻고자 할 때

개발 시 전체적인 흐름

  1. 큰 작업이면 Fork하여 병렬 처리.
  2. 작은 작업이면 직접 계산으로 효율적 처리.
  3. 모든 계산이 끝나면 병렬 결과를 Join하여 최종 결과를 얻음.

작은 작업은 직접 계산하는 이유

  1. 작업 분할의 비용 문제:
    • Fork/Join Framework는 큰 작업을 작은 작업으로 나누고 각 작업을 병렬적으로 실행
    • 하지만 작업을 너무 많이 나누면 작업 분할과 작업 병합(merge)에 드는 오버헤드(비용)가 커질 수 있음
    • 작은 작업에 대해서는 작업 분할을 하지 않고 직접 계산하여 오버헤드를 줄임
  2. 효율성 최적화:
    • 일정 크기 이하의 작업은 더 이상 병렬로 처리할 필요가 없으므로 직접 계산이 더 효율적
    • 예를 들어, 배열의 일부를 합산하거나 특정 범위의 숫자를 더하는 간단한 작업이라면, 병렬처리 대신 반복문을 통해 순차적으로 계산하는 것이 빠름

 

ForkJoinPool의 주요 메서드

  • invoke(ForkJoinTask<?> task): 기다리고 결과를 받음
  • execute(ForkJoinTask<?> task): 작업을 비동기로 실행
  • submit(ForkJoinTask<?> task): 작업을 실행하고 Future를 반환

ForkJoinPool 개발 시 RecursiveTaskRecursiveAction의 역할

  1. RecursiveTask<V>:
    • 반환값이 있는 병렬 작업을 정의할 때 사용
    • 작업을 분할하고 결과를 합산하여 반환(compute() 메서드)
  2. RecursiveAction:
    • 반환값이 없는 병렬 작업을 정의할 때 사용.
    • 단순히 작업을 수행하고 결과를 반환하지 않는 경우 적합(compute() 메서드)

꼭 써야해?

RecursiveTask를 상속하지 않고도 직접 ForkJoinTask 또는 Runnable과 같은 인터페이스를 사용할 수 있다. 하지만 이는 더 복잡하고 비효율적이며 코드 복잡성을 증가시킨다.

ForkJoinPool

스레드 갯수를 생략하면, 기본적으로 가용한 CPU 코어 수에 따라 동작

  • 스레드 수 = Runtime.getRuntime().availableProcessors()
    즉, 현재 시스템의 CPU 코어 수(논리적 코어 포함)가 기본 스레드 수로 사용됨
ForkJoinPool pool = new ForkJoinPool(); //내부적으로 가용한 프로세서 수를 기반으로 스레드 풀 크기를 결정

ForkJoinPool pool = new ForkJoinPool(4); // 스레드 4개 사용

예시

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinExample {
    static class SumTask extends RecursiveTask<Integer> {
        private final int[] arr;
        private final int start, end;
        private static final int THRESHOLD = 10;

        public SumTask(int[] arr, int start, int end) {
            this.arr = arr;
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            if (end - start <= THRESHOLD) {
                // 작은 작업은 직접 계산
                int sum = 0;
                for (int i = start; i < end; i++) {
                    sum += arr[i];
                }
                return sum;
            } else {
                // 작업 분할
                int mid = (start + end) / 2;
                SumTask leftTask = new SumTask(arr, start, mid);
                SumTask rightTask = new SumTask(arr, mid, end);

                leftTask.fork(); // 병렬 처리
                int rightResult = rightTask.compute(); // 동기 처리
                int leftResult = leftTask.join(); // 병합

                return leftResult + rightResult;
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = new int[100];
        for (int i = 0; i < arr.length; i++) arr[i] = i + 1;

        ForkJoinPool pool = new ForkJoinPool();
        SumTask task = new SumTask(arr, 0, arr.length);
        int result = pool.invoke(task);

        System.out.println("Sum: " + result); // 출력: Sum: 5050
    }
}

1. leftTask.fork();

  • 작업 분할:
    • leftTask를 병렬로 처리할 수 있도록 Fork-Join Pool에 작업 큐로 제출
    • fork() 메서드는 현재 작업을 Fork-Join Pool의 스레드가 처리하도록 요청하며, 비동기적으로 실행
    • 이 시점에서 leftTask는 아직 결과를 계산하지 않음

2. int rightResult = rightTask.compute();

  • 동기 실행:
    • rightTask는 직접 현재 스레드에서 동기적으로 계산
    • compute() 메서드는 RecursiveTask에서 작업을 처리하는 메인 로직
    • 이렇게 함으로써 하나의 스레드가 rightTask를 바로 계산하여, 자원을 최대한 활용

3. int leftResult = leftTask.join();

  • 결과 병합:
    • join() 메서드는 leftTask가 완료될 때까지 대기하고 결과를 반환
    • 만약 leftTask가 이미 완료되었으면, 바로 결과를 반환
    • 이를 통해 leftTask와 rightTask의 결과를 병합

왜 이런 방식으로 처리?

  • 자원의 효율적 활용:
    • leftTask는 병렬로 실행하도록 요청 (fork())
    • 한편, rightTask는 현재 스레드에서 처리 (compute())
    • 이렇게 하면 다른 작업 스레드가 leftTask를 처리하는 동안, 현재 스레드가 놀지 않고 rightTask를 계산하여 자원을 최대한 활용
  • 병렬성과 동기화의 조합:
    • fork()로 비동기 작업을 시작하고 join()으로 결과를 기다리며 동기화.
    • 병렬성과 동기화의 균형을 유지하면서 성능을 최적화

ForkJoinPool

  • 특징:
    • Java 7에서 도입.
    • 작업 분할(divide-and-conquer)을 기반으로 병렬 처리를 수행.
    • Work-Stealing 알고리즘을 사용해 작업이 끝난 스레드가 다른 스레드의 작업을 훔쳐 효율성을 높임.
    • 주로 재귀적인 작업 처리작업 분할에 사용.
    • RecursiveTask(결과 반환)와 RecursiveAction(결과 없음)을 통해 작업 정의.
  • 사용 사례:
    • 큰 작업을 작은 작업으로 나눠 처리하는 경우.
    • 예: 대규모 데이터 처리, 배열 합산, 병렬 검색.
  • 장점:
    • 스레드 수를 효율적으로 관리 (스레드 풀 크기 설정 가능).
    • Idle(대기) 상태인 스레드가 다른 작업을 훔쳐 병렬 처리 최적화.
  • 단점:
    • 작업 분할이 필요 없는 간단한 병렬 작업에는 적합하지 않을 수 있음.
    • Work-Stealing 비용이 단순 작업에서는 오히려 비효율적.

ExecutorService

  • 특징:
    • Java 5에서 도입.
    • 병렬 작업을 스레드 풀에서 실행하여 스레드 관리를 자동화.
    • Java의 스레드 풀을 관리하는 인터페이스로, 스레드의 생성, 실행, 종료를 간편하게 처리.
    • 개발자는 스레드 풀을 직접 관리할 필요가 없음!
    • 스레드 풀이 다양한 종류로 제공:
      • FixedThreadPool: 고정된 크기의 스레드 풀.
      • CachedThreadPool: 동적으로 크기가 변하는 스레드 풀.
      • ScheduledThreadPool: 예약 및 지연 실행 작업용.
      • SingleThreadExecutor: 단일 스레드로 작업 처리.
  • 사용 사례:
    • 병렬 작업이 분할되지 않거나 작업 분할을 수동으로 처리해야 할 때.
    • 예: 웹 서버 요청 처리, 비동기 작업 관리.
  • 장점:
    • API가 간단하고 다양한 스레드 풀 종류 제공.
    • 반복적이고 독립적인 병렬 작업에 적합.
    • 작업 분할 없이 단순 병렬 실행 가능.
  • 단점:
    • ForkJoinPool만큼 작업 분할에 최적화되지 않음.
    • 대규모 데이터 병렬 처리에는 적합하지 않을 수 있음.
import java.util.concurrent.*;

public class ExecutorServiceExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //4개의 스레드로 구성된 고정 크기 풀 생성
        ExecutorService executor = Executors.newFixedThreadPool(4);

        Callable<Integer> task1 = () -> {
            Thread.sleep(1000);
            return 1;
        };
        Callable<Integer> task2 = () -> {
            Thread.sleep(1000);
            return 2;
        };

        Future<Integer> result1 = executor.submit(task1);
        Future<Integer> result2 = executor.submit(task2);

		//Future.get() 호출로 각각의 결과를 대기하고 출력
        //task1과 task2는 1초 동안 대기 후 각각 1과 2를 반환
        System.out.println("Result 1: " + result1.get());
        System.out.println("Result 2: " + result2.get());

		//스레드 풀 종료
        executor.shutdown();
    }
}

 


728x90
반응형
반응형

프로세스 또는 스레드 간의 동기화공유 자원 관리를 위해 사용하는 동기화 도구

원리인가, 구현인가?

  1. 원리 측면:
    • 뮤텍스와 세마포어는 동시성 제어 문제를 해결하기 위한 개념적인 원리
    • 프로세스 간 공유 자원 접근 문제(Critical Section Problem)를 해결하기 위해 설계
    • "P/V 연산", "락/언락" 등의 수학적 원리에 기반
  2. 구현 측면:
    • 운영 체제는 뮤텍스와 세마포어를 시스템 콜로 구현하여 동기화를 제공
    • 프로그래밍 언어에서는 이를 감싸는 고급 추상화 클래스/메서드로 구현

 

뮤텍스; 단일 스레드/프로세스가 임계 구역을 독점적으로 보호

특징

  • 초기값이 0인 상태에서 스레드가 자원을 점유하면 값을 0 -> 1로 변경.
  • 뮤텍스는 한 번에 하나의 스레드만 자원을 점유할 수 있도록 제한하는 도구
  • 뮤텍스는 자원의 소유권 개념이 있어, 락을 획득한 스레드만 언락이 가능
  • Java의 ReentrantLock

뮤텍스 초기값 0의 의미

  • 0:
    • 뮤텍스가 열려 있는 상태(사용 가능)
    • 어떤 스레드도 해당 뮤텍스를 점유하고 있지 않은 상태

뮤텍스의 동작 과정

  1. 락(Lock):
    • 스레드가 뮤텍스를 점유하려고 하면, 뮤텍스 값을 1로 변경
    • 동시에 다른 스레드가 뮤텍스를 점유하려고 하면 대기 상태로 전환
  2. 언락(Unlock):
    • 스레드가 작업을 완료하고 뮤텍스를 해제하면, 뮤텍스 값을 0으로 
    • 대기 중인 다른 스레드가 뮤텍스를 점유할 수 있도록 허용
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MutexExample {
    private final Lock lock = new ReentrantLock();

    public void criticalSection() {
        lock.lock(); // 임계 구역 접근
        try {
            System.out.println(Thread.currentThread().getName() + " is in critical section.");
        } finally {
            lock.unlock(); // 접근 해제
        }
    }
/////
    public static void main(String[] args) {
        MutexExample example = new MutexExample();

        Runnable task = example::criticalSection;

        // 스레드 2개 실행
        new Thread(task).start();
        new Thread(task).start();
    }
}

 

  • 첫 번째 스레드가 실행되어 lock.lock()으로 락 획득
    • 임계 구역에 진입해 메시지를 출력
    • lock.unlock()로 락 해제
  • 두 번째 스레드는 첫 번째 스레드가 락을 해제하기 전까지 대기
    • 첫 번째 스레드가 락을 해제하면, 두 번째 스레드가 락을 획득하고 임계 구역에 진입

 

  • 임계 구역 보호:
    • ReentrantLock을 사용하여 임계 구역에 동시에 하나의 스레드만 접근하도록 보장
  • 스레드 간 동기화:
    • 두 스레드는 순차적으로 criticalSection 메서드에 접근하며, 동시에 실행되지 않음
  • 락 해제 보장:
    • try-finally 구조를 통해 락이 반드시 해제되도록 보장하여 데드락을 방지

 

 

 

세마포어; 여러 스레드/프로세스가 제한된 자원을 공유

특징

세마포어는 정수 값(카운터)을 기반으로 동작하며, 공유 자원에 대한 접근을 제어하는 데 사용; Java의 Semaphore

 

  • 정수 값 기반 제어:
    • 세마포어는 정수 값을 사용하여 현재 공유 자원에 접근할 수 있는 허용 가능한 스레드 또는 프로세스의 수를 나타냄
    • 이 값은 초기화된 이후, 특정 연산을 통해 증가하거나 감소
  • P 연산 (wait 또는 acquire):
    • 세마포어 값을 감소
    • 값이 0보다 작아질 경우, 현재 프로세스나 스레드는 대기 상태에 들어가고, 다른 스레드가 세마포어 값을 증가시킬 때까지 블록
  • V 연산 (signal 또는 release):
    • 세마포어 값을 증가
    • 대기 중인 스레드가 있으면 이를 깨워서 실행

세마포어의 초기값

세마포어의 초기값은 관리할 자원의 개수를 나타냄

  • 초기값 1: 바이너리 세마포어(Binaray Semaphore)처럼 동작하여 뮤텍스와 비슷한 역할
  • 초기값 N (N > 1): 자원을 N개까지 동시 허용하는 카운팅 세마포어(Counting Semaphore) 역할
  • 세마포어는 다음과 같은 연산으로 동작한다:
    • P(Wait):
      • 자원의 개수를 감소시킴.
      • 값이 0 이하가 되면 스레드는 자원이 해제될 때까지 대기.
    • V(Signal):
      • 자원의 개수를 증가시킴.
      • 대기 중인 스레드가 있다면 해제된 자원을 할당받아 실행을 재개.

세마포어의 값과 자원의 상태

  1. 값 > 0:
    • 자원이 하나 이상 사용 가능한 상태
    • 대기 중인 스레드가 즉시 자원에 접근 가능.
  2. 값 = 0:
    • 모든 자원이 이미 사용 중
    • 자원을 사용하려는 스레드는 대기 상태로 전환
  3. 값 < 0 (특정 구현에서 허용):
    • 대기 중인 스레드의 수를 나타낼 수 있
    • 하지만 일반적인 세마포어 구현에서는 음수값을 사용하지 않고 **대기열(queue)**로 관리

 

세마포어의 용도

  1. 임계 구역(Critical Section) 보호:
    • 공유 자원에 대한 동시 접근을 제어하여 데이터 무결성을 보장합니다.
  2. 스레드 동기화:
    • 여러 스레드가 특정 순서대로 작업을 수행하도록 제어합니다.
  3. 제한된 자원 관리:
    • 예를 들어, 네트워크 연결, 데이터베이스 연결과 같이 제한된 수의 자원을 사용하는 경우, 세마포어를 사용하여 접근 수를 제한할 수 있습니다.

 

  • 데이터베이스 커넥션 풀 관리
  • 생산자-소비자 문제
  • 네트워크 리소스 관리 (동시에 처리 가능한 연결 제한)
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private final Semaphore semaphore = new Semaphore(2); // 동시에 2개 허용

    public void accessResource() {
        try {
            semaphore.acquire(); // P 연산
            System.out.println(Thread.currentThread().getName() + " accessing resource.");
            Thread.sleep(1000); // 작업 수행
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release(); // V 연산
            System.out.println(Thread.currentThread().getName() + " released resource.");
        }
    }
////
    public static void main(String[] args) {
        SemaphoreExample example = new SemaphoreExample();

        Runnable task = example::accessResource;

        for (int i = 0; i < 5; i++) {
            new Thread(task).start(); // 스레드 5개 실행
        }
    }
}
/////////
Thread-0 accessing resource.
Thread-1 accessing resource.
Thread-0 released resource.
Thread-2 accessing resource.
Thread-1 released resource.
Thread-3 accessing resource.
Thread-2 released resource.
Thread-4 accessing resource.
Thread-3 released resource.
Thread-4 released resource.
  • 초기 상태에서 Semaphore의 카운터는 2
  • 5개의 스레드가 실행되며, 다음과 같이 동작
  1. Thread-0Thread-1이 먼저 자원을 획득하여 작업을 수행
  2. 다른 스레드(Thread-2, Thread-3, Thread-4)는 자원이 반환될 때까지 대기
  3. 작업이 끝난 스레드가 자원을 반환(release())하면, 대기 중인 스레드 중 하나가 자원을 획득
  4. 이 과정이 반복되며, 모든 스레드가 차례로 작업을 완료

 

 

뮤텍스와 바이너리 세마포어는 같은 것? NO

  • 뮤텍스는 락을 가진 자만 해제 가능, 세마포어는 그렇지 않다.
  • 뮤텍스는 priority inheritance 속성을 가지나 세마포어는 그렇지 않다(누가 시그널을 날릴지 모름)
    • 우선순위 높은 작업이 락에 의해 블라킹되면 그 블라킹 작업의 우선순위를 높여 우선순위작업이 빨리 처리되게끔 하는 것

 

상호배제(단일 자원에 대한 독점적 접근 보장)만 필요하다면 뮤텍스를, 작업 간의 실행순서 동기화가 필요하면 세마포어 사용

언제 뮤텍스를 사용할까?

  • 자원에 대한 단일 접근만 보장하면 되는 경우.
  • 예:
    • 공유 변수나 데이터 구조 보호.
    • 파일 쓰기 작업.

언제 세마포어를 사용할까?

  • 작업 간 실행 순서를 동기화해야 하는 경우.
  • 여러 스레드가 동시에 제한된 자원(예: DB 연결, 네트워크 포트)에 접근할 때.
  • 예:
    • 생산자-소비자 문제.
    • 연결 풀 관리(최대 N개 동시 연결).
      • 디비풀; 커넥션풀
728x90
반응형

+ Recent posts