본문 바로가기
Study/Java_study

Java_study_10 (멀티쓰레드 프로그래밍)

by hjshims 2021. 8. 30.

♨학습내용

더보기

 Thread 클래스와 Runnable 인터페이스

 쓰레드의 상태

쓰레드의 우선순위

 Main 쓰레드

 동기화

데드락

 

  • 개념정리
  • 프로세스(process)

- 사전적 의미로는 일의 과정이나 공정을 의미한다.

- 여기선 실행중인 프로그램을 의미한다.

- 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.

 

  • 쓰레드(Thread)

- 프로세스라는 작업 공간내에서 실제로 작업을 처리하는 역할이다.

- 프로세스의 자원을 이용해서 작업을 수행한다.

- 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재한다.

- 쓰레드가 하나이면 싱글 쓰레드, 둘 이상이면 멀티 쓰레드 라고 한다.

 

  • 멀티 태스킹(multi-tasking)

- 대부분의 OS가 지원한다.

- 여러 개의 프로세스가 동시에 실행 될 수 있는 것을 말한다.

 

  • 멀티 쓰레딩(multi-threading)

- 하나의 프로세스 내에서 둘 이상의 쓰레드가 동시에 작업을 수행하는 것을 말한다.

- CPU의 코어가 한번에 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 갯수와 일치한다.

- 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써, 여러 작업들이 모두 동시에 수행되는 것처럼 보이게한다.

- 프로세스의 성능은 쓰레드의 개수와 비례하지 않는다.

 

- 장점: CPU의 사용률 향샹, 자원을 보다 효율적으로 사용, 작업이 분리되어 코드가 간결해진다.

- 단점: 멀티쓰레딩은 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에, 발생할 수 있는 동기화(synchronization), 교착상태(deadlock)와 같은 문제들을 고려해서 신중히 프로그래밍 해야한다.

 

 


 

  • Thread 클래스와 Runnable 인터페이스

- 쓰레드를 구현하는 방법은 2가지가 있다.

- Thread클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법

 

  • Thread 클래스

- Thread 레퍼런스 객체를 생성하여 run() 함수(Method)를 오버라이드 해주는 방법이다.

 

public class Test extends Thread {
    @Override
    public void run() {
        System.out.println("Thread run");
    }
    
    public static void main(String[] args) {
        Test test = new Test();
        test.start();
    }
}

 

- 첫번째 방법으로는 위 예제 코드처럼 Thread 클래스를 상속받은 클래스를 작성하는 방법이 있다.

- Thread 클래스의 run() 메서드는 새 쓰레드를 만들며, Thread를 상속받은 클래스에서는 이 run() 메서드를 오버라이딩해서 사용해야 한다.

- run메서드는 단순 return하도록 작성되어 있기 때문에, 오버라이딩 하지 않으면 쓰레드가 바로 종료된다.

- 그런데, 예제 코드에서 보면 Test 객체 test를 만들고는 test의 run이 아니라 start() 메서드를 호출한 것을 볼 수 있다.

- 만약 run 메서드를 호출하게 되면 쓰레드가 생성만 되고 Runnable한 상태가 되지 않기 때문에, 정상적으로 실행되지 않는다.

- 그래서 start 메서드를 통해 실행시켜줘야 한다.

- start()는 Thread 클래스에 구현된 메서드이며, 오버라이딩 해서는 안된다.

- start()는 생성된 쓰레드 객체를 Runnable하게 전환시킨 후 JVM에 의해 이 쓰레드가 선택되면 run 메서드가 호출되고 실행된다.

 

 

  • Runnable 인터페이스

- Runnable 인터페이스를 구현해서 만드는 구현체이다.

 

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(new Foo());
        thread.start();
    }
}

class Foo implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread run");
    }
}

 

- Runnable 인터페이스를 구현해서 만드는 구현체에는 start() 메서드가 존재하지 않는다.

- 그래서 별도의 Thread 객체를 생성하고, 이 때 Runnable 인터페이스의 구현체를 인자로 넘겨주어야 한다.

