프로그래밍/JAVA

[ JAVA ] 쓰레드란?

리신 2023. 2. 21. 23:36
반응형

먼저, 쓰레드를 설명하기전에 프로세스부터 알아보자!    

 

프로세스란?

간단히 말하자면 실행 중인 프로그램 이다.

운영체제로 부터 시스템 자원(메모리)을 할당받는 작업의 단위이며 실행된 프로그램을 의미한다.

 

프로세스의 구성

프로세스 = 실행중인 프로그램 (자원[데이터 + 메모리] + 쓰레드)

 

할당 시스템 자원

CPU시간, 운영시 필요한 주소공간 Code, Data, Stack, Heap 의 구조로 되어있는 독립된 메모리 영역이 있다.

프로세스 메모리

 

특징

- 프로세스는 각각 독립된 메모리 영역 (Code, Data, Stack, Hea) 구조를 할당 받고, 최소 1개의 메인 쓰레드를 가지고 있다.

- 각 프로세스는 별도의 주소공간에서 실행되며, 한 프로세스는 다른 프로세스의 변수나 자료구조에 접근 할 수 없다.

- 한 프로세스가 다른 프로세스의 자원에 접근하려면 프로세스가느이 통신 (IPC, inter-processCommunication)을 사용함.

 ex) pipe, file, socket

 


쓰레드(thread) 란? 

프로세스 내에서 실행되는 여러 흐름의 단위이며 프로세스의 특정한 수행 경로이다.

즉, 프로세스가 할당받은 자원을 이용하는 실행의 단위입니다.

 

조금 이해하기 어렵다면!

프로세스(공장) , 쓰레드 (일꾼) 이라고 생각하면 이해하기 쉬울 것이다!

 

쓰레드의 메모리

쓰레드는 각각 Stack 영역은 따로 할당 받고 나머지 영역은 공유한다.

따라서 하나의 쓰레드에서 오류가 발생한다면 프로세스의 다른 쓰레드도 모두 강제 종료 된다.

 

멀티태스킹 & 멀티쓰레딩

  • 멀티태스킹: 여러 프로세스가 동시에 실행되는 것
  • 멀티쓰레딩: 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것

 

싱글쓰레드

프로세스가 단일 쓰레드로 동작하는 방식이다.

하나의 레지스터, 스택으로 표현한다.

 

장점

1. 자원 접근에 대한 동기화

멀티 쓰레드의 경우 자원을 공유 하기 때문에 동기화를 항상 고려해야함. 

하지만 싱글 쓰레드의 경우 자원의 동기화를 신경쓸 필요가 없다.

 

2. 문맥 교환 (Context Switching) 작업을 하지 않음

단일 쓰레드로 동작하기 때문에 문액을 교환할 필요가 없다.

 

※ 문맥 교환이란?

CPU가 한 개의 Task를 실행하고 있는 상태에서 다른 Task로 실행이 전환되는 과정에

기존의 Task 상태 및 Register 값들에 대한 정보(문맥, Context)를 저장하고,

새로운 Task의 정보(문맥, Context)으로 교체하는 작업을 말합니다.

 

※ 레지스터란? CPU가 요청을 처리하는데 필요한 데이터를 일시적으로 저장하는 기억장치이다.

 

단점

1. CPU 코어를 모두 활용하지 못함.

싱글 스레드는 하나의 물리적 코어밖에 사용하지 못해 멀티 코어 머신에서 CPU 사용을 최적화할 수 없다.

최적화를 위해선 Cluster 모듈을 이용하여 여러 프로세스를 사용할 수 있다.

하지만 앞서 프로세스끼리의 자원 공유는 어렵기 때문에 Redis와 같은 부가 인프라가 필요하다.

 

멀티 쓰레드

두개 이상의 쓰레드가 프로세스 내부에서 자원을 공유하여 작업을 수행한다.

각각 쓰레드가 고유 레지스터와 스택으로 표현된다.

 

장점

1. CPU 사용률 향상

싱글스레드와 달리 다중 CPU 구조에서 각각의 스레드가 다른 프로세서에서 병렬로 수행될 수 있습니다.

 

2. 작업이 분리되기 때문에 코드가 간결해진다.

3. 사용자에 대한 응답성 향상

4. 작업이 분리되기 때문에 코드가 간결해진다.

 

 

단점

1. 교착 상태

두 개 이상의 작업이 하나씩 자원을 소지하고 있으면서 상대방이 가진 자원을 서로 원하고 있는 상태.어떤 작업도 실행되지 못하고 계속 서로 상대방의 작업이 끝나기만을 바라는 무한정 대기상태이다.

 

2. 동기화 문제

둘 이상의 쓰레드에서 공유 자원에 접근 할 때 와  다른 쓰레드에서 사용 중인 경우 값을 읽어 올 때 동기화 문제가 발생할 수 있다.

 

※  그럼 동기화는 하지 못하는가?

아니다. 자바의 synchronized와 같이 동기화를 할 수 있다.

