본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.
프로그램, 프로세스, 쓰레드
프로그램을 작성하다 보면 어쩔 수 없이 동시에 수행해야 하는 일들이 있다. 예시로 비디오 재생 프로그램을 작성시에 화면 재생과 오디오 재생을 동시에 실행해야 하는 것. 여러 개의 작업이 동시에 수행되도록 하기 위해서는 한정된 코어의 수를 갖는 CPU를 여러 개의 작업이 나눠 사용해야 하는데, 이것이 쓰레드다.
프로그램과 프로세스의 개념
프로그램, 프로세스, 쓰레드의 개념을 명확히 알기 위해서는 컴퓨터의 구조를 이해할 필요가 있다. 컴퓨터를 이루는 주요 구성 요소에는 중앙 처리 장치(CPU), 메모리(memory), 하드디스크(hard disk)가 있다. 이 중 CPU는 연산을 수행해 실제 프로그램을 실행하는 장치로, 가장 빠른 속도로 동작한다. 메모리의 속도는 CPU보다 느리지만, CPU와 근접한 속도로 동작할수 있다. 실제 하드디스크에 저장된 프로그램이 실행되기 위해서는 먼저 프로그램을 메모리로 로딩하는 과정을 거쳐 프로세스 상태로 만들어야 한다. 이렇게 로딩된 메모리의 프로세스가 CPU와 비슷한 속도로 대화하며 프로그램을 실행하는 것이다.
정리하자면, 프로그램은 하드디스크에 저장된 파일들의 모임, 프로세스는 메모리상에 로딩된 프로그램을 의미한다. 메모리는 프로그램 전체를 로딩하는 것이 아니라 그때그때 필요한 부분만을 동적으로 로딩한다.
동일한 프로그램을 메모리에 2번 로딩하면 2개의 프로세스가 동작하는데, 이를 멀티 프로세스라 한다.
쓰레드의 개념
CPU는 속도 차이의 문제로 메모리의 프로세스와만 대화한다. 바꿔 말해 프로세스만 CPU를 사용할 수 있는 것. 하지만 실제 CPU를 사용하는 것은 프로세스 내부의 쓰레드다. 쓰레드가 없는 프로세스는 CPU를 사용하지 않는 프로그램이라는 것으로 쓰레드를 포함하고 있지 않은 프로세스는 존재할 수 없다. 다른 말로 정의하면, 쓰레드는 CPU를 사용하는 최소 단위라 말할 수 있다.
자바 프로그램에서의 쓰레드
먼저 .class 파일을 실행하면 자바 가상 머신은 main 쓰레드를 생성하며 main() 메서드에서 작성한 내용이 main 쓰레드에서 동작한다. main 쓰레드의 내부에서 2개의 쓰레드를 실행하면 동시에 2개 이상의 쓰레드가 동작하는데, 이를 멀티 쓰레드(multi-thread) 프로세스라 한다.
멀티 쓰레드의 필요성
비디오 프레임 번호와 자막 번호를 출력하고자 할 때, 당연히 자막 번호는 비디오 프레임에 맞춰 출력되어야 할 것이다. 아래에서는 2개의 반복문을 사용해 다음과 같은 코드를 작성했다.
public class TestThread {
public static void main(String[] args) {
int[] intArr = {1, 2, 3, 4, 5};
// 비디오 프레임
String[] strArr = {"하나", "둘", "셋", "넷", "다섯"};
// 자막 번호
for (int i = 0 ; i < intArr.length ; i++) {
System.out.println("비디오 프레임" + intArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
for (int i = 0 ; i < strArr.length ; i++) {
System.out.println("자막 번호" + strArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
2개의 반복문은 단일 쓰레드(single-thread)에서 실행된다. 즉, 비디오 출력이 모두 완료된 이후에 자막이 출력되는 것. 이 문제를 해결하기 위한 것이 바로 멀티 쓰레드(multi-thread)다.
쓰레드는 정말 동시에 수행될까?
2개의 작업을 동시에 수행하기 위해서는 멀티 쓰레드를 사용해야 한다고 했다. 근데 컴퓨터에는 단 하나의 CPU만 있는데 어떻게 각 쓰레드가 동시에 수행될까? 이를 이해하기 위해 쓰레드의 동시성(concurrency)와 병렬성(parallslism)을 이해해 보자.
먼저, 단일 쓰레드로 2개의 작업을 처리할 때 각 작업은 순차적(sequential)으로 실행된다. 즉, 먼저 시작된 작업이 완전히 종료된 이후에 두 번째 작업이 실행되는 것.
멀티 쓰레드에서는 동시성 또는 병렬성을 갖고 처리된다.
동시성
처리할 작업의 수가 CPU의 코어 수보다 많을 때다. 이때 CPU는 각 작업 쓰레드의 요청 작업을 번갈아가면서 실행한다. 매우 짧은 간격으로 교차 실행하므로 사용자는 두 작업이 마치 동시에 실행되는 것처럼 보인다.
병렬성
만일 CPU의 코어 수가 작업 수보다 많을 때에는 각각의 작업을 각각의 코어에 할당해 동시에 실행할 수 있기 때문에 동시에 작업이 수행된다.
만약 작업 수가 6개, 코어가 2개라면 쓰레드의 동시성과 병렬성이 함께 적용된다. 먼저 작업이 2개의 코어에 나뉘어 할당되고(병렬성), 각각의 코어는 할당된 작업을 번갈아 실행할 것이다.(동시성)
정리해 멀티 쓰레드의 목적은 병렬성과 동시성을 활용해 여러 작업을 동시에 실행하거나 동시에 실행하는 것처럼 보이게 하는 것이다.
쓰레드의 생성 및 실행
쓰레드를 생성하는 방법은 크게 2가지로 나눌 수 있다.
쓰레드 생성 및 실행방법
1) Thread 클래스를 상속받아 run() 메서드 재정의
첫 번째 과정은 다음과 같이 세 과정으로 구성된다
- Thread 클래스를 상속받아 run() 메서드를 오버라이딩한 클래스(또는 익명 이너 클래스) 정의
class MyThread extends Thread {
@Override
public void run() {
// 작업 내용
}
}
- Thread 객체 생성
Thread myThread = new MyThread();
// 또는
MyThread myThread = new MyThread();
- start() 메서드를 이용해 쓰레드 실행
myThread.start()
run() 메서드와 start() 메서드의 차이점?
실제 CPU와 이야기하기 위해서는 스택(stack) 메모리를 포함해 준비해야 할 것이 많다. start() 메서드는 새로운 쓰레드 생성/추가를 위한 모든 준비와 새로운 쓰레드 위에서 run() 메서드 실행 2가지 작업을 연속으로 실행하는 메서드다.
쓰레드 내부의 run() 메서드를 직접 호출해도 오류는 발생하지 않는다. 다만 별도의 쓰레드가 아닌 현재의 쓰레드에서 일반 메서드처럼 실행된다.
이제 비디오 프레임과 자막을 출력하는 프로그램을 멀티 쓰레드로 바꿔보자.
class SMFileThread extends Thread {
@Override
public void run() {
// 자막 번호 배열
String[] strArr = {"하나", "둘", "셋", "넷", "다섯"};
try {Thread.sleep(10);} catch(InterruptedException e) {}
for (int i = 0 ; i < strArr.length ; i++) {
System.out.println(strArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
public class StartThread1 {
public static void main(String[] args) {
Thread smiFileThread = new SMFileThread();
smiFileThread.start();
// 비디오 프레임 배열
int[] intArr = {1, 2, 3, 4, 5};
for (int i = 0 ; i < intArr.length; i++) {
System.out.println(intArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
참고로 멀티 쓰레드는 독립적으로 실행되기 때문에 먼저 start() 메서드로 호출됐다 하더라도 나중에 실행된 쓰레드보다 늦게 실행될 수 있으므로 자막 쓰레드에 Thread.sleep(10)을 추가하여 자막 번호가 항상 비디오 번호 뒤에 나오도록 했다.
다음 코드는 위의 코드와 동일한 기능을 수행하지만 3개의 쓰레드를 사용해 수행한다. main 쓰레드는 2개의 쓰레드 객체를 생성해 실행하는 역할만 수행한다.
class SMFileThread extends Thread {
@Override
public void run() {
// 자막 번호 배열
String[] strArr = {"하나", "둘", "셋", "넷", "다섯"};
try {Thread.sleep(10);} catch(InterruptedException e) {}
for (int i = 0 ; i < strArr.length ; i++) {
System.out.println(strArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
class VideoThread extends Thread {
@Override
public void run() {
// 비디오 프레임 배열
int[] intArr = {1, 2, 3, 4, 5};
for (int i = 0 ; i < intArr.length; i++) {
System.out.println(intArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
public class StartThread2 {
public static void main(String[] args) {
Thread smiFileThread = new SMFileThread();
smiFileThread.start();
Thread videoThread = new VideoThread();
videoThread.start();
}
}
2) Runnable 인터페이스 구현 객체 생성 후 Thread 생성자로 Runnable 객체 전달
두번 째 방법도 세 단계로 구성되어 있다.
- Runnable 인터페이스를 구현한 클래스를 정의하는 것으로, 이때 run() 추상 메서드를 구현하면서 여기에 쓰레드의 작업 내용을 작성한다.
- 정의한 클래스를 이용해 Runnable 객체를 생성하고, Thread 객체를 생성시 Runnable 객체를 생성자에 전달한다.
- start() 메서드를 이용해 쓰레드 실행
class SMFileRunnable implements Runnable {
@Override
public void run() {
// 자막 번호 배열
String[] strArr = {"하나", "둘", "셋", "넷", "다섯"};
try {Thread.sleep(10);} catch(InterruptedException e) {}
for (int i = 0 ; i < strArr.length ; i++) {
System.out.println(strArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
public class StartThread3 {
public static void main(String[] args) {
// runnable 객체 생성
Runnable smiFileRunnable = new SMFileRunnable();
Thread thread = new Thread(smiFileRunnable);
thread.start();
int[] intArr = {1, 2, 3, 4, 5};
for (int i = 0 ; i < intArr.length ; i++) {
System.out.println(intArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
아래의 코드는 2번째 방법의 두 번째 예제로 3개의 쓰레드를 사용해 코드를 변경했다.
class SMFileRunnable implements Runnable {
@Override
public void run() {
// 자막 번호 배열
String[] strArr = {"하나", "둘", "셋", "넷", "다섯"};
try {Thread.sleep(10);} catch(InterruptedException e) {}
for (int i = 0 ; i < strArr.length ; i++) {
System.out.println(strArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
class VideoRunnable implements Runnable {
@Override
public void run() {
// 비디오 프레임 배열
int[] intArr = {1, 2, 3, 4, 5};
for (int i = 0 ; i < intArr.length; i++) {
System.out.println(intArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
}
public class StartThread4 {
public static void main(String[] args) {
Runnable smiFileRunnable = new SMFileRunnable();
Thread thread1 = new Thread(smiFileRunnable);
thread1.start();
Runnable videoRunnable = new VideoRunnable();
Thread thread = new Thread(videoRunnable);
thread2.start();
}
}
마지막 예시로는, 3개의 쓰레드가 동작하는 예제와 동일하지만, 클래스를 정의하지 않고 익명 이너 클래스 문법을 사용해 Runnable 인터페이스 객체를 생성한다.
public class StartThread4 {
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
// 자막 번호 배열
String[] strArr = {"하나", "둘", "셋", "넷", "다섯"};
try {Thread.sleep(10);} catch(InterruptedException e) {}
for (int i = 0 ; i < strArr.length ; i++) {
System.out.println(strArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
// 비디오 프레임 배열
int[] intArr = {1, 2, 3, 4, 5};
for (int i = 0 ; i < intArr.length; i++) {
System.out.println(intArr[i]);
try {Thread.sleep(200);} catch(InterruptedException e) {}
}
}
});
thread1.start();
thread2.start();
}
}
'Backend > JAVA' 카테고리의 다른 글
[Java] #15.4 쓰레드의 동기화 (0) | 2023.07.08 |
---|---|
[Java] #15.3 쓰레드의 속성 (0) | 2023.07.05 |
[Java] #14 예외 처리 (0) | 2023.06.30 |
[Java] #13 이너 클래스와 이너 인터페이스 (1) | 2023.06.06 |
[Java] #12 추상 클래스와 인터페이스 (0) | 2023.06.04 |