본문 바로가기

🌈 Java/Back to Basic 101

10주차 과제: 멀티쓰레드 프로그래밍

학습할 것 (필수)

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

✔️ 개념 정리 

🌟 Process (프로세스)

: 운영체제로 부터 필요한 메모리(자원)를 할당받아 실행중인 프로그램 

  • 코드 / 데이터 / 스택 / 힙 메모리 영역으로 구성 

프로세스 구조와 예시

🌟Thread (쓰레드)

: 프로세스의 자원을 이용해 작업을 수행하는 프로세스를 처리하는 일꾼 같은 역할. (종류 : 싱글 쓰레드 / 멀티 쓰레드)

  • 특징 : 쓰레드는 각자의 개별의 스택을 가지고 프로세스의 전역 메모리 공간을 공유하여 실행 된다. 
장점 단점
 - 사용자 응답성 향상 (병렬 처리, 동시 실행)
 - 자원 공유 효율
   : 번거로운 프로세스간 자원공유를 위한 ipc작업이 없음
- 하나의 스레드에 문제가 있어도 모두 영향을 받는다. 
- 쓰레드가 많으면 context switching 의 부화로 성능 저하 
    : 쓰레드가 많으면 모든 쓰레드를 스케쥴링해함. 

 

 

 

멀티 프로세스는 자원에 문제가 생겨도 다른 프로세스는 영향을 받지 않지만 멀티 쓰레드의 경우에는 하나의 쓰레드에 문제가 생기면 모든 프로세스에 영향이 간다. 

 

 

 

 

 

인터뷰에서 가장 자주 나오는 운영체제에 대한 면접 질문!!!

 

📍 프로세스와 쓰레드의 차이를 설명하세요. 

프로세스 쓰레드
OS로부터 자원을 할당받아 실행
- 독립적
- 자신만의 주소 영역을 가짐 
- 프로세스간 IPC 기법을 통신  
 - 프로세스 로부터 자원을 할당받아 실행 
       * 쓰레드는 개별의 스택을 가지고 프로세스의 전역 메모리 공간을
         공유한다. 
 - 하나의 프로세스 안에 must! 하나 || 여러 멀티 쓰레드 생성 가능 
- 프로세스의 서브셋 
- 주소영역 공유

 

🌟Multi-Tasking (Multi-Processing)

: 운영체제가 지원하는 여러 프로세스가 동시에 실행될수 있는 것 

   - 태스크 : 운영체제에서 처리하는 작업의 단위 (process의 확장 개념)

   - 스케쥴링에 의해 task 여러개를 번갈아가며 수행하여 동시에 처리하는것 처럼 보인다. 

 

 

 

🌟 Parallel-Programming (병렬 프로그래밍)

두 개 이상의 쓰레드가 동시에 병렬로 수행하는 경우 

*. Concurrent Programming 

: 하나의 프로세서/쓰레드 가 입출력 작업 종료를 대기할 동안 다른 프로그램을 수행할 수 있도록 시분할 시스템을 이용하요 병렬로

  처리하여 수행하는 것  

  - 자원 낭비를 막기 위함. 

  - "concurrent" refers to doing multiple tasks at the same time

  -  ex. within Java, running multiple threads are supported for concurrent programming within a single program

# 참고 : www3.ntu.edu.sg/home/ehchua/programming/java/j5e_multithreading.html

 

 

🌟 Multi-Threading (멀티 쓰레딩)

하나의 프로세스 내에 같은 자원을 공유하는 여러개의 쓰레드(실행 단위)가 수행 

   - CPU는 한번에 하나의 작업만을 수행할 수 있으므로 실제로는 동시에 처리되는 작업의 개수와 일치 (아주 짧은 시간동안 여러 작업을 번

     갈아 수행하여 동시에 실행되는 것 처럼 보이게 한다.)

  - 병렬 처리 

장점 단점
 - CPU 사용률 향상
 - 자원을 효율적으로 이용 가능
 - 코드가 간결해짐 (코드를 짜기 나름)
여러 쓰레드가 같은 프로세스의 자원을 공유하여 작업을 하기 때문에 동기화, 교착상태같은 문제 발생 가능 

  - 주의: 멀티 쓰레드라고 무조건 좋은건 아니고 상황에 따라 낮은 성능을 보일 수도 있다. 

 

 