이때 synchronized 키워드를 사용한 메소드 혹은 블럭은 보호 구역으로 지정되며 하나의 스레드만 자원을 사용할 수 있도록 Lock을 걸어준다.

 

 


쓰레드 구현 및 실행

1. Thread Class 상속

class MyThread extends Thread{
	public void run() { /* 작업내용 */ }
}

 

2. Runnable 인터페이스를 구현

class MyThread implements Runnable{
	public void run() { /* 작업내용 */ }
}

 

Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없다.

때문에 Runnuable 인터페이스로 구현하는 것이 일반적이다.

 

Runnable로 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 보다 객체지향적인 방법이라고 할 수 있다.


Runnable로 구현할 경우,

Thread 클래스의 static메서드인 currentThread를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출 가능하다.

 

반대로 Thread 상속하여 구현하는 경우는 Thread 클래스의 메서드를 직접 호출 가능하다.

 

 

아래 코드로 한번 비교해서 보자!

class ThreadEx1 {
	public static void main(String args[]) {
		ThreadEx1_1 t1 = new ThreadEx1_1();

		Runnable r  = new ThreadEx1_2();
		Thread   t2 = new Thread(r);	  // 생성자 Thread(Runnable target)
		// Thread t2 = new Thread(new ThreadEx1_2())
		t1.start(); //쓰레드 실행
		t2.start(); //쓰레드 실행
	}
}

//Thread 클래스를 상속할 경우
class ThreadEx1_1 extends Thread {
	public void run() {
		for(int i=0; i < 5; i++) {
        		//getName를 직접 호출 가능
			System.out.println(getName()); // 조상인 Thread의 getName()을 호출
		}
	}
}

class ThreadEx1_2 implements Runnable {
	public void run() {
		for(int i=0; i < 5; i++) {
		   // Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
		    System.out.println(Thread.currentThread().getName());
		}
	}
}

위 코드를 보면 Thread 클래스를 상속하여 구현한 것과 Runnable 인터페이스로 구현한 것은

서로  getName을 호출하는 방법이 다르다.

 

Thread 클래스를 상속하여 구현한 코드를 보면 getName 메소드를 직접 호출한다.

반면, Runnable 인터페이스로 구현한 것은 직접 호출하지 못하고 현재 실행중인 Thread를 가져와서 getName 메소드를 호출한다.

 

그리고 쓰레드 호출에 있어서 알아야 할 부분들이 있다.

start()를 호출한다해서 쓰레드가 시작되는 것이 아니라 실행 대기 상태에 있다가 차례가 되면 실행한다.
한 번 start하면 재할당을 하지 않으면 재호출 불가능하다.

즉, 종료된 쓰레드는 다시 실행 할 수 없다.

 

만일 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시 IllegalThreadStateException 에러가 발생한다.

 

 

 


start()와 run()

쓰레드를 실행시킬 때 run()이 아닌 start()를 호출한다는 것에 대해서 좀 의문이 들었을 수도 있다!

 

main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 싱핼시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.

main메서드에서 run()을 호출했을 때의 호출스택

 

 

반면 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서,

생성된 호출스택에 run()이 첫 번째로 올라가게 한다.

 

모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.

 

아래 그림으로 알아보자!

1. main 메서드에서 쓰레드의 start()를 호출한다.

2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.

3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.

4. 이제는 호출스택이 2개이므로 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.

 

즉, 간단하게 정리해 보자면

main에서 run() 호출: 단순히 클래스에 선언된 메서드를 호출
start(): 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 후 run()을 호출해 호출스택에 run 실행

 

Main쓰레드

main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다.

우리는 지금까지 우리도 모르는 사이에 이미 쓰레드를 사용하고 있었던 것이다!!

 

앞에서 쓰레드를 쉽게 말해서 일꾼이라고 하였는데,

프로그램이 실행되기 위해서 작업을 수행하는 일꾼이 최소한 하나는 필요하지 않을까?

그래서 프로그램을 싱행하면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 하는 것이다.

 

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


싱글 쓰레드와 멀티 쓰레드

 

싱글쓰레드 프로세스와 멀티쓰레드

위 그림을 보면,

하나의 쓰레드로 두 개의 작업을 수행하는 경우

두 개의 쓰레드로 두 개의 작업을 수행하는 경우 의 그림이 있다.

 

둘 중 어떤 쓰레드가 작업 속도가 더 빠를까?

상황에 따라 다르다!

 

싱글코어에서는 단순히 CPU만을 사용하는 계산작업은 싱글 쓰레드가 더 효율적이다.

 

그 이유는 뭘까?멀티 쓰레드는 두개의 쓰레드가 작업을 번갈아가면서 처리한다.그러면 한쓰레드가 작업이 끝나고 나서 다른 쓰레드로 작업을 전환한다.작업을 전활 할 때 다음에 실행해야할 위치 등의 정보를 저장하고 읽어 오는 시간이 소요된다.

 

위 그림을 보면 멀티코어일 경우 작업이 겹치는 부분이 생기는데,

이 경우는 한 쓰레드가 화면에 출력하고 있는 동안 다른 쓰레드는 대기상태이다.

 