- Runnable 인터페이스의 구현체로 만드는 쓰레드도, Thread 클래스를 상속해서 만드는 쓰레드와 마찬가지로 start()를 통해 실행시킨다.

 


 

  • 쓰레드의 상태

- 쓰레드는 총 6가지의 상태를 가지며 이 상태들은 JVM에 의해 관리된다.

 

상태(State) 설명(desciprtion)
NEW 쓰레드 객체는 생성되었지만, 실행되지 않은 상태, start()를 호출해주어야 Runnable 상태가 된다.
RUNNABLE 쓰레드가 실행되고 있거나 실행 준비되어 JVM의 스케줄링을 기다리는 상태이다.
BLOCKED 쓰레드가 실행 중지 상태이며, 모니터락(Moniter Lock)이 풀리기를 기다리는 상태이다.
WAITING 쓰레드가 대기중인 상태이다.
TIMED_WAITING 특정 시간만큼 쓰레드가 대기중인 상태이다.
TERMINATED 쓰레드가 종료된 상태로, 한 번 TERMINATED 상태로 돌입한 쓰레드는 다른 상태가 될 수 없다.

 

 


 

  • 쓰레드의 우선순위

- 자바에서 Thread(쓰레드)는 Priority(우선순위)에 관한 필드를 가지고 있다.

- 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드에 우선순위를 부여하여 쓰레드 별로 작업시간을 다르게 부여할 수 있다.

- 우선순위가 높은 쓰레드 일수록 더 많은 작업시간을 할당받는다.

- 쓰레드의 우선순위는 절대적인 값이 아니라 상대적인 것이며, 쓰레드 클래스 내부에 멤버 변수로 존재한다.

 

필드 설명
static int MAX_PRIORITY      (10) 쓰레드가 가질 수 있는 최대 우선순위를 명시한다. 
static int MIN_PRIORITY        (1) 쓰레드가 가질 수 있는 최소 우선순위를 명시한다. 
static int NORMAL_PRIORITY (5) 쓰레드가 생성될 때 가지는 기본 우선순위를 명시한다. 

 

- 쓰레드가 우선순위로 보통 값(NORMAL_PRIORITY) 5를 가지고 있기 때문에 main 쓰레드의 모든 자식 쓰레드들은 5의 우선순위를 가지고 생성된다.

- 쓰레드의 setPriority(int newPriority) 메서드를 사용하면 우선순위를 newPriority 값으로 바꿀 수 있다.

 


 

  • Main 쓰레드

- 모든 자바 애플리케이션은 main thread가 main() 메서드를 실행하면서 시작한다. 

- 메인 쓰레드는 main() 메서드의 첫 코드부터 아래로 순차적으로 실행하고, main() 메서드의 마지막 코드를 실행하거나 return문을 만나면 종료된다.

- 메인 쓰레드는 필요에 따라 작업 쓰레드를 만들어서 병렬로 코드를 실행한다. 

 

https://blog.naver.com/umjaejeong2/222247340200

 

- 메인쓰레드가 실행되면 그 안에서 멀티쓰레드 구현이 가능해진다.

- 여기서 주의할 점은 싱글쓰레드의 경우, 메인쓰레드가 종료되면 프로세스가 모두 종료되지만,

- 멀티쓰레드의 경우 메인쓰레드가 종료되어도 프로세스는 종료되지 않는다는 점을 주의해야한다.

 

https://blog.naver.com/umjaejeong2/222247340200

 

 


 

 

  • 동기화

- 멀티쓰레드 프로세스의 경우, 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하는 것을 말한다.

- 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 말한다.

- 즉, 현재 실행중인 쓰레드를 제외하고, 나머지 쓰레드에서는 데이터에 접근할 수 없도록 막는 개념이다.

- 멀티쓰레드를 잘 사용하면 성능을 증가시키지만, 공유자원에 대한 동기화가 없는 경우에는 데이터의 안정성과 신뢰성을 보장할 수 없다.

 

  • synchronized를 이용한 동기화

- synchronized가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어서 작업을 수행하다가 메서드가 종료되면 lock을 반환한다.

