Process & Thread

이재현·2024년 7월 31일

Java

목록 보기
13/15
post-thumbnail

🩵 Process와 Thread

💙 Process란?

운영체제로부터 자원을 할당 받는 작업의 단위로, 프로세스는 실행 중인 프로그램을 의미한다.

즉, OS 위에서 실행되는 모든 프로그램은 OS가 만들어준 프로세스에서 실행된다.

OS가 프로세스를 할당할 때 프로세스 안에 프로그램의 코드와 데이터, 메모리영역을 함께 할당해준다.

  • Stack : 지역변수, 매개변수 리턴 변수를 저장하는 공간
  • Heap : 프로그램이 동적으로 필요한 변수를 저장하는 공간 (new(), mallock()))

💙 Thread란?

프로세스가 할당 받은 자원을 이용하는 실행의 단위로, 쓰레드는 일종의 코드 실행의 흐름이다.

프로세스가 작업 중인 프로그램에서 실행 요청이 들어오게 되면 쓰레드를 만들어서 명령을 처리하도록 한다.
프로세스 안에는 여러 쓰레드들이 있고, 쓰레드들은 실행을 위한 프로세스 내 주소 공간이나 메모리 공간(Heap)을 공유 받는다.
또한, 쓰레드들은 각각 명령 처리를 위한 자신만의 메모리 공간(Stack)도 할당 받는다.


💙 Java의 Thread

자바 프로그램을 실행하게 되면 JVM 프로세스 위에서 실행된다.
자바 프로그램의 쓰레드는 Java Main 쓰레드부터 실행된다.




🩵 Multi Thread

💙 Single Thread란?

프로세스 안에서 하나의 쓰레드만 실행되는 것이다.
Java 프로그램의 경우 main() 메서드만 실행시켰을 때 싱글 쓰레드라 한다.
초반에 실습했던 내용들은 모두 싱글 쓰레드였다.


💙 Multi Thread란?

프로세스 안에서 여러 개의 쓰레드가 실행되는 것을 의미한다.
하나의 프로세스는 여러 개의 실행 단위(쓰레드)를 가질 수 있으며 이 쓰레드들은 프로세스의 자원을 공유한다.

💙 장점

여러 개의 쓰레드를 통해 여러 작업을 동시에 할 수 있다.
Stack을 제외한 모든 영역에서 메모리를 공유하기 때문에 자원의 효율적 사용이 가능하다.
응답 쓰레드와 작업 쓰레드를 분리하여 빠른 응답을 해줄 수 있다.

💙 단점

프로세스의 자원을 공유하며 작업을 처리하기 때문에 동기화 문제가 발생할 수 있다.

  • 서로 자원을 사용하려고 하는 충돌이 발생하는 경우가 있다.
    둘 이상의 쓰레드가 서로의 자원을 원하는 상태가 되면, 작업이 종료되기만을 기다린다.
  • 작업을 더 진행하지 못하는 Dead Lock 상태가 된다.



🩵 Thread의 구현

쓰레드의 구현 방법은 여러가지 방법이 있다.

💙 Thread 클래스 상속

쓰레드는 Runnable 인터페이스를 구현하고 있기 때문에, run() 메서드를 Override해서 사용할 수 있다.
Thread는 start() 메서드를 통해서 Override한 run() 메서드를 실행할 수 있다.

public class MyThread extends Thread {
	@Override
	public void run() {
		… // 쓰레드 수행작업
}
}

public static void main(String[] args) {
Thread myThread = new MyThread(); // 쓰레드 생성
myThread.start(); // 쓰레드 실행
} 

💙 Runnable 인터페이스 구현

클래스에서 Runnable 인터페이스를 구현하고, Thread를 인스턴스화 할 때 생성자의 매개변수로 Runnable 인스턴스를 사용한다.

public class MyRunnable implements Runnable {
@Override
public void run() {
		… // 쓰레드 수행작업
	}
}
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable();
Thread myThread= new Thread(myRunnable); // 쓰레드 생성
myThread.start(); // 쓰레드 실행
}

💙 익명 클래스로 Runnable 인터페이스 구현

Runnable 인터페이스를 구현하지 않고, 익명 클래스로 구현하는 방법이다.

public static void main(String[] args) {
Thread myThread= new Thread(new Runnable() {
		@Override
		public void run() {
			System.out.println("MyRunnable is running");
		}
	});
	myThread.start();
}

💙 람다(lambda)로 Runnable 인터페이스 구현

Runnable 인터페이스는 함수형 인터페이스이기 때문에, Lambda를 통해 구현할 수 있다.

public static void main(String[] args) {
	Thread myThread= new Thread(() -> {
		@Override
		public void run() {
			System.out.println("MyRunnable is running");
		}
	});
	myThread.start();
}