📍 여기서 잠깐! 그러면 여러개의 프로세스와 여러개의 쓰레드 차이는??

멀티 태스킹 멀티 쓰레드
OS에서 지원하는 독립된 메모리를 가져 자원 공유가 이루어지지않는 장점이 있다. 하지만 OS에 부담을 줄 수 있다.  쓰레드끼리 자원 공유가 가능하며 프로그래밍을 통해 구현 

 

🌟 DeadLock (교착상태)

: 2 개 이상의 프로세스가 자원을 점유한 상태에서 다른 프로세스의 작업이 끝나기만을 기다리며 작업을 더 이상 진행하지 못하는 상태 

 

🌟 Semaphore & Mutex

: 임계 구역 (Critical Section) 에 대한 접근을 막기 위한 Locking 매커니즘 필요 

    - semphore (깃발) : 공유자원의 개수를 나타내는 변수 

         - counting semaphore : 도메인 제한이 없어 공유 자원의 개수를 count하고 사용시 하나씩 감소하고 반환하면 세마포어++

         - binary sempahore: 공유자원이 하나이기 때문에 key를 가지거나 안가지는 것으로 분류 

 

Semaphore Mutex (binary sempahore)
signal mechanism  locking mechanism
공유된 자원을 여러 프로세스가 접근하려고 하는 것을 막음. 
 - 임계구역에 여러 쓰레드가 들어갈 수 있음
 - counter을 두어서 동시에 리소스를 접근 할 수 있는 허용 가능한
   쓰레드 수를 제어 
공유된 자원을 여러 스레드가 접근하는 것을 막음. 
 - 임계구역에 하나의 스레드만 들어감 

 

 

operations: wait() / signal()

shared data 를 쓰려고 하는데 공유자원이 이미 사용중이라면 0 으로 사용할 수 없음 으로 wait() 하고 signal() 을 주고 만약 공유 자원이 비워 있다면 1 이라는 signal() 으로 공유자원이 이용가능하다는 signal()을 주어야 함.

 

 

 

 

 

 

🌟 Daemon Thread

: 주 작업 쓰레드를 돕는 보조적인 역할을 하는 쓰레드 . 주 쓰레드가 종료되면 강제적으로 같이 종료 

  - ex. JVM 에서 가비지 컬렉터, 워드 프로세서에서 자동저장, 회원 자동 생산, 미디어 플레이어의 음악 재생 

  - 데몬 쓰레드의 우선순위는 자신을 생성한 쓰레드와 같은 우선순위를 가지게 됨. 

 

✔️ Thread 클래스와 Runnable 인터페이스

: 자바에서 쓰레드를 생성하는 방법은 두가지 방법이 있습니다. 

   ** 쓰레드 구현 = 쓰레드를 통해 작업할 내용을 run() 안에 지정

     1. Thread 클래스 상속

     2. Runnable 인터페이스 

 

🌟 Thread 클래스 상속  

: 자식 클래스는 실행 메소드 재정의해야 인스턴스 할당하고 실행 가능 

public class MyThread extends Thread {
	@Override
    public void run() {
    	System.out.println("Thread클래스를 상속받아 만든 쓰레드 입니다.");
    }
}

📍 Thread 클래스 

   ✔️ 필드

         : 쓰레드의 우선순위에 대한 상수 필드 (우선순위, 우선순위 최소값, 우선순위 최대값)

 

   ✔️생성자

  • gname = 스레드 이름 자동생성
  • name = 인자 (매개변수에 들어갈 값) 새로운 스레드 이름
  • targe : run()메소드가 호출될 객체
  • group : 생성할 스레드를 설정할 스레드 그룹
  • stacxkSize : 새로운 스레드 스택 사이즈 (할당할 주소 공간-바이트수)

    ✔️메소드  

  • public void run() : 쓰레드가 실행되면 run() 메소드를 호출하여 작업
    • 메소드 수행이 종료되면 호출 스택이 비워지면서 생성된 호출 스택도 소멸됨. 
  • public synchronized void start() : 쓰레드를 실행시키는 메소드. start 메소드가 호출되면 실행 대기 상태에 있다가 자신의 차례가 되면 실행됨. 
    • IllegalThreadException : 두번이상 호출되면 에러. 한번만 호출 가능 

   - start 메소드를 호출해서 쓰레드 실행. run()은 쓰레드를 실행하는 것이 아닌 메소드를 호출하는 역할. 

  - 쓰레드의 스케쥴링과 관련된 메소드 (참고)

