멀티 스레드

개발자가 되고 싶어요 ㅣ 2024. 4. 17. 20:45

멀티 스레드

멀티 스레드 개념

운영체제는 실행 중인 프로그램을 프로세스로 관리한다. 두 가지 이상의 작업을 처리해야 할 때는 멀티 프로세스를 생성해서 처리하기도 한다. 하지만 멀티 태스킹이 꼭 멀티 프로세스를 뜻하지는 않는다.

하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램들도 있다. 예를 들어 메신저는 채팅 작업을 하면서 동시에 파일 전송 작업을 수행하기도 한다.

하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티 스레드가 있기 때문이다. 스레드는 코드의 실행 흐름을 말하는데, 프로세스 내에 스레드가 두 개라면 두 개의 코드 실행 흐름이 생긴다는 의미이다.

멀티 프로세스가 프로그램 단위의 멀티 태스킹이라면 멀티 스레드는 프로그램 내부에서의 멀티 태스킹이라고 볼 수 있다.

멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다. 하지만 멀티 스레드프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미친다.

 

메인 스레드

모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하면서 시작된다. 메인 스레드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수도 있다.

싱글 스레드에서는 메인 스레드가 종료되면 프로세스도 종료된다. 하지만 멀티 스레드에서는 실행중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.

 

작업 스레드 생성과 실행

멀티 스레드로 실행하는 프로그램을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.

자바 프로그램은 메인 스레드가 반드시 존재하기 때문에 메인 작업 이외에 추가적인 작업 수만큼 스레드를 생성하면 된다. 자바는 작업 스레드객체로 관리하므로 클래스가 필요하다. Thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.

 

Thread 클래스로 직접 생성

java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출하면 된다.

 

방법 1

Runnable을 상속받아서 run() 이라는 메소드를 오버라이딩하고 메인 메서드에서 Runnable 객체를 새로 생성해 thread 안에 넣어주는 방법이다.

public class ThreadPractice implements Runnable {
	@Override
	public void run() {
		// 스레드가 실행할 코드
	}
	
	public static void main(String[] args) {
		Runnable task = new ThreadPractice();
		Thread thread = new Thread(task);
	}
}

 

방법 2

Thread 객체를 생성할 때 Runnable 객체를 새로 생성하고 동시에 run() 메서드를 재정의해주는 방법이다. 코드도 짧고 간결해서 더 자주 사용할 것 같다.

public static void main(String[] args) {
	Thread thread = new Thread(new Runnalbe() {
		@Override
		public void run() {
			//스레드가 실행할 코드
		}
	});
}

작업 스레드 객체가 생성되었다고 해서 바로 작업 스레드가 실행되지는 않는다. 작업 스레드를 실행하려면 스레드 객체의 start() 메소드를 호출해야 한다.

thread.start();

start() 메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 작업을 처리한다. 다음은 작업 스레드가 생성되고 실행되기까지의 순서를 보여준다.

 

thread 자식 클래스로 생성

작업 스레드 객체를 생성하는 또 다른 방법은 Thread의 자식 객체로 만드는 것이다. Thread 클래스를 상속한 다음 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.

 

방법1

public class ThreadPractice extends Thread {
	@Override
	public void run(){
		//스레드가 실행할 코드
	}
	public static void main(String[] args) {
		Thread thread = new ThreadPractice();
		
		thread.start();
	}
}

방법2

public class ThreadPractice {
	public static void main(String[] args) {
		Thread thread = new Thread() {
			@Override
			public void run() {
				//스레드가 실행할 코드
			}
		};
		
		thread.start();
	}
}

 

스레드 이름

스레드는 이름을 가지고 있다. 메인 스레드는 ‘main’ 이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 ‘Thread-n’이라는 이름을 가진다. 이때 이름을 바꾸고 싶으면 thread.setName(”스레드 이름”); 으로 변경 가능하다.

 

스레드 상태

 

구분 메소드 설명
일시 정지로 보냄 sleep(long milis) 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
join() join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태가 되려면, join() 메서드를 가진 스레드가 종료되어야 한다.
wait() 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다.
일시 정지에서 벗어남 interrupt() 일시 정지 상태일 경우, InterruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다.
notify() wait() 메소드로 인해 일시 정지 상태인 스레드를 실행 대기 상태로 만든다.
notifyAll()
실행 대기로 보냄 yield() 실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다.

 

스레드 동기화

멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다. 따라서 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때 까지 객체에 잠금을 걸면 된다. 이를 위해 자바는 동기화 메소드와 블록을 제공한다.

 

방법1

public synchronized void method1() {
	// 단 하나의 스레드만 실행하는 영역
}

방법2

public void method2() {
	// 여러 스레드가 실행할 수 있는 영역
	
	synchronized(공유객체) {
		// 단 하나의 스레드만 실행하는 영역
	}
	
	// 여러 스레드가 실행할 수 있는 영역
}

 

스레드 안전 종료

스레드를 안전하게 종료하는 방법은 사용하던 리소스들을 정리하고 run() 메소드를 빨리 종료하는 것이다. 주로 조건 이용 방법interrupt() 메소드 이용 방법을 사용한다.

 

- 조건 이용

  • whlie문에 조건을 이용해서 run() 메소드의 종료를 유도하고 마지막에 리소스를 정리하며 끝낸다.

- interrupt() 메소드 이용

  • interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다.
    • sleep()을 통해 일시 정지 상태(InterruptedException이 발생)를 만든다. 예외가 발생하면 try-catch문을 통해 while문을 빠져나와 catch 문으로 이동 후 나머지 실행문을 실행하고 run()함수를 종료하게 만든다.
public class PrintThread extends Thread{
	public void run() {
		try {
			while(true) {
				System.out.println("실행 중");
				Thread.sleep(1);
			}
		} catch(InterruptedException e) {
			
		}
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}
  • interrupt()를 이용해서 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시킨다.
public class InterruptExample {
	public static void main(String[] args) {
		Thread thread = new PrintThread();
		thread.start();
		
		try {
			Thread.sleep(1000);
		} catch(InterruptedException e) {
			
		}
		thread.interrupt();
	}

}

 

출력문

 

스레드풀

  • 병렬 작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다. 이에 따라 애플리케이션의 성능 또한 급격히 저하된다. 이렇게 병렬 작업 증가로 인한 스레드의 폭증을 막으려면 스레드풀을 사용하는 것이 좋다.
  • 스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 Queue에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다. 작업 처리가 끝난 스레드는 다시 작업 Queue에서 새로운 작업을 가져와 처리한다. 이렇게 하면 작업량이 증가해도 스레드의 개수가 늘어나지 않아 애플리케이션의 성능이 급격히 저하되지 않는다.
                      •  

'Java' 카테고리의 다른 글

tmp  (0) 2024.05.18
tmp  (0) 2024.04.28
싱글톤 패턴  (0) 2024.03.02
Getter와 Setter  (0) 2024.02.27
접근 제한자  (0) 2024.02.26