멀티프로세서 스케줄링(Multiprocessor Scheduling)


 

멀티프로세서 스케줄링(Multiprocessor Scheduling)은 여러 개의 CPU가 동시에 있는 컴퓨터에서 CPU 시간을 어떻게 효율적으로 분배하는 것에 대한 문제다. 단일 CPU만 있던 시절에는 한 번에 하나의 프로그램만 CPU를 차지할 수 있었기 때문에, 프로그램들이 차례로 CPU를 쓰도록 스케줄링하는 것이 주된 고민이었다. 하지만 요즘은 컴퓨터 안에 여러 개의 CPU 코어가 들어 있기 때문에, 동시에 여러 프로그램을 처리할 수 있다. 이때, 어떤 CPU에 어떤 프로그램을 할당할지 결정하는 것이 바로 “멀티프로세서 스케줄링” 문제다.


과거에는 데스크톱이나 노트북 컴퓨터에 CPU가 1개뿐이었고, 멀티프로세서 시스템은 서버나 슈퍼컴퓨터처럼 특수한 곳에나 있었다. 하지만 이제는 우리가 쓰는 대부분의 PC에도 여러 코어가 들어 있다. “듀얼코어”, “쿼드코어”라는 단어를 들어본 적 있을 것이다. 이는 한 CPU 칩 안에 여러 개의 처리 장치가 있어서 동시에 여러 작업을 처리할 수 있다는 뜻이다.

 

싱글코어 CPU 속도를 높이는 데 한계가 오면서, CPU 제조사들은 한 칩에 코어를 여러 개 넣는 전략을 택했다. 그 결과, 프로그램을 더 빨리 실행하고 싶다면 이제는 단순히 CPU 클록 속도를 높이기보다, 여러 코어를 활용하도록 프로그램을 만들어야 한다. 즉, “동시에 여러 일을 할 수 있는 프로그램”을 작성하는 일이 중요해졌다.


멀티프로세서 시스템에서 발생하는 문제들

    1. 캐시 일관성 문제(Cache Coherence)
      각 CPU는 빠른 처리를 위해 “캐시”라는 작은 저장소를 갖고 있다. 프로그램이 메모리 데이터를 자주 읽거나 쓸 때, 매번 느린 메모리에서 가져오지 않고 이 캐시에 올려두면 더 빠르게 처리할 수 있다.
      하지만 CPU가 여러 개일 때 문제가 생긴다. 예를 들어, CPU1이 어떤 데이터(A)를 캐시에 올린 뒤 그 값을 변경했다고 해보자. 아직 메인 메모리는 업데이트되지 않았을 수 있다. 그 시점에서 CPU2가 같은 데이터(A)를 필요로 하여 메인 메모리에서 읽어오면, CPU2는 업데이트 전 옛날 값을 가져올 수 있다. 이 문제를 해결하기 위해 하드웨어 수준의 “캐시 일관성 프로토콜”이 동작한다. 모든 CPU의 캐시가 서로 모니터링해서, 한 CPU에서 데이터가 바뀌면 다른 CPU들도 그 사실을 알도록 하고, 낡은 데이터를 쓰지 않도록 한다.

    2. 동기화(Synchronization)의 필요성
      여러 CPU가 동시에 같은 자료구조에 접근한다면, 서로 충돌해서 데이터가 꼬일 수 있다. 이를 막기 위해 락(lock)이라는 장치를 사용한다. 한 CPU가 리스트를 수정할 때는 “잠금”을 걸어두고, 다른 CPU는 그 작업이 끝날 때까지 기다린다. 그래야 리스트가 꼬이거나 같은 데이터를 두 번 삭제하는 일이 없다.

    3. 캐시 친화성(Cache Affinity)
      각 프로그램(또는 스레드)이 실행되는 동안, 해당 CPU의 캐시에 데이터가 쌓인다. 프로그램이 매번 다른 CPU에서 실행되면 캐시를 매번 새로 채워야 하는데, 이것은 시간 낭비다. 하지만 다음에 이 프로그램을 다시 실행할 때 같은 CPU에서 실행하면 캐시를 재사용할 수 있어 속도가 빨라진다. 이를 “캐시 친화성”이라고 한다. 즉, 프로그램을 가능한 한 이전에 실행되던 CPU에 다시 올리는 것이 유리하다.