하지만 위 결과는 실행할 때마다 다른 결과를 얻을 수 있다.

그 이유는 실행 중인 예제프로그램(프로세스)이 OS의 프로세스 스케줄러의 영향을 받기 때문이다.

JVM의 쓰레드 스케줄러에 의해서 어떤 쓰레드가 얼마동안 실행될 것인지 결정되는 것과 같이 프로세스도 프로세스 스케줄러에 의해서 실행 순서와 실행 시간이 결정되기 때문에 매 순간 상황에 따라 프로세스에게 할당되는 실행시간이 일정하지 않고 쓰레드에게 할당되는 시간 역시 일정하지 않게 된다.

따라서 쓰레드가 이러한 불확실성을 가지고 있다는 것을 염두에 두어야 한다.

 

※ 자바가 OS독립적이라 하지만 실제로는 OS종속적인 부분이 몇 가지 있는데 쓰레드도 그 중의 하나이다.

 

그러면 두 쓰레드가 서로 다른 자원을 사용할 경우에는 어떨까?

 

멀티쓰레드가 효율적이다.

다른 쓰레드가 입력을 기다릴때 또 다른 쓰레드가 일을 처리하면 되기 때문이다.

ex) 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력이 필요로 하는 경우가 이에 해당한다.

 


쓰레드 우선순위

쓰레드는 우선순위라는 속성을 가지고 있다.

이 우선순위의 값에따라 쓰레드가 얻는 실행시간이 달라진다.

 

쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.

 

쓰레드의 우선숭위 지정하기

void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority() // 쓰레드의 우선순위를 반환한다.

public static final int MAX_priority = 10 // 최대우선순위
public static final int MAX_priority = 1  // 최소우선순위
public static final int MAX_priority = 5  // 보통우선순위

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

 

여기서 주의할 점!

쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받다.

 

main메서드를 수행하는 쓰레드는 우선순위가 5이다.

main메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.


쓰레드 그룹

쓰레드 그룹(ThreadGroup)은 관련된 쓰레드를 묶어서 관리할 목적으로 이용된다.

 

JVM이 실행되면 system 쓰레드 그룹을 만들고,

JVM 운영에 필요한 쓰레드들을 생성해서 system 쓰레드 그룹에 포함시킨다.

 

그리고 system의 하위 쓰레드 그룹으로 main을 만들고 메인 쓰레드를 main 쓰레드 그룹에 포함시킨다.

모든 쓰레드는 그룹을 갖는다.

쓰레드그룹을 명시적으로 선언하지 않으면 자신을 생성한 쓰레드와 같은 그룹에 속한다.

 

참고

https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadGroup.html

 

ThreadGroup (Java Platform SE 8 )

Copies into the specified array every active thread in this thread group. If recurse is true, this method recursively enumerates all subgroups of this thread group and references to every active thread in these subgroups are also included. If the array is

docs.oracle.com


데몬쓰레드

데몬 쓰레드는 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

일반 쓰레드가 모두 종료되면, 데몬 쓰레드는 강제적으로 자동  종료된다.

ex) 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신 등 

 

데몬 쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족 되면 작업을 수행하고 대기하도록 작성한다.

 

데몬 쓰레드는 일반 쓰레드의 작성방법과 실행방법이 같다.

다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하기만 하면 된다.

 

그리고  데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다는 점이 있다!

 

class ThreadEx10 implements Runnable  {
	static boolean autoSave = false;

	public static void main(String[] args) {
		Thread t = new Thread(new ThreadEx10()); //일반 쓰레드 생성
        //데몬 쓰레드는 반드시 쓰레드 생성 후 setDaemon()을 호출해야 함.
		t.setDaemon(true);		// 이 부분이 없으면 종료되지 않음.
		t.start();

		for(int i=1; i <= 10; i++) {
			try{
				Thread.sleep(1000); //1초마다
			} catch(InterruptedException e) {}
			System.out.println(i); //i를 출력하고
			
			if(i==5) //i가 5가 되었을 때 (5초)
				autoSave = true; 
		}

		System.out.println("프로그램을 종료합니다.");
	}

	public void run() {
		while(true) {
			try { 
				Thread.sleep(3 * 1000);	// 3초마다
			} catch(InterruptedException e) {}	

			// autoSave의 값이 true이면 autoSave()를 호출함.
			if(autoSave) { 
				autoSave();
			}
		}
	}

	public void autoSave() {
		System.out.println("작업파일이 자동저장되었습니다.");
	}
}

위코드는 setDemon을 했기 때문에 일반 쓰레드가 종료되면 자동으로 데몬 쓰레드가 종료되기 때문에 프로그램이 종료된다.

그러지 않을 경우 종료되지 않고 끝나지 않는다.

반응형

'프로그래밍 > JAVA' 카테고리의 다른 글

[ JAVA ] 직렬화란  (0) 2023.04.25
[ JAVA ] 입출력  (0) 2023.04.05
[JAVA] Properties 란?  (0) 2023.01.31
[JAVA] TreeMap 이란  (0) 2023.01.29
[JAVA] LinkedList  (0) 2023.01.05