🩵 Demon & User Thread

💙 Demon Thread

백그라운드에서 실행되는 낮은 우선순위를 가진 쓰레드를 의미한다.
데몬 쓰레드는 보조적인 역할을 담당한다.

  • Garbage Collector: 메모리 영역을 정리해주는 대표적인 데몬 쓰레드

.setDemon(true)의 형식으로 설정할 수 있으며, true일 경우 데몬 쓰레드로 실행된다.

아무래도 우선순위가 낮고, 다른 쓰레드가 모두 종료되면 강제 종료된다. (JVM이 강종한다..)

💙 User Thread

포그라운드에서 실행되는 높은 우선순위를 가진 스레드를 의미한다.
유저 스레드는 프로그램 기능을 담당한다.

  • Main Thread: 가장 대표적인 유저 스레드
    기존 방식으로 만드는 스레드들은 다 이에 해당한다.

🩵 Priority

모든 Thread는 priority(우선순위)를 가지고 있다.

우선순위가 높은 쓰레드는 우선 순위가 낮은 쓰레드보다 더 많은 리소스를 사용하려 한다.
상대적으로 낮은 우선순위를 가진 스레드는 리소스를 더 적게 얻으려고 한다.

우선순위는 기본적으로 부모 스레드의 우선순위이며, 1에서 10까지의 우선순위를 갖는다.

메인 스레드의 경우 기본적으로 5의 우선순위를 갖는다.

따라서 이 안에서 스레드를 생성한다면 기본적으로 5의 우선순위를 갖게 된다.

별도로 우선순위를 설정하기 위해서는 setPriority(설정할 우선순위) 메서드를 사용해 설정할 수 있다. (확인하고 싶을 때는 getPriority() 메서드 사용)

Thread 클래스에서 상수로 priority값을 제공하고 있기 때문에 이를 활용할 수도 있다.

  • int MIN_PRIORITY = 1; , int NORM_PRIORITY = 5; , int MAX_PRIORITY = 10;

만일 Priority값이 1 이하, 혹은 10 초과가 된다면 IllegalArgumentException이 발생하게 된다.

스레드의 우선순위는 다중 스레드 프로그램에서 특정 작업에 중요도를 할당하거나, 특정 스레드를 특정 작업에 사용하기 위해 제어하는 방식으로 사용할 수 있다.

다만, 운영체제 및 하드웨어의 동작에 따라 실제로는 예측하기 어렵기 때문에 주의하여 사용해야 한다.




🩵 Thread 상태 및 제어

💙 Thread 상태

스레드는 다음과 같은 방식으로 run() 메서드를 수행한다.

이렇게 스레드는 실행과 대기를 반복하며 run() 메서드를 수행한다.

⭐ 스레드 상태 표

상태Enum설명
객체 생성 상태New스레드 객체 생성, start() 호출 전
실행 대기Runnable실행 상태로 언제든지 갈 수 있는 상태
일시정지WAITING다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태
일시정지TIMED_WAITING주어진 시간 동안 기다리는 상태
일시정지BLOCKED사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태
종료TERMINATED쓰레드의 작업이 종료된 상태

💙 Thread 제어

스레드는 다음과 같은 메서드들을 이용하여 제어할 수 있다.

sleep(long milliSecond)

Thread의 클래스 메서드로 인자에 입력한 수의 milliSecond 동안 스레드를 멈춘다.
실행한 스레드의 상태는 일시 정지 상태로 전환되며, 인자로 전달한 시간이 경과하거나, interrupt() 메서드를 호출한 경우에 다시 실행 대기 상태로 복귀한다.

interrupt()

일시 중지 상태인 스레드를 실행 대기 상태로 복귀시킨다.
sleep(), wait(), join() 메서드에 의해 일시 정지된 스레드들은 각 해당 메서드에서 정지한다.
따라서 정지 중인 스레드가 아닌 다른 스레드에서 ‘.interrupt()’를 호출하여 정지 상태를 실행 대기 상태로 전환할 수 있다.

yield()

다른 스레드에게 실행을 양보하는 메서드이다.
실행 중인 상태에서 yield() 호출되면 실행 대기 상태로 전환된다.
예를 들어, 운영 체제의 스케줄러에 의해 10초를 할당받은 스레드가 있다면,
3초 동안 작업을 수행하다가 yield() 메서드를 호출하면, 남은 7초는 다음 스레드에게 양보하는 것이다.

join() / join(long milliSecond)

