운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스라고 부릅니다. 사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행하는데 이것이 프로세스라고 부릅니다.
하나의 애플리케이션으로 다중 프로세스를 만들기도 하는데 예를 들어 Chrome 브라우저를 두 개 실행했다면 두 개의 Chrome 프로세스가 생성된 거라고 할 수 있습니다.
멀티 태스킹(Multi tasking)은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데 운영체제는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적정히 할당해주고 병렬로 실행시켜줍니다.
예로는 워드로 문서작업을 하면서 윈도에 있는 미디어 플레이어로 음악을 들을 수 있는 것입니다.
그렇다고 멀티 태스킹은 꼭 멀티 프로세스를 뜻하지는 않습니다. 한 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 애플리케이션들도 있습니다.
멀티 프로세스들은 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 서로 독립적입니다.
따라서 하나의 프로세스가 오류가 난다고 해도 다른 프로세스에게 영향을 미치지는 않습니다.
하지만 멀티 스레드는 하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미치게 됩니다.
예는 멀티 프로세스인 워드와 엑셀을 동시에 사용하던 도중 워드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능합니다. 하지만 멀티 스레드로 동작하는 메신저의 경우 파일을 전송하는 스레드에서 예외가 발생되면 메신저 프로세스가 자체가 종료되기 때문에 채팅 스레드도 같이 종료가 되게 됩니다. 그렇기에 멀티 스레드에서는 예외 처리를 꼭 신경 써주셔야 합니다.
main thread()
- 모든 자바 애플리케이션은 메인 스레드가 main() 메서드를 실행하면서 시작되는데 메인 스레드는 main() 메서드의 첫 코드부터 아래로 순차적으로 실행하고, main() 메서드의 마지막 코드를 실행하거나 return문을 만나면 실행이 종료됩니다.
public static void main(String[] args) {
String data = null;
if(...) {
}
while (...) {
}
System.out.println();
}
메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있습니다.
즉 멀티 스레드를 생성해서 멀티 태스킹을 수행할 수 있습니다.
옆의 그림을 보면 메인 스터드가 작업 스레드 1을 생성하고 실행 후 다음, 작업 스레드 2를 생성하고 실행합니다.
싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료됩니다. 하지만 멀티 스레드 어플에서는 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않습니다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않습니다.
멀티 스레드로 실행하는 어플을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해 줘야 합니다.
어떤 자바 어플 메인 스레드는 반드시 존재하기 때문에 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 됩니다. 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요합니다. java.lang.Thread 클래스를 직접 객체화해서 생성해도 되지만. Thread를 상속해서 하위 클래스를 만들어 생성할 수도 있습니다.
java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 Runnable을 매개 값으로 갖는 생성자를 호출해줘야 합니다.
Thread thread = new Thread(Runnable target);
Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름입니다.
Runnable은 인터페이스 타입이기에 구현 객체를 만들어 대입해줘야 합니다. Runnable 에는 run() 메서드 하나가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업 스레드가 실행할 코드를 작성해야 합니다.
Runnable 구현 클래스 작성 방법
Class Task implements Runnable {
public void run() {
스레드가 실행할 코드;
}
}
Runnable 은 작업 내용을 가지고 있는 객체이지 실제 스레드는 아닙니다.
Runnable 구현 객체를 생성한 후, 이것을 매개 값으로 해서 Thread 생성자를 호출하면 비로소 작업 스레드가 생성됩니다.
Runnable task = new Task();
Thread thread = new Thread(task);
// Runnable task ^
코드를 절약하기 위해 Thread 생성자를 호출할 때 Runable 익명 객체를 매개 값으로 사용할 수 있습니다.
Thread thread = new Thread(new Runable() {
public void run() {
스레드가 실행할 코드;
}
} );
Runnable 인터페이스는 run() 메서드 하나만 정의되어 있기 때문에 함수적 인터페이스입니다.
Thread thread = new Thread( ()-> {
스레드가 실행할 코드;
} );
작업 스레드는 생성되는 즉시 실행되는 것이 아니라. start() 메서드를 다음과 같이 호출해야 함 비로 실행됩니다.
thread.start();
start() 메서드가 호출되면. 작업 스레드는 매개 값으로 받은 Runable의 run() 메서드를 실행하면서 자신의 작업을 처리합니다.
0.5초 주기로 비프음을 발생시키면서 동시에 프린팅을 하는 작업이 있다고 가정해보고 비프음 발생과 프린팅은 서로 다른 작업이므로 메인 스레드가 동시에 두 가지 작업을 처리할 수 없습니다. 만약 밑에 처럼 작성했다면 메인 스레드는 비프음을 모두 발생한 다음에 프린팅을 시작합니다.
import java.awt.Toolkit;
public class BeepPrintEx {
// 메인 스레드만 이용한 경우
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
// toolkit 객체 얻기
for(int i =0; i<5; i++) {
toolkit.beep(); // 비프음 발생
try {Thread.sleep(500);} catch(Exception e) {}
// Thread.sleep(500) - 0.5초간 일시정지
}
for(int i=0; i<5; i++) {
System.out.println("띵");
try {Thread.sleep(500);} catch(Exception e) {}
}
}
}
0.5 초 주기로 띵 출력이 됩니다.
package com.javateam;
import java.awt.Toolkit;
// 비프음을 들려주는 작업 정의
public class BeepTask implements Runnable{
public void run() {
// 스레드 실행 내용
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i =0; i<5; i++) {
toolkit.beep();
try {Thread.sleep(500); }
catch (Exception e) {}
}
}
}
package com;
import java.awt.Toolkit;
import com.javateam.BeepTask;
public class BeepPrintEx2 {
// 메인 스레드와 작업 스레드가 동시에 실행
public static void main(String[] args) {
Runnable beepTask = new BeepTask();
Thread thread = new Thread(beepTask);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
출력 결과는 위 와동일 합니다.
메인 스레드가 실행되어지면서 객체를 생성하고 start() 메서드를 호출하여 0.5초 딜레이를 걸로
띵 이 한번 출력하고 다시 위로올라가 작업을 반복합니다.
==0.5초 딜레이 띵 0.5초 딜레이 띵 0.5초 딜레이 띵 0.5초 딜레이 띵 0.5초 딜레이 띵
Thread 하위 클래스로부터 생성
- 작업 스레드가 실행할 작업을 Runnable로 만들지 않고 Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있습니다.
아래는 작업 스레드 클래스를 정의 하는 방법인데 Thread 클래스를 상속한 후 run 메서드를 재정의 해서 스레드가 실행할 코드를 작성하면 됩니다. 작업 스레드 객체를 생성하는 방법은 일반적인 객체를 생성하는 방법과 동입합니다.
public class WorkerThread extends Thread {
@Override // run() 메소드 재정의
public void run() {
// 스레드가 실행될 코드
}
Thread thread = new WorkerThread();
}
코드를 좀 더 절약하기 위해 Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있습니다.
Thread thread = new Thread() {
public void run() {
// 스레드가 실행할 코드;
}
};
// new Thread() 부터 }; 까지 익명 자식 객체
이렇게 생성된 작업 스레드 객체에서 start() 메서드를 호출하면 작업 스레드는 자신의 run() 메소드를 실행하게 됩니다.
Thread.start();
스레드의 이름
- 스레드는 자신의 이름을 가지고 있습니다. 스레드의 이름이 큰 역할을 하는 것은 아니지만, 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 가끔 사용됩니다. 메인 스레드는 "main"이라는 이름을 가지고 있고, 우리가 직접 생성한 스레드는 자동적으로 "Thread-n"이라는 이름으로 성정 됩니다. n은 스레드의 번호를 말하고 Thread -n 대신 다른 이름을 설정하고 싶다면 Thread 클래스의 setName() 메서드를 변경하면 됩니다.
Thread.setName("스레드의 이름");
반대로 스레드 이름을 알고 싶을 경우에는 getName() 메소드를 호출하면 됩니다.
thread.getName();
setName()과 getName()은 Thread의 인스턴스 메서드이므로 스레드 객체의 참조가 필요합니다.
만약 스레드 객체의 참조를 가지고 있지 않다면, Thread의 정적 메서드인 currentThread()로 코드를 실행하는 현재 스레드의 참조를 얻을 수 있습니다.
Thread thread = Thread currentThread();
밑의 예제는 메인 스레드의 참조를 얻어 스레드 이름을 콘솔에 출력하고, 새로 생성한 스레드의 이름을 setName() 메서드로 설정한 후, getName() 메소드로 읽어 오도록 했습니다.
public class ThreadA extends Thread{
public ThreadA() {
setName("ThreadA");
// 스레드 이름 설정
}
public void run() {
for(int i=0; i<2; i++) {
System.out.println(getName() + "가 출력한 내용");
// ThreadA실행 내용
}
}
}
public class ThreadB extends Thread{
public void run() {
for(int i=0; i<2; i++) {
System.out.println(getName() + "가 출력할 내용");
}
}
}
메인 스레드 이름 출력 및 UserThread 생성 및 시작
public class ThreadNameEx {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
// 이 코드를 실행하는 스레드 객체 얻기
System.out.println("프로그램 시작 스레드 이름 : " +
mainThread.getName());
ThreadA threadA = new ThreadA();
// ThreadA 생성
System.out.println("작업 스레드 이름 : " +
threadA.getName());
threadA.start();
// ThreadA 시작
ThreadB threadB = new ThreadB();
// ThreadB 생성
System.out.println("작업 스레드 이름 : " +
threadB.getName());
threadB.start();
// ThreadB 시작
}
}
출력 결과
멀티 스레드는 동시성 또는 병렬성으로 실행되기 때문에 이 용어들에 대해 정확히 이해하는 것이 좋습니다.
동시성은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질을 말하고, 병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 말합니다.
싱글 코어 CPU를 이용한 멀티 스레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업입니다. 번갈아 실행하는 것이 워낙 빠르다 보니 병렬성으로 보일 뿐입니다.
스레드의 개수가 코어의 수보다 많을 경우 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 스레드 스케줄 일라고 합니다. 스레드 스케줄링에 의해 스레드들을 아주 짧은 시간에 번갈아가면서 그들의 run() 메서드를 조금씩 실행합니다.
자바의 스레드 스케줄링은 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 사용합니다.
우선순위 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 것을 말합니다.
순환 할당 방식은 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식을 말합니다.
스레드 우선순위 방식은 스레드 객체에 우선순위 번호를 부여할 수 있기 때문에 개발자 코드로 제어할 수 있습니다. 하지만 순환 할당 방식은 자바 가상 기계에 의해서 정해지기 때문에 코드로 제어할 수 없습니다.
thread.setPriority(우선순위);
우선순위의 매개값으로 1~10까지의 값을 직접 주어도 가능 하지만 코드의 가독성을 높이기 위해서 Thread 클래스의 상수를 사용할수도 있습니다.
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);
MAX = 10 / NORM = 5 / MIN = 1 의 값을 가지고 있습니다.
다른 스레드에 비해 실행 기회를 더 많이 가지려면 MAX_PRIORITY로 우선순위를 높게 설정하면 됩니다.
'JAVA' 카테고리의 다른 글
Arrary (0) | 2022.06.21 |
---|---|
Stream (0) | 2022.06.03 |
Map 컬렉션 (0) | 2022.05.31 |
컬렉션 프레임워크 Set 컬렉션 (0) | 2022.05.30 |
컬렉션 프레임워크 LinkedList (0) | 2022.05.30 |