[Java] Ch.13 쓰레드

yoons(이윤서)·2024년 7월 19일

[Java] 자바의 정석

목록 보기
13/14

👉🏻 이 글은 자바의 정석(3판) Ch.13을 공부하며 적은 내용입니다.

📌 프로세스와 쓰레드

- 프로세스(process)

: '실행 중인 프로그램'

  • 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다.
  • 프로세스는 프로그램을 실행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있다.

- 쓰레드(thread)

: 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것.

모든 프로세스에는 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 '멀티쓰레드 프로세스' 라고 부른다.

- 멀티 테스킹 / 멀티 쓰레딩

  • 멀티 테스킹 (다중 작업) : 대부분의 OS는 멀티테스킹을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있다.

  • 멀티 쓰레딩 : 하나의 프로세스 내에서 둘 이상의 쓰레드가 동시에 작업을 수행하는 것이다.
    CPU의 사용률을 향상시킨다, 각 쓰레드가 프로세스의 메모리를 공유하므로 시스템 자원의 낭비가 적다, 사용자와의 응답성이 좋아진다.
    멀티 스레드 프로그램은 공유하는 자원에 대해 동기화 문제가 발생할 수 있다.
    CPU의 core는 한 번에 단 하나의 작업만 수행할 수 있으므로 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다.

-> 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하며 작업을 하기 때문에 동기화, 교착상태 같은 문제들 발생 가능


📌 쓰레드의 구현과 실행

Tread 클래스를 상속받는 방법과 Runnable인터페이스를 구현하는 방법 두 가지가 있다. 차이는 별로 없지만 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에 일반적으로 Runnable인터페이스를 구현한다.

Thread를 상속받을 경우와 Runnable인터페이스를 구현할 때의 인스턴스 생성방법이 다르다.

1. Thread클래스 상속받기

class MyThread extends Thread {
	public void run() { /*작업내용*/ }	// Thread클래스의 run()을 오버라이딩
}

- 인스턴스 생성

MyThread t = new MyThread();	// Thread 자손 클래스의 인스턴스 생성

2. Runnable인터페이스 구현하기

class MyThread implements Runnable {
	public void run() { /*작업내용*/ }	// Runnable인터페이스의 run()을 구현
}

Runnable인터페이스는 오직 run()만 정의되어 있는 간단한 인터페이스이다.

// Runnable인터페이스 내용
public interface Runnable{
	public abstract void run();
}

"쓰레드를 구현한다" = 쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통 { }을 채운다는 것이다.

- 인스턴스 생성

class Thread_2 implements Runnable {
	public void run() {
    	...
    }
}

Runnable r = new Thread_2();
Thread t2 = new Thread(r);		//생성자 Thread(Runnable target)

// ⬇️ 위의 두 줄을 간단히
Thread t2 = new Thread(new Thread_2());

Runnable인터페이스를 구현한 Thread_2에는 멤버가 run()밖에 없기 때문에 쓰레드의 이름을 호출할 때 Thread.currentTread().getName();와 같이 해야한다.

👉🏻 쓰레드의 실행

  • start()를 호출해야만 쓰레드 실행됨.
  • 한 번 실행이 종료된 쓰레드는 다시 시작할 수 없다.
  • 다시 수행하려면 새로운 쓰래드를 new로 다시 생성한다.
  • run()은 쓰레드를 실행시키는 것이 아님. 단순히 클래스에 선언된 매서드를 호출하는 것임.
    ➡️ 반면 start()는 쓰레드가 작업 실행에 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서 생성된 스택에 run()이 첫번째로 올라가게 한다. (독립된 공간에서 작업을 수행)

  • 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.


📌 싱글쓰레드 & 멀티쓰레드

두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다.
Ex. 사용자로부터 데이터 입력받기, 네트워크로 파일 주고받기....

import javax.swing.JOptionPane;

public class TreadEx {
	public static void main(String[] args) throws Exception{
		ThreadEx th = new ThreadEx();
		th.start();
		
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 "+input+"입니다.");
	}
	
	class ThreadEx extends Thread{
		public void run() {
			for(int i=10; i>0; i--) {
				System.out.println(i);
				try {
					sleep(10000);
				} catch(Exception e) {}
			}
		}
	}
}

이렇게 하면 사용자가 입력을 마치지 않았어도 숫자가 출력되는 것을 볼 수 있다.

📌 쓰레드의 우선순위

  • 쓰레드의 우선순위는 실행하기 전에만 변경할 수 있다.
  • void setPriority(int newPriority) : 우선순위 변경
  • int getPriority() : 우선순위 반환
  • MAX_PRIORITY = 10 : 최대 우선순위
  • MIN_PRIORITY = 1 : 최소 우선순위
  • NORM_PRIORITY = 10 : 중간 우선순위

📌 쓰레드 그룹

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다.
-> 만약 쓰레드 그룹 생성자를 사용하지 않으면 자기 자신의 쓰레드 그룹에 포함되어 있는 것으로 된다.

쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 묶어서 관리할 수 있게 한다.
자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만, 다른 쓰레드 그룹의 쓰레드를 변경할 수 없다.

ThreadGroup(String name) : 지정된 이름의 새로운 쓰레드 그룹을 생성

  • 쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용해야한다.
    Thread(ThreadGroup group, String name)
    Thread(ThreadGroup group, Runnable target)
    Thread(ThreadGroup group, Runnable target, String name)
    Thread(ThreadGroup group, Runnable target, String name, long stackSize)

📌 데몬 쓰레드

일반 쓰레드를 보조하는 역할로, 일반 쓰레드가 모두 종료되면 자동으로 종료된다.
Ex. 가비지 컬렉터, 워드 자동저장


📌 쓰레드 실행제어

쓰레드 프로그래밍이 어려운 이유는 스케줄링(scheduling)과 동기화(synchronization)때문이다.

쓰레드의 메서드

: sleep(ling millis), join() - 다른 쓰레드의 작업을 기다린다, interrupt() - 깨워서 실행대기상태, [ stop() - 즉시종료, suspend() - 일시정지, resume() - 실행대기상태로 ], yield() - 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태

[ ]는 쓰레드를 교착상태로 만들기 쉽기 때문에 deprecated되었다.

쓰레드의 상태

NEW : 쓰레드가 생성되고 start()가 호출되지 않은 상태
RUNNABLE : 실행중, 실행가능 상태
BLOCKED : 동기화 블럭에 의해 일시정지된 상태
WAITING, TIMED_WAITTING : 작업이 종료되지는 않았지만 실행가능하지 않은 일시정지 상태 (후자는 정지시간이 지정된 경우)
TERMINATED : 쓰레드의 작업이 종료된 상태


sleep() : 일정시간동안 쓰레드를 멈추게한다.

항상 try-catch문으로 예외를 처리해줘야 한다.
매번 처리해주기 번거롭기 때문에 매서드로 만들어서 사용하기도 한다.

void delay(ilong millis) {
	try{
    	Tread.sleep(millis);
    } catch(InterruptedException e) {}
}

실제로 영향 받는 것은 main메서드를 실행하는 main쓰레드이다.

join() : 다른 쓰레드의 작업을 기다린다.

th1.join();, th1.join(long millis); : 현재 실행중인 쓰레드가 쓰레드 th1의 작업이 끝날 때까지 기다린다.

sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, 호출부분을 try-catch로 감싸야한다.


📌 쓰레드의 동기화

멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.

그래서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 "임계 영역(critical section)"과 "잠금(lock)"이다.

공유 데이터를 사용하는 코드 영역을 임계영역으로 지정해놓고, 공유 데이터가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다.

이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화(synchronization)'라고 한다.


1. synchronized를 이용한 동기화

  • synchronized : 임계영역 설정

    가장 간단한 동기화 방법이다.
    메서드 전체를 임계영역으로 지정하거나 특정한 영역을 임계영역으로 지정할 수 있다.
	public void withdraw(int money){
        if(balance >= money){
            try{ Thread.sleep(1000); } catch(Exception e){}
            balance -= money;
        }
    }

잔고를 확인하는 if문과 출금하는 문장은 하나의 임계영역으로 묶어져야 한다.
⬇️
1. 메소드에 synchronized 붙이기
: 한 쓰레드에 의해 먼저 withdraw가 호출되면, 이 메서드가 종료되어 lock이 반납될 때까지 다른 쓰레드는 withdraw를 호출하더라도 대기상태에 머물게 된다.

	public synchronized void withdraw(int money){
        if(balance >= money){
            try{ Thread.sleep(1000); } catch(Exception e){}
            balance -= money;
        }
    }
  1. synchronized 블럭 사용
	public void withdraw(int money){
    	synchronized(this){
        	if(balance >= money){
            	try{ Thread.sleep(1000); } catch(Exception e){}
            	balance -= money;
            }
        }
    }

wait()과 notify()

wait()에 의해 lock을 반납했다가, 나중에 다시 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서 다시 lock을 얻어서 임계영역에 들어와 중단했던 쓰레드를 다시 진행한다. 이것을 재진입(reentrance)이라고 한다.

매개변수가 있는 wait()은 지정된 시간동안만 기다린다.

2. Lock과 Condition을 이용한 동기화

'java.util.concurrent.locks'패키지가 제공하는 lock클래스들을 이용하는 방법이다. 가은 메서드 내에서만 lock을 걸 수 있다는 제약이 불편할 때 이 lock클래스를 이용한다.

lock 클래스의 종류 3가지

  1. ReentrantLock : 재진입 가능 lock. 가장 일반적인 배타 lock
  2. ReentranrReadWriteLock : 읽기에는 공유적, 쓰기에는 배타적
  3. StampedLock : ReentrantReadWriteLock에 낙관적인 기능 추가
    -> 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것이다.

profile
개발공부하는 잠만보

0개의 댓글