특정 스레드가 작업하는 동안에 자신을 일시 정지 상태로 전환하는 메서드이다.
인자로 millisecond 단위로 전달할 수 있으며, 전달한 인자만큼의 시간이 경과하거나 interrupt() 메서드가 호출되거나, join() 호출 시 지정했던 다른 스레드가 모든 작업을 마치면 다시 실행 대기 상태로 복귀한다.
sleep() 메서드와의 차이점은 특정 스레드에 대해 동작할 수 있다는 점이다.

wait() / notify()

두 스레드가 교대로 작업을 처리해야 할 때 사용하는 메서드이다.
두 스레드 A와 B가 하나의 공유 객체를 두고 협업을 하는 상황이라면, A가 먼저 작업을 한 뒤 완료하면, B와 교대하기 위해 notify() 메서드를 호출한다.
notify() 메서드가 호출되면 B는 실행 대기 상태로 전환되며, A는 wait() 메서드를 호출하여 자기 자신을 정지 상태로 만든다.
위 상황을 반복하여 두 스레드가 공유 객체에 대해 배타적으로 접근하여 실행할 때 사용한다.
다만, 스레드를 구분해서 통제하는 것이 불가능하다는 제약이 있다.




🩵 Synchronized

한 쓰레드가 진행 중인 작업을 다른 쓰레드가 침범하지 못하도록 막는 것을 Synchronized라 한다.

동기화를 하려면 다른 쓰레드의 침범을 막아야 하는 코드들을 ‘임계 영역’ 으로 설정한다.

임계 영역에는 Lock을 가진 단 하나의 쓰레드만 출입이 가능하다.




🩵 Lock, Condition

synchronized 블럭으로 동기화하면 자동적으로 Lock이 걸리고 풀리지만, 같은 메서드 내에서만 Lock을 걸 수 있다는 제약이 있다.

이러한 제약을 해결하기 위해서 Lock 클래스를 사용한다.

💙 ReentrantLock

재진입 가능한 Lock, 가장 일반적인 배타 Lock이다.
특정 조건에서 Lock을 풀고, 나중에 다시 Lock을 얻어 임계 영역으로 진입이 가능하다.
ReentrantLock() / ReentrantLock(Boolean fair) 메서드로 기본적으로 생성할 수 있다.

void lock(): lock 잠금
void unlock(): lock 해지
boolean isLocked() : lock 잠금여부 반환.
boolean tryLock() : 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않는다.
boolean tryLock(long timeout, TimeUnit unit) : 지정된 시간만큼만 기다린다.


💙 ReentrantReadWriteLock

읽기를 위한 Lock과 쓰기를 위한 Lock을 따로 제공한다.

읽기에는 공유적이고, 쓰기에는 배타적인 lock이다.

간단히 말해서, 읽기 Lock이 걸려있으면 다른 쓰레드가 읽기 Lock을 중복해서 걸고 읽기를 수행할 수 있다는 의미이다.

  • 읽는 것은 내용이 변경되는 점이 없으므로 문제가 없다.

그리고 이때 쓰기 Lock이 불가능하다.


💙 StampedLock

Lock을 걸거나 해지할 때 스탬프(long타입의 정수값)를 사용하는 Lock이다.

ReentrantReadWriteLock에 낙관적 읽기(Optimistic Reading) lock의 기능이 추가된 것이다.

낙관적 읽기 Lock은 쓰기 Lock에 의해 바로 풀리는 Lock이다.
풀리게 되면 다시 읽기 Lock을 얻어서 다시 읽어야 한다.

무조건 읽기 Lock을 걸지 않고, 쓰기 읽기와 충돌할 때만 쓰기가 끝난 후에 읽기 Lock을 거는 형태이다.




🩵 Condition

앞의 wait() & notify()의 문제점인 waiting pool 내 쓰레드를 구분하지 못한다는 것을 해결한 것이 Condition이다.

  • wait()과 notify()는 객체에 대한 모니터링 락(lock)을 이용하여 스레드를 대기시키고 깨우지만,
    wait()과 notify()는 waiting pool 내에 대기 중인 스레드를 구분하지 못하므로, 특정 조건을 만족하는 스레드만 깨우기가 어렵다.

Condition은 ReentrantLock과 함께 사용되며 waiting pool 내의 스레드를 분리하여 각각의 watiting pool에서 대기하도록 한다.

추후 특정 조건이 만족될 때만 깨우도록 할 수 있으며, ReentrantLock 클래스와 함께 사용한다.

사용 방법은 기존 wait() & notify() 대신에 Condition의 await() & signal() 을 사용하면 된다.

ObjectCondition
void wait()void await()
void awaitUninterruptibly()
wait(long timeout)boolean await(long time, TimeUnit unit)
long awaitNanos(long nanosTimeout)
boolean awaitUntil(Date deadline)
void notify()void signal()
void notifyAll()void signalAll()

0개의 댓글