멀티프로세서 스케줄링 기법

  1. 단일 큐 멀티프로세서 스케줄링(SQMS)
    : 모든 작업을 하나의 대기열(queue)에 넣어두고, 여러 CPU가 그 큐에서 작업을 하나씩 꺼내서 실행하는 방법
    • 장점: 구현이 비교적 쉽다. 원래 단일 CPU용 스케줄러에서 거의 바꾸지 않고도 여러 CPU에 적용할 수 있다.
    • 단점: 한 큐를 모든 CPU가 공유하기 때문에 “락” 경쟁이 심해진다. 프로그램이 어느 CPU에서 실행될지 매번 바뀌어 캐시 친화성도 떨어진다.
  2. 멀티 큐 멀티프로세서 스케줄링(MQMS)
    : CPU마다 자신의 큐를 하나씩 두는 방법.(CPU0는 큐0, CPU1은 큐1) 작업이 들어오면 어떤 큐에 넣을지 정하고, 그 큐의 CPU에서만 작업을 처리한다.
    • 장점: 각 CPU가 자신의 큐를 독립적으로 관리하므로 락 경쟁이 적어지고, 캐시 친화성도 좋아진다(같은 CPU에서 계속 실행)
    • 단점: 워크로드(작업량) 균형 문제
      → 작업 이주(migration): 바쁜 CPU의 큐에서 일이 많은 작업을 하나 덜어내어 한가한 CPU 큐로 옮김.(작업 훔치기(work stealing))

 

자바는 한 번 작성하면 어디서든 실행할 수 있는 "Write Once, Run Anywhere"라는 철학을 바탕으로, 플랫폼 독립성을 가진 대표적인 프로그래밍 언어다. 이러한 플랫폼 독립성은 JVM(Java Virtual Machine)이라는 가상 머신 덕분에 가능한데, 관련해서 무엇인지 작성해보겠다.


1. 자바 실행 과정의 특징

자바 프로그램의 실행 과정은 크게 다음과 같다.

 

소스 코드 작성 및 컴파일

개발자는 .java 확장자를 갖는 자바 소스 코드를 작성하면, javac 컴파일러가 이 소스 코드를 바이트코드(.class)로 컴파일한다.

  • 바이트코드: 특정 운영체제에 종속되지 않는 중간 형태의 코드

클래스 로딩 및 링크(Linking)

실행 시점에 JVM은 클래스 로더(Class Loader)를 통해 필요한 .class 파일(바이트코드 파일)을 메모리에 로드하고, 심볼릭 참조를 직접 참조로 변경하는 링크 과정을 수행한다.

바이트코드 실행

로드된 바이트코드는 실행 엔진에 의해 해석되거나 JIT(Just-In-Time) 컴파일러를 통해 기계어로 변환되어 실제 OS 환경에서 실행된다.

이 과정을 통해 자바는 특정 운영체제나 하드웨어 아키텍처에 종속되지 않고 동일한 바이트코드를 기반으로 다양한 환경에서 실행될 수 있는 것이다.


2. JVM의 구조 및 역할

JVM은 자바 프로그램이 실질적으로 동작하는 "가상 머신" 환경으로, 운영체제 위에서 자바 바이트코드를 해석하고 실행하는 역할을 한다.

클래스 로더(Class Loader)

클래스 로더는 자바 클래스(.class 파일)를 동적으로 메모리에 로드하고, 필요한 경우 클래스를 초기화하는 역할을 담당한다. 자바 프로그램 실행 중 필요한 클래스를 언제든 불러올 수 있으며, 이를 통해 유연한 모듈성 및 동적 로딩이 가능하다.

  • 로딩: .class 파일을 찾아서 JVM 메모리에 로드
  • 링크: 심볼릭 레퍼런스(클래스, 메소드, 필드 정보 등)를 다이렉트 레퍼런스로 변경하고, 정적 변수를 초기화
  • 초기화: 정적 블록 실행 및 정적 변수의 초기값 할당 등 클래스 초기화 과정 수행

실행 엔진(Execution Engine)

실행 엔진은 로딩된 바이트코드를 실제 CPU가 이해할 수 있는 기계어 수준으로 해석하고 실행한다.

  • 인터프리터: 바이트코드를 명령어 단위로 해석하며 순차적으로 실행. 초기 실행 속도가 빠르지만, 반복 실행되는 코드에서는 비효율적.
  • JIT 컴파일러: 자주 실행되는 바이트코드 영역을 선별하여 기계어로 컴파일함으로써, 반복되는 코드 실행 시 성능을 향상시킴. 실행 시간이 지날수록 최적화가 이루어져 퍼포먼스가 향상.

메모리 영역(Runtime Data Area)