🌟 Runnable Interface 구현 

public class MyThread implements Runnable {
	@Override
    public void run() {
    	System.out.println("Runnable interace로 구현한 쓰레드");
    }
}



@FunctionalInterface
public interface Runnable {
		public abstract void run(); 

}

 

📍 Thread 클래스를 상속(extends) 받으면 다른 클래스를 상속 받을 수 없기 때문에 Runnable 인터페이스를 구현하는 방법이 일반적 

      -  reusability & high code consistency 유지 -> 객체지향적인 방법 

✔️ 쓰레드의 상태

: 구조 -> New/ Runnable / Blocekd / Waiting, Timed_waiting / Terminated 

  - 정교한 스케쥴링을 통해 프로세스에게 주어진 자원을 여러 쓰레드가 낭비없이 효율적으로 사용하도록 프로그래밍 

     : 쓰레드 객체가 start() 를 호출하면 쓰레드가 실행되는 것 처럼 보이지만 실행 대기 상태가 된다. 스케쥴링으로 선택된 쓰레드가 CPU를

       점유하고 run() 실행. (New -> Runnable -> Running -> Runnable -> Running -> Terminated)

        실행 중이 쓰레드가 Runnable 상태에 들어갔을때 CPU 는 다른 쓰레드를 선택하여 실행한다. 

 

스레드의 생성부터 소멸까지의 구조

1. 쓰레드를 생성하고 start() 를 호출하면 스케쥴링 상 본인의 차례가 되면 run() 실행 

   - 대기열은 Queue 구조로 FIFO 

2. 본인 차례에 실행 

3. 할당된 실행 시간이 다되거나 yield() - 양보 메소드를 만나면 다시 실행 대기 상태 (Runnable)이 되고 스케쥴링 상 지정된

     다음 쓰레드가 실행 상태가 됨. 

4. 실행 중, suspend() / sleep()/ wait() / join()/ I/O Block에 의해 일시정지 상태가 될 수 있음. 

    - I/O block은 입출력 작업에서 발생하는 지연 상태 : 사용자 입력 받는 경우 

5. 일시정지 시간이 끝나거나 notify() / resume() / interrupt() 가 호출되면 다시 대기열에 저장되어 차례기다리는 상태로 변경 

6. 실행을 모두 마치거나 stop() 호출시 쓰레드 소멸 

✔️ 쓰레드의 우선순위

: 쓰레드 클래스에는 우선순위라는 맴버변수 속성을 가지고 있는데 이 우선순위에 따라 쓰레드가 얻는 실행 시간이 달라진다. 

   - 작업의 주요도에 따라 우선순위를 다르게 지정하여 특정 쓰레드가 더 많은 작업 시간을 갖도록 한다. 

   - 쓰레드가 가질수 있는 우선순위의 범위는 1-10이며 숫자가 높을수록 우선순위가 높음. 

   - 메인 메소드 수행하는 쓰레드는 우선순위가 5 이므로 그 안에 생성하는 쓰레드는 default = 5

final static int MIN_PRIORITY = 1 우선 순위의 최소값
final static int NORM_PRIORITY = 5 기본 우선순위 값
final static int MAX_PRIORITY = 10 우선 순위의 최대값
setPriority(int newPriority) 쓰레드의 우선순위를 지정한 값으로 변경 
getPriority() 쓰레드의 우선순위 반환 

✔️ Main 쓰레드

: JVM 은 하나의 프로세스이고 Java Application은 기본적으로 하나의 메인 쓰레드를 가지고 있음. 

    - 모든 쓰레두눈 메인 쓰레드로부터 생성됨. 

✔️ 동기화

: 시간에 딱딱 맞춰서 일을 진행하여 다른 두시간을 하나로 합치는 행위  -- 종류 : 동기식 (synchorinzed) / 비동기식 (asynchorinzed)  

 

동기식 : 멀티 쓰레드 환경에서 여러 쓰레드가 같은 프로세스의 자원을 공유하여 작업하다보니 영향을주기 때문에 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 하는 것 ( 중간에 interrupt 불가능) 

<--> 비동기식 (쓰레드 진행 중에도 다른 쓰레드가 방해하여 끊을 수 있다) - ex. messaging 

 