- lock을 걸고자 하는 객체를 참조변수로!

- 임계영역(critical section)은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에, 가능하면 임계영역을 최소화 해서 효율적으로 돌아가도록 해야한다.

 

// 1. 메서드 전체를 임계 영역(critical section)으로 지정
public synchronized void calcSum() {
    //...
}

// 2. 특정한 영역을 임계 영역으로 지정
synchronized (객체의 참조변수) {
    //...
}
class ThreadEx {
	public static void main(String args[]) {
		Runnable r = new RunnableEx();
		new Thread(r).start();
		new Thread(r).start();
	}
}

class Account {
	private int balance = 1000; // private으로 해야 동기화가 의미가 있다.

	public int getBalance() {
		return balance;
	}

	public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화한다.
		if(balance >= money) { // 이 구분을 통과하고 출금하기 직전에 다른 쓰레드가 끼어들면 잔고가 마이너스가 된다.
			try { Thread.sleep(1000);} catch(InterruptedException e) {}
			balance -= money;
		}
	}
}

class RunnableEx implements Runnable {
	Account account = new Account();

	public void run() {
		while(account.getBalance() > 0) {
			// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
			int money = (int)(Math.random() * 3 + 1) * 100;
			account.withdraw(money);
			System.out.println("balance:" + account.getBalance());
		}
	}
}

 

- 아래처럼 설정도 가능하다.

public void withdraw(int money){
		synchronized(this){
			if(balance >= money) {
				try { Thread.sleep(1000);} catch(InterruptedException e) {}
				balance -= money;
			}
		}
	}

 

 

  • wait()와 notify()

- 임계영역(critial section)의 코드를 더이상 진행할 수 없으면, wait() 호출하여 쓰레드가 lock을 반납하고 기다리게 한다.

- lock이 반납되면 다른 쓰레드가 lock을 얻어 해당 객체에 대한 작업을 진행 할 수 있다.

- notify()를 호출해서 작업을 중단했던 쓰레드가 다시 lock을 얻어서 진행한다. (재진입, reentrance)

- 그렇지만, 오래 기다린 쓰레드가 lock을 얻는다는 보장이 없다.

- 이러한 현상을 기아현상(starvation) 이라고 하는데, notifyAll()을 사용해서 해결 가능하다.

- race condition : 여러 쓰래드가 lock을 얻기 위해 서로 경쟁하는 것이다.

 


 

  • 데드락

- 2개 이상의 프로세스가 다른 프로세스의 작업이 끝나기만 기다리며 작업을 더 이상 진행하지 못하는 상태를 교착 상태 (dead lock)이라고 한다.

- 교착 상태가 발생하기 위해서는 아래의 4가지 조건을 만족해야 한다. 이 4가지 조건을 교착 상태의 필요조건 이라고 한다.

  1. 상호 배제 (Mutual exclusion)
    - 자원을 공유하지 못하면 교착 상태가 발생한다. 여기서 자원은 배타적인 자원이어야 한다. 배타적인 자원은 임계구역에서 보호되기 때문에 다른 쓰레드가 동시에 사용할 수 없다.
  2. 비선점 (No preemption)
    - 자원을 빼앗을 수 없으면 자원을 놓을 때까지 기다려야 하므로 교착상태가 발생한다.
  3. 점유와 대기 (Hold and wait)
    - 자원 하나를 잡은 상태에서 다른 자원을 기다리면 교착 상태가 발생한다.
  4. 원형 대기 (Circular wait)
    - 자원을 요구하는 방향을 원을 이루면 양보를 하지 않기 때문에 교착상태가 발생한다.

- 위 조건들 중 한가지라도 만족하지 않으면 교착상태는 일어나지 않는다.

 

 

 

'Study > Java_study' 카테고리의 다른 글

Java_study_12 (애노테이션)  (0) 2021.09.14
Java_study_11 (Enum)  (0) 2021.09.07
Java_study_9 (예외처리)  (0) 2021.08.23
Java_study_8 (인터페이스)  (0) 2021.08.17
Java_study_7 (패키지)  (0) 2021.08.11