JVM은 자바 프로그램 실행 중 다양한 데이터를 저장한다. 메모리 영역은 다음과 같다.

  • 메소드 영역: 클래스에 대한 메타데이터(클래스 정보, 메소드 정보, 상수 풀(Constant Pool), 정적 변수) 등을 저장.
  • : new 키워드를 통해 생성되는 객체와 배열이 저장. 가비지 컬렉터에 의해 사용되지 않는 객체 정리.
  • JVM 스택: 메소드 호출 시 스택 프레임을 생성하여 지역 변수, 파라미터, 연산 중간 결과 저장. 메소드가 종료되면 해당 스택 프레임은 제거.
  • PC(Program Counter) 레지스터: 현재 실행 중인 JVM 명령어의 주소를 저장.
  • 네이티브 메소드 스택: 자바 외부의 네이티브 코드(C, C++) 호출 시 사용되는 스택 영역.

자바의 플랫폼 독립성

JVM은 자바 바이트코드를 어떤 플랫폼(Windows, Mac OS, Linux) 위에서도 동일하게 해석하고 실행한다(플랫폼 독립성). 즉, 자바 개발자는 OS나 하드웨어 환경에 구애받지 않고 하나의 바이트코드만으로 다양한 환경에서 애플리케이션을 동작할 수 있는 것이다(이식성).

1. 프로세스(Process)와 프로그램(Program)

프로그램은 하드디스크 위에 고정되어 있는 코드와 데이터의 집합→ 죽어있는 상태

프로세스는 이 프로그램을 실제로 메모리에 올려 CPU가 명령어를 하나씩 실행하는 것 → 살아있는 존재

  • 프로그램: 하드디스크 상의 정적인 명령어와 데이터 묶음
  • 프로세스: 메모리에 로드되어 CPU가 실행 중인 “활동하는” 프로그램

쉽게 말해, 게임 실행 파일 아이콘은 그냥 프로그램이고, 그것을 더블 클릭해서 게임이 실제로 화면에 움직이는 상태가 되면 그게 프로세스입니다.

 


 

2. 왜 프로세스가 중요할까?
우리는 평소 컴퓨터로 작업할 때 여러 프로그램을 동시에 켜놓음.

여러 작업이 동시에 실행 중인 것처럼 보이는데, 사실 CPU는 한 번에 하나의 명령만 처리할 수 있다.


그렇다면 어떻게 여러 프로세스가 동시에 동작하는 것처럼 보일까요?


운영체제는 CPU를 빠르게 돌아가며 여러 프로세스에 할당하는 시분할(time sharing) 기법을 사용.

ex) 한 프로세스를 실행 가능한 시점까지 실행하고, I/O 등 CPU를 사용하지 않는 작업을 할 때는 다른 프로세스를 실행.

 


 

3. 메커니즘(mechanism)과 정책(policy)
CPU 가상화를 잘 구현하기 위해서는 두 가지가 필요함.

  • 메커니즘: 어떻게 프로세스를 멈추고 다른 프로세스로 넘어가는지, 프로세스를 만들고 종료시키는 방법은 무엇인지 등의 구체적인 수단과 방법
  • 정책: 여러 개의 프로세스가 실행 대기 중일 때 어느 프로세스를 먼저 실행할지, 어떤 기준으로 CPU 사용 순서를 정할지 등의 결정 방법

메커니즘과 정책을 독립적으로 분리해두면, 나중에 정책을 바꾸더라도 메커니즘 부분을 변경하지 않아도 되고, 반대로 메커니즘을 바꿔도 정책은 그대로 둘 수 있어 편리하다.

 


 

4. 프로세스의 구성 요소

  • 메모리(주소 공간): 코드(실행 명령)데이터(변수), 힙(동적으로 할당되는 메모리), 스택(함수 호출 정보와 지역 변수)
  • CPU 레지스터 상태: 현재 어느 명령어를 실행 중인지 알려주는 프로그램 카운터(PC), 스택 포인터(SP), 각종 일반 레지스터 값 등
  • 입출력 상태: 현재 열려있는 파일들, 표준 입력/출력(키보드, 모니터), 네트워크 소켓 등
  •  

 

5. 프로세스의 생애주기(상태 전이)

  • 생성(Create) : 프로세스가 생성되는 중
  • 실행(Running) : 프로세스가 프로세서를 차지하여 명령어들이 실행 중
  • 준비(Ready) : 프로세스가 프로세서를 언제든지 사용할 수 있는 상태로, 프로세서 할당 대기 중.
  • 대기(Waiting) : 프로세스가 입출력 완료, 시그널 수신 등 디스크나 네트워크 입출력을 기다리고 있는 상태.
  • 종료(Terminated) : 프로세스의 실행 종료.

프로세스는 필요에 따라 이 상태들을 오가게 됨.
ex) A프로세스가 실행 중(Running)이었는데, 갑자기 파일을 읽어야 한다면 디스크 입출력이 끝날 때까지 대기(Waiting) 상태로 전환되고, 이때 다른 프로세스가 CPU를 차지(Ready→Running)하게 되는 식입니다.


7. 프로세스의 생성과 종료

  • 프로세스 생성(Create): 디스크에 있는 프로그램 코드를 메모리에 올리고, 스택과 힙을 초기화한 뒤, 표준 입력/출력 설정한 후 main 함수 실행.
  • 프로세스 종료(Destroy): 프로세스가 할 일을 모두 마치면 종료. 프로그램 내부에서 종료 요청을 하거나(예: return 0;), 사용자가 강제로 종료. 프로세스가 종료되면 운영체제가 점유 중이던 메모리나 파일 등의 자원을 다시 회수.

 

 

 

프로그램을 실행시키면, OS는 해당 프로그램을 프로세스 형태로 관리함.

프로세스 API: 프로그래머가 프로세스를 생성, 제어, 모니터링할 수 있도록 OS가 제공하는 함수(시스템 콜)들의 집합. Unix 계열 시스템에서는 fork(), exec(), wait() 등이 있다.


fork(): 새로운 프로세스 만들기

  • 기능: fork()는 현재 프로세스를 복사하여 "자식 프로세스"를 하나 더 만듦.
  • 동작 원리:
    호출한 시점에서 프로세스가 두 개로 나뉨.
    • 부모 프로세스: 기존 프로세스
    • 자식 프로세스: 부모의 복사본
  • 반환값 차이:
    • 부모 프로세스: fork()가 자식 프로세스의 PID(프로세스 ID)를 반환
    • 자식 프로세스: fork()가 0을 반환
  • 결과:
    이렇게 반환값의 차이를 이용해 부모와 자식에서 각각 다른 코드를 실행.

예제 코드:

int rc = fork();
if (rc < 0) {
    fprintf(stderr, "fork failed\n");
    exit(1);
} else if (rc == 0) {
    // 자식 프로세스 영역
    printf("I am the child process!\n");
} else {
    // 부모 프로세스 영역
    printf("I am the parent process, child pid = %d\n", rc);
}

exec(): 다른 프로그램 실행하기

  • 기능: exec() 계열 함수(execvp, execl, execve 등)는 현재 프로세스를 완전히 다른 프로그램으로 대체.
  • 사용 시나리오: fork()를 통해 자식을 만들었는데, 그 자식이 부모와 똑같은 프로그램을 실행하는 대신 전혀 다른 프로그램을 실행하고 싶을 때 호출.
  • 동작 원리:
    • exec()가 성공하면 기존에 로드되어 있던 코드와 데이터가 새로운 프로그램으로 덮어씌어짐.
    • exec()는 성공한 뒤 원래 코드로 돌아오지 않으며, 바로 새로운 프로그램의 main()부터 실행.

예제 코드 (자식에서 wc 명령 실행):

int rc = fork();
if (rc == 0) { 
    // 자식 프로세스
    char *args[] = {"wc", "filename.txt", NULL};
    execvp(args[0], args);
    // 여기까지 도달하지 않음 (exec 성공 시)
    fprintf(stderr, "exec failed\n");
    exit(1);
}

wait(): 자식 프로세스 종료 대기

  • 기능: wait()는 부모 프로세스가 자식 프로세스가 끝날 때까지 기다리게 하는 시스템 콜.
  • 사용 이유:
    • 부모 프로세스가 자식의 수행 결과를 필요로 할 때
    • 자식이 종료되기 전에 부모가 먼저 종료해버리면 자식이 "고아 프로세스"가 될 수 있으므로, 이를 방지하기 위해 사용
  • 반환값: 종료한 자식의 PID를 반환(어떤 자식이 끝났는 지 알 수 있음).

예제:

int rc = fork();
if (rc == 0) {
    // 자식 프로세스
    printf("Child running...\n");
    sleep(2); // 예: 2초 후 종료
    exit(0);
} else {
    // 부모 프로세스
    int wc = wait(NULL); // 자식이 끝날 때까지 대기
    printf("Child %d finished\n", wc);
}

왜 이렇게 복잡할까? (fork + exec 조합의 이유)

  • 유연성 확보:
    자식 프로세스를 만든 후 exec()를 호출하기 전까지 환경을 마음대로 설정할 수 있다.
    ex) 자식 프로세스에서 파일 디스크립터를 재지정하여 표준 출력이 화면 대신 파일로 가게 하거나, 파이프를 이용해 다른 프로세스와 연동 등등..
  • 쉘의 구현:
    쉘(shell): 사용자가 입력한 명령어를 실행하는 프로그램.
    fork()로 자식을 만든 뒤, 그 자식 안에서 exec()를 호출하여 사용자가 입력한 명령어 실행.
    이 과정에서 I/O 재지정, 파이프, 환경 변수 설정 등등 가능..

추가적인 프로세스 관련 API와 개념

  • kill(): 특정 프로세스에 시그널을 보내 프로세스를 종료, 중단, 재개하는 등.
  • ps, top: 현재 실행 중인 프로세스를 모니터링하고 CPU, 메모리 사용량을 확인.
  • nice(): 프로세스의 우선순위를 조정.
  • setsid(), setpgid(): 프로세스를 세션이나 프로세스 그룹으로 묶어 제어.
  • 파이프(pipe) & 리다이렉션(>), <): 파이프를 통해 한 프로세스의 출력을 다른 프로세스의 입력으로 연결하거나, 명령어 실행 결과를 파일로 저장하는 등 다양한 입출력을 조작.

다른 운영체제와 비교

  • Windows:
    Windows는 CreateProcess()라는 단일 API 콜로 새로운 프로세스 생성과 프로그램 로딩을 함께 처리. (CreateProcess() 하나로 프로세스 생성 및 실행)
  • 멀티스레드 환경:
    fork()는 호출 시점의 스레드와 메모리 상태를 복사 → 스레드 안전성 문제 fork() 직후 exec()를 바로 호출하는 것이 권장된다.

 

GPT님님님의 의견

멀티스레드 프로그램에서 fork()를 호출할 경우 다음과 같은 문제가 발생할 수 있습니다:

  1. 스레드 복제 방식:
    fork()는 호출한 스레드를 포함한 전체 프로세스의 메모리 상태를 복사하지만, 실제로 새로 생성된 자식 프로세스에는 호출한 스레드 하나만 존재하게 됩니다. 즉, 부모 프로세스에는 여러 스레드가 있었지만, fork() 후 자식 프로세스에는 단 하나의 스레드(호출한 스레드)만 남게 됩니다. 이 과정에서 다른 스레드들이 잡고 있던 락(lock)이나 공유 자원에 대한 접근 상태, 전역 변수 상태 등은 그대로 복사되지만 정작 그 스레드들은 존재하지 않습니다. 결과적으로 일부 리소스나 락이 "해제 불가능한 상태"가 되거나 비정상적인 동기화 상태에 빠질 수 있습니다.
  2. 동기화 상태 불일치:
    멀티스레드 프로그램은 락, 조건 변수, 뮤텍스, 세마포어 등의 동기화 기법으로 스레드 간 자원 접근을 조율합니다. fork()를 통해 복사된 자식 프로세스는 메모리 상에서 동기화 객체의 상태를 그대로 물려받지만, 실제로 락을 잡고 있던 스레드는 사라졌으므로 해당 락은 유령 상태(잠긴 상태에서 더는 풀 수 없는 상태)가 될 수 있습니다. 또한 조건 변수가 특정 스레드를 깨우기 위한 신호를 기다리고 있는 상태일 때, 그 스레드는 더 이상 존재하지 않으므로 조건을 충족할 수 없는 교착 상태가 발생할 수 있습니다.
  3. 해결책 – fork() 후 즉시 exec():
    이러한 문제를 피하기 위해, 표준 C 라이브러리와 POSIX 표준에서는 멀티스레드 환경에서 fork()를 호출한 뒤에는 가능한 한 빠르게 exec() 계열 함수를 호출하여 완전히 새로운 프로그램 이미지로 전환하는 것을 권장합니다. exec()를 호출하면 기존 메모리 공간(및 그 안에서 뒤틀린 동기화 상태)을 모두 버리고 새 프로그램의 깨끗한 메모리 상태로 시작하기 때문에 스레드 안전성 문제를 근본적으로 해소할 수 있습니다.

정리하자면, 멀티스레드 환경에서 fork()를 호출하면 부모 프로세스의 복잡한 스레드 및 동기화 상태가 그대로 복사되지만 실제로 자식 프로세스에는 한 스레드만 남기 때문에, 동기화 객체와 공유 자원 상태가 비정상적이고 예측 불가능한 상황에 놓일 수 있습니다. 이를 피하기 위해 자식 프로세스에서 곧바로 exec()를 호출하여 새 프로그램 이미지를 로드하는 것이 안전한 패턴입니다.

 

 

📒 문제

https://www.acmicpc.net/problem/20920

화은이는 이번 영어 시험에서 틀린 문제를 바탕으로 영어 단어 암기를 하려고 한다. 그 과정에서 효율적으로 영어 단어를 외우기 위해 영어 단어장을 만들려 하고 있다. 화은이가 만들고자 하는 단어장의 단어 순서는 다음과 같은 우선순위를 차례로 적용하여 만들어진다. 

1. 자주 나오는 단어일수록 앞에 배치한다.
2. 해당 단어의 길이가 길수록 앞에 배치한다.
3. 알파벳 사전 순으로 앞에 있는 단어일수록 앞에 배치한다. 

보다 짧은 길이의 단어의 경우 읽는 것만으로도 외울 수 있기 때문에 길이가 이상인 단어들만 외운다고 한다. 화은이가 괴로운 영단어 암기를 효율적으로 할 수 있도록 단어장을 만들어 주자.

입력

첫째 줄에는 영어 지문에 나오는 단어의 개수 과 외울 단어의 길이 기준이 되는 이 공백으로 구분되어 주어진다. (, )
둘째 줄부터 번째 줄까지 외울 단어를 입력받는다. 이때의 입력은 알파벳 소문자로만 주어지며 단어의 길이는 을 넘지 않는다.
단어장에 단어가 반드시 1개 이상 존재하는 입력만 주어진다.

출력

화은이의 단어장에 들어 있는 단어를 단어장의 앞에 위치한 단어부터 한 줄에 한 단어씩 순서대로 출력한다.


#include <iostream>
#include <algorithm>
#include <stdio.h>
#include <string>
#include <vector>
#include <map>
using namespace std;
int n, m;
vector <string> voca;
map<string, int> mp;
string v;
bool compare(string a, string b) {
    if (a.size() == b.size() && mp[a] == mp[b]) {
        return a < b;
    }
    else if (mp[a] == mp[b]) {
        return a.size() > b.size();
    }
    return mp[a] > mp[b];
}

int main() {
    
    cin.tie(nullptr); ios::sync_with_stdio(false);
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        cin >> v;
        if (v.size() < m) continue;
        if (mp.find(v)==mp.end()) {
            mp[v] = 0;
            voca.push_back(v);
        }
        mp[v]++;
    }
    sort(voca.begin(), voca.end(), compare);

    for (int i = 0; i < voca.size(); i++) {
        cout << voca[i] << '\n';
    }
}

 

 

 

 

 

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;

public class Main {

    static class Meeting implements Comparable<Meeting> {
        int start, end, people;

        Meeting(int start, int end, int people) {
            this.start = start;
            this.end = end;
            this.people = people;
        }

        @Override
        public int compareTo(Meeting o) {
            return this.end - o.end; // 종료 시간 기준 정렬
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(bf.readLine());

        Meeting[] meetings = new Meeting[n];
        for (int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(bf.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            int people = Integer.parseInt(st.nextToken());
            meetings[i] = new Meeting(start, end, people);
        }

        // 종료 시간 기준 정렬
        Arrays.sort(meetings);

        // DP 배열 및 종료 시간 배열
        int[] dp = new int[n];
        int[] endTimes = new int[n];

        for (int i = 0; i < n; i++) {
            endTimes[i] = meetings[i].end; // 종료 시간만 저장
        }

        dp[0] = meetings[0].people; // 첫 회의 인원 초기화

        for (int i = 1; i < n; i++) {
            // 현재 회의 인원
            dp[i] = meetings[i].people;

            // 이전 회의 중 겹치지 않는 가장 마지막 회의 찾기 (이분 탐색)
            int idx = binarySearch(endTimes, meetings[i].start);
            if (idx != -1) {
                dp[i] += dp[idx];
            }

            // 이전 최대값과 비교
            dp[i] = Math.max(dp[i], dp[i - 1]);
        }

        System.out.println(dp[n - 1]);
    }

    // 이분 탐색: 종료 시간이 target 이하인 가장 마지막 인덱스를 찾음
    private static int binarySearch(int[] endTimes, int target) {
        int left = 0, right = endTimes.length - 1;
        int result = -1;

        while (left <= right) {
            int mid = (left + right) / 2;
            if (endTimes[mid] <= target) {
                result = mid; // 가능한 인덱스 갱신
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return result;
    }
}

'Algorithm' 카테고리의 다른 글

[boj] 이차원 배열과 연산17140 JAVA  (0) 2024.10.27
[boj] 연구소 3_17142 JAVA  (0) 2024.10.21
[boj] Cryptographer’s Conundrum_11269 C++  (0) 2024.10.14
[boj] 양치기꿍_3187 JAVA  (1) 2024.10.07
[boj] 좋은수열_2661 JAVA  (0) 2024.09.30

1. 자바의 메모리 구조

메서드 영역

메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 이 영역은 프로그램의 모든 영역에서 공유한다.

  • 클래스 정보: 클래스의 실행코드(바이트 코드), 필드, 메서드와 생성자 코드 등 모든 실행 코드가 존재한다.
  • static 영역: static 변수들을 보관한다.
  • 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다.

스택 영역

스택 영역은 자바 실행 시 생성이 되는데, 스레드 1개당 하나의 스택이 생성된다. 그리고 메소드가 호출될 때마다 스택 영역에는 스택 프레임이 하나씩 쌓이게 된다.

  • 스택 프레임: 지역 변수(매개변수 포함), 중간 연산 결과, 메소드 호출 정보 등을 포함한다.
    - Local Variables: 메소드의 지역 변수들을 갖는다.
    - Operand Stack: 메소드 내 계산을 위한 작업 공간이다.
    - Constant Pool Reference: 런타임 상수 풀의 참조를 갖는다.

힙 영역

힙 영역은 인스턴스와 배열이 생성되는 곳이다. 가비지 컬렉션(GC)이 주로 이루어지며 더 이상 참조되지 않는 객체는 GC에 의해 청소된다. 쓱싹.


👶 인스턴스는 내부에 변수와 메서드를 가지고 있는데, 그럼 각각의 인스턴스의 메서드는 인스턴스가 생성될 때마다 인스턴스 영역에 들어가게 되는지,,?
👩🏻‍💻 인스턴스들의 필드 값은 모두 다른 주소를 가져야겠지만, 메소드의 코드는 모두 동일합니다. 따라서 메서드는 메서드 영역에서 공통으로 관리됩니다. 즉, 인스턴스의 메서드를 호출하면 힙영역이 아닌 메서드 영역에 있는 코드가 호출이 됩니다.


2. Static 변수 (정적 변수)

static 키워드가 붙는 변수는 모두 메서드 영역(스태틱 영역)에서 관리가 된다. 즉, 인스턴스 영역에 생성되지 않는 것이다.


위와 같이 인스턴스와 함께 생성이 되지 않기 때문에 현재까지 인스턴스를 몇 개를 만들었는지 확인하는 static int count 변수가 사용 가능하다. 즉, 스태틱 변수는 클래스 하나당 하나만 생성이 되며 인스턴스 변수는 인스턴스의 수만큼 생성이 된다.

변수와 생명주기

  • 지역 변수(매개변수 포함): 스택 영역의 스택 프레임에 저장되며 메서드가 종료되면 프레임이 제거됨과 동시에 제거된다.
  • 인스턴스 변수: 힙 영역에 저장되며 인스턴스가 개발자에 의해 제거되거나 GC가 발생하기 전까지 생존한다.
  • 클래스 변수(스태틱 변수, 정적 변수): 메서드 영역의 static 영역에 저장되며 JVM 로딩과 함께 생성되며 JVM이 종료되면 함께 생명이 끝난다.

Static 변수 접근법

1. 인스턴스를 통해 접근

Data data = new Data();
data.count++;

2. 클래스를 통한 접근

Data.count++;

스태틱 변수는 클래스 변수라고 불리는 만큼 클래스를 통해 접근하는 것이 가독성이 훨씬 좋다. 인스턴스 변수에 접근하는 것처럼 보이는 것 보다 한눈에 명확히 클래스 변수에 접근하는 것처럼 보이는 것이 더욱 명확하기 때문이다.

3. Static 메서드 (정적 메서드)

Static 메서드는 정적 변수와 마찬가지로 스태틱 영역에서 관리된다. 따라서 마찬가지로 인스턴스 생성 없이 클래스 명을 통해 호출이 가능하다.

public class Math() {

	public static int add(int a, int b) {
    	return a+b;
    }
    
    public static int subtract(int a, int b) {
    	return a-b;
    }
}

위와 같은 유틸리티 클래스(Utility Class, 정적 메서드만 존재하는 클래스)에서 이러한 접근법은 유용하게 쓰인다.

정적 메서드 접근법

1. 인스턴스를 통해 접근

Math math = new Math();
math.sum(1, 2);

2. 클래스명을 통해 접근

Math.sum(1, 2);

3. static import를 통해 접근 (정적 변수도 import 가능)

import static 패키지명.Math.*;

sum(1, 2);

정적 메서드 사용 시 주의할 점

static 메서드는 static 만 사용 할 수 있다.


생각해보면 당연하다. 정적 메서드는 참조값이 없다. 그런데 인스턴스 변수나 메서드를 사용하려면 참조값을 알아야한다. 따라서 정적 메서드에서 인스턴스 변수나 메서드를 불러오면 자바는 어느 주소를 찾아가야 하는지 알 수 없게 되고(참조값 부재), 컴파일 에러가 뜨게 된다.


4. main() 메서드

인스턴스 생성 없이 사용하는 메서드의 가장 대표적인 예는 main() 메서드이다. 따라서 main에서 호출할 메서드 또한 모두 static 이어야 한다. 변수 또한 마찬가지다.

정적 변수를 작성해서 에러가 나는 경우

public class Math() {
	public int count;
    
    public static void main(String[] args) {
    	count++;
    }
}

정적 메서드를 사용해서 에러가 나는 경우

public class Math() {

    public static void main(String[] args) {
    	test();
    }
    
    public void test() {
    	System.out.println("test!");
    }
}

문제 캡처

 

소스 코드

import java.io.*;
import java.util.*;
import java.util.Map.Entry;

public class Main {

    static Comparator<Integer> comparator(Map<Integer, Integer> map) {
        return (key1, key2) -> {
            int value1 = map.get(key1), value2 = map.get(key2);
            if(value1 == value2) {
                return key1 - key2;
            }
            return value1 - value2;
        };
    };

    public static void main(String[] args) throws IOException {
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(bf.readLine());
        int r = Integer.parseInt(st.nextToken())-1,
        c = Integer.parseInt(st.nextToken())-1,
        k = Integer.parseInt(st.nextToken());

        int[][] arr = new int[101][101];

        for(int i=0;i<3;i++) {
            st = new StringTokenizer(bf.readLine());
            for(int j=0;j<3;j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        int time = 0;
        int rowCnt = 3, colCnt = 3;
        while(arr[r][c] != k && time <= 100) {
            time++;
            if(rowCnt >= colCnt) {
                int col = colCnt;
                //R연산
                for(int i=0;i<rowCnt;i++) {
                    Map<Integer, Integer> freqMap = new HashMap<>();
                    //행 연산 실행
                    for(int j=0;j<colCnt;j++) {
                        if(arr[i][j] == 0) continue;
                        freqMap.put(arr[i][j], freqMap.getOrDefault(arr[i][j], 0)+1);
                    }
                    TreeMap<Integer, Integer> sortMap = new TreeMap<>(comparator(freqMap));
                    sortMap.putAll(freqMap);
                    int nowColCnt = sortMap.size()*2;
                    for(int j=0;j<nowColCnt;j+=2) {
                        Entry<Integer, Integer> value = sortMap.pollFirstEntry();
                        arr[i][j] = value.getKey();
                        arr[i][j+1] = value.getValue();
                    }
                    for(int j=nowColCnt;j<colCnt;j++) {
                        arr[i][j] = 0;
                    }
                    col = Math.max(col, nowColCnt);
                }
                colCnt = col;
            } else {
                int row = rowCnt;
                //C연산
                for(int j=0;j<colCnt;j++) {
                    Map<Integer, Integer> freqMap = new HashMap<>();
                    //열 연산 실행
                    for(int i=0;i<rowCnt;i++) {
                        if(arr[i][j] == 0) continue;
                        freqMap.put(arr[i][j], freqMap.getOrDefault(arr[i][j], 0)+1);
                    }
                    TreeMap<Integer, Integer> sortMap = new TreeMap<>(comparator(freqMap));
                    sortMap.putAll(freqMap);
                    int nowRowCnt = sortMap.size()*2;
                    for(int i=0;i<nowRowCnt;i+=2) {
                        Entry<Integer, Integer> value = sortMap.pollFirstEntry();
                        arr[i][j] = value.getKey();
                        arr[i+1][j] = value.getValue();
                    }
                    for(int i=nowRowCnt;i<rowCnt;i++) {
                        arr[i][j] = 0;
                    }
                    row = Math.max(row, nowRowCnt);
                }
                rowCnt = row;
            }
        }
        
        System.out.println(time>100 ? -1 : time);

    }
}

'Algorithm' 카테고리의 다른 글

Boj_ 회의실 배정 4 java  (0) 2024.11.11
[boj] 연구소 3_17142 JAVA  (0) 2024.10.21
[boj] Cryptographer’s Conundrum_11269 C++  (0) 2024.10.14
[boj] 양치기꿍_3187 JAVA  (1) 2024.10.07
[boj] 좋은수열_2661 JAVA  (0) 2024.09.30

+ Recent posts