📍 이해를 돕기위한 간단한 예시,

  동기 -> 라면을 끓이는 중에는 다른 일 멀티테스킹 불가함. 

  비동기 -> 라면 물이 끓는동안 청소기도 돌리고 다시 돌아와서 라면과 스프를 넣고 끓는동안 빨래도 넣어두고 다시 돌아온다. 

 

✔️ 동기식

: 공유 데이터를 사용하는 코드를 임계 구역 (critical section) 으로 지정하고 lock을 가지고 있는 쓰레드만 이 영역 내의 코드를 수행할수 있도록 지정해준다. lock 을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 코드 수행 

** 임계 구역 : 공유 자원 점근 순서에 따라 실행 결과가 달라지는 프로그램 영역 

   - 임계구역 문제 해결 조건 

  • 상호 배제 (mutual exclusion)
  • 한정 대기 (bounded waiting)
  • 진행의 융통성 (progress flexibility)

🌟 Synchornized Block

: 프로그램의 성능을 위해 메소드 전체에 락을 걸기보다 블럭으로 임계구역 최소화해서 보다 효율적인 프로그램 생성

class Account {
	private int balance = 1000;
    
    public int getBalance() {
    	return balance;
    }
    
    // 1. 메소드 전체를 임계 구역으로 지정
    // : 메소드가 호출된 시점부터 lock을 얻어서 작업을 하고 메소드가 종료되면 lock반환 
    public synchorinzed void withdraw(int money) {
    	if(balance >= money) {
        	try {
            	Thread.sleep(1000);
            } catch (InterruptedException e) {
            
            }
            balance -= money;
        }
    }
}

 

 🌟 wait() / notify() 의 역할 

: 동기화를 통해 공유 데이터를 보호할 수 있지만 특정 쓰레드가 객체의 락을 너무 오래동안 가지지 않는것도 중요 

     - 특정 쓰레드의 락이 반환되기를 너무 오래 기다리면 작업이 원활하지 않다. :( 

  그러므로, 임계구역에서 lock 이 필요한 상황이 아니라면 wait() 을 통해 락을 반납하고 다른 쓰레드에게 작업 수행 권한을 넘겨주고 작업을

  다시 진행할때 notify() 를 통해서 락을 받아 다시 작업을 진행 합니다. 

wait()  락을 반납하고 기다리는 상태로 변경 
notify() 대기중인 쓰레드 중 임의의 쓰레드에게 lock 을 얻을 수 있는 상태로 변경 
notifyAll() 기다리고 있는 모든 객체에게 lock을 얻을 수 있는 상태로 변경  - waiting pool 대기중이 쓰레드 해당

🌟 Starvation (기아상태) & Race Condition (경쟁 상태)

   ✔️ starvation : 오래동안 lock 을 얻지 못하고 기다리는 형상 

      - 우선순위에서 밀려 특정 쓰레드가 계속 기다리는 상태로 남아있는 경우

   ✔️ Race Condition : 모든 쓰레드가 notifyAll() 을 통해 통지 받기에 불필요한 쓰레드까지 락을 얻으려고 경쟁하는 현상 

 

  ->> 해결 방법 : Lock 과 Condition 이용하여 선별적인 통지 

 

🌟Lock 클래스 

ReetrantLock 재진입이 가능한 lock ; 가장 일반적인 배타(exclusive) lock
ReentrantReadWriteLock 읽기에는 공유적이고 쓰기에는 배타적인 lock 
- 일기를 위한 lock 과 쓰기를 위한 lock 을 제공
- 읽기 lock 에 걸린 상태에서 동시에 여러 쓰레드가 읽기 lock 을 얻는것은 가능하지만 다른 쓰기 lock 을 거는     것은 불가능  (읽기 - 읽기 lock /  쓰기 - 쓰기 lock)
StampedLock ReentrantReadWriteLock 에 낙관적인 lock 기능 추가 
 - 낙관적 읽기 락 (optimistic reading lock) 기능 추가 
 - 낙관적 읽기 lock 은 쓰기 lock 에 의해 풀림. 
int getBalance() {
	long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock 
    
    int curBalance = this.balance; // 공유 데이터인 balance 읽어옴. 
    
    if(lock.validate(stamp)) { // 쓰기 lock 에 의해 낙관적 읽기 lock 풀렸는지 확인 
    	stamp = lock.readLock(); // lock이 풀렸으면, 읽기 lock 을 얻으려고 기다린다. 
        try{
        	curBalance = this.balance; // 공유 데이터 다시 읽어오기
        } finally {
        	lock.unlockRead(stamp); // 읽기 lock 풀기 
        }
    }
    return curBalance; //낙관적 읽기 lock 이 풀리지 않았으면 읽어온 값 반환
}

 

🌟Condition

: wait() / notify() -쓰레드 구분하지 못함-의 단점인 경쟁상태 해결   

   - waiting pool 에서 세분화하여 쓰레드의 종류에 따라 구분하여 넣음 (경쟁 상태 발생 가능성을 낮출 수 있음)

private ReentrantLock lock = new ReentrantLock(); //lock 생성

//lock condition 생성 
private Condition forBanker = lock.newCondition();
private Condition forCustomer = lock.newCondition();
Object Condition
wait() await()/ awaitUninterruptibly()
wait(long timeout) boolean await(long time, TimeUnit unit) / awaitNanos(nanosTimeout) /
boolean awaitUntil(Date deadline)
notify() signal()
notifyAll() signalAll()

** notify() + wait() : 대상이 무엇인지 보이지 않음. 

    await() + signal() : 대기와 통지의 대상이 명확하게 보임. 👍👍

 

🌟 Fork() / Join()

 - fork() :  쓰레드 풀의 작업큐에 넣기 / asynchronous method

      * 호출만 하고 결과를 기다리지 않음 -- 기다리지 않고 다음줄 명령어 실행 

 - join() : 해당 작업의 수행 끝을 기달렸다가 수행이 끝나면 그 결과를 반환 / synchronous method 

 

✔️ DeadLock (교착 상태)

 

 

예시 :

다섯명의 사람이 원형 테이블을 공유하는데 테이블 중앙에 밥이 있다. 서로 소통하지 않고 왼쪽에 있는 젓가락과 오른쪽에 있는 젓가락으로 젓가락을 2개 집었을 경우 식사를 할 수 있다. 식사를 마치면 젓가락 2개를 모두 놓고 다시 생각하고 누가 쓰고 있을 때는 젓가락을 사용하지 못한다. 

 

- 교착 상태 발생하는 4 가지 조건

상호 배제 (mutual exclusion) 한번에 한개의 프로세스만 공유 자원을 사용할수 있다. 
자원을 공유하지 못하면 교착 상태가 발생합니다. 자원은 배타적인(exclusive) 자원으로 임계구역에서 보호되어 다른 프로세서(쓰레드)가 동시에 사용할 수 없습니다. 
비선점 (non-preemption) 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 강제로 빼앗을 수 없습니다. 그러므로 상대가 자원을 놓을때까지 기다려야 함으로 교착상태 발생 
점유와 대기 (hold and wait) 자원 하나를 잡은 상태에서 다른 자원을 추가로 점유하기 위해 기다리면 교착 상태 발생 
원형 대기 (circular wait) 자원을 요구하는 방향이 원을 이루면 양보가 불가능하기 때문에 교착 상태 발생 

 - 교착 상태 해결 방법

교착 상태 예방 네가지 조건중 하나를 없앱니다. 
교착 상태 회피 교착 상태가 발생하지 않는 수준으로 자원 할당 
교착 상태 검출 자원 할당 그래프를 사용해 교착 상태 발견 
교착 상태 회복 검출한 후 해결 

 

 - 세마포어 해결방법 (Semaphore Solution)

semaphore chopstick[5] 젓가락을 하나의 세마포어로 표현한다. 
wait()  젓가락을 집으려고 한다 
signal() 젓가락을 놓는다. 
while(true) {
	wait(chopstick[i]);
    wait(chopstick[(i+1) % 5]);
    // eat
    signal(chopstick[i]);
    signal(chopstick[(i+1) % 5]);
    // think
}

 -- 교착 상태 야기 가능성 있음

1. 최대 4명의 사람만 테이블에 앉게 한다. 

2. 한 사람이 두개를 모두 집을수 있을때만 젓가락을 집도록 한다. 

3. 비대칭 해결안 사용 

    - 홀수번째 사람은 왼쪽 젓가락부터 집고 짝수는 오른쪽 젓가락부터 집는다.