[ Java ] - 프로세스와 쓰레드 - 01)
프로세스란?
* 프로그램 설치 시 (설치한 프로그램은)하드 디스크에 저장되며, 프로그램을 실행하는 것은 하드디스크에 저장된 프로그램 내용을 메모리로 올리는 것(Loading)을 의미합니다. 이와 같이 실행을 위해 메모리로 올라온 프로그램을 프로세스라고 합니다. 즉 하드 디스크 - 메모리 - CPU 과정을 통해 프로그램이 실행 됩니다.
* 멀티 프로세스(Multi-process)란 동일한 프로그램을 여러 개 실행시키는 것을 의미합니다.
프로세스(Process)란 기본적으로는 현재 실행 중인 프로그램을 의미하며, 자바 프로그래밍 관점에서 자세히 설명하면 JVM(=Java Virtual Machine)을 실행하는 하나의 인스턴스로 볼 수 있습니다. 프로그램은 운영체제로부터 메모리와 리소스를 할당받아 실행됩니다. 각각의 프로세스는 독립적으로 작동되며, 서로의 메모리 공간에 접근할 수 없고, 오직 자신이 할당받은 메모리 공간 내에서 작동됩니다.
* 자바 프로그램 실행 과정
1. 컴파일러를 통해 .java로 된 자바 소스 코드를 .class로 된 바이트 코드로 변환합니다.
2. 바이트 코드로 변환된 클래스 파일을 JVM이 메모리에 로드합니다.
3. JVM은 메모리에 로드된 바이트 코드를 다시 기계어로 변환하여 실행함으로써 프로그램을 동작시킵니다.
자바 프로그래밍 시에 코드 작성 후 빌드하는 것은 자바 소스 코드를 .class 파일로 컴파일하는 과정을 의미합니다.
쓰레드란?
* 쓰레드는 CPU를 사용하는 최소 단위입니다. 모든 프로그램은 최소한 하나 이상의 쓰레드를 가지게 됩니다.
쓰레드란 프로세스를 구성하는 하나의 실행 단위를 의미합니다. 하나의 프로세스 내에는 하나 이상의 쓰레드가 존재할 수 있습니다. 쓰레드를 통해 프로세스 내에서 병렬 처리와 다중 작업이 가능합니다. 예를 들어 유튜브로 영상을 보면서 해당 영상에 댓글을 남기는 것이 가능한 이유는 멀티쓰레드에 의해 처리되었기 때문입니다(멀티쓰레드가 아니었다면 영상이 재생되는 동안에는 댓글을 남기는 등의 다른 작업이 불가능하게 됩니다). 이와 같이 하나의 프로세스에 여러 개의 쓰레드가 존재하는 멀티 쓰레드는 서로 같은 메모리 공간을 사용하기 때문에 간단하고 빠른 데이터 공유가 가능합니다. 이로 인해 사용자는 동시에 여러 작업을 하는 것이 가능한 반면, 잘못 구현할 경우 데드락 등과 같은 문제가 발생할 수 있습니다.
* 데드락(=deadlock=교착상태)이란?
두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰 있는 상태를 의미합니다.
자바에서는 쓰레드를 실행시키기 위해 Thread 클래스를 상속받거나, Runnable 인터페이스를 구현한 후, start()메서드를 호출하여 실행이 가능합니다. 쓰레드는 생성(=NEW,) 실행(=RUNNABLE), 실행종료(=BLOCKED), 대기(=WAITING) 라이프 사이클을 가집니다.
* 쓰레드 풀
반복적으로 쓰레드가 사용되는 경우 쓰레드의 잦은 생성과 소멸로 인한 잡 오버헤드를 줄이기 위해 미리 쓰레드를 생성하고 재사용하는 메커니즘을 가지는 기술로, ExecutorService를 통해 생성할 수 있습니다.
쓰레드 구현
쓰레드 구현을 위해 앞서 이야기 한 것처럼 Thread클래스를 상속받거나 Runnable 인터페이스를 구현하는 방법이 있으나, Thread의 경우 Thread클래스 를 상속받으면, 다른 클래스를 상속받는 것이 불가능하기 때문에 일반적으로 Runnable 인터페이스를 구현하는 방식으로 사용됩니다.
쓰레드 구현 코드
public class ThreadTest {
public static void main(String args[]) {
ThreadEx1_1 t1 = new ThreadEx1_1();
Runnable r = new ThreadEx1_2();
Thread t2 = new Thread(r);
// start 메서드를 통해 호출해야만 쓰레드가 실행된다.
t1.start();
t2.start();
}
}
/* Thread 클래스 상속 **/
class ThreadEx1_1 extends Thread {
public void run () {
for (int i=0; i<5; i++) {
System.out.println(getName());
}
}
}
/* Runnable 인터페이스를 구현 **/
class ThreadEx1_2 implements Runnable {
public void run() {
for (int i =0; i < 5; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
1. Thread 클래스 상속받는 경우 -> Thread 클래스의 run()을 오버라이딩하여, 쓰레드로 작업할 코드를 구현하여 줍니다.
2. Runnable 인터페이스를 구현하는 경우 -> Runnable 인터페이스의 추상 메서드인 run의 내용을 구현하여 줍니다.
Thread 클래스를 상속받은 클래스의 경우에는 해당 클래스의 인스턴스를 생성하여 바로 start()메서드를 통해 사용할 수 있으며, Runnable 인터페이스를 구현하여 사용하려는 경우에는 해당 클래스의 인스턴스를 생성 후 생성자 Thread([인터페이스 구현 클래스]) 로 감싸주는 과정을 한 번 더 거쳐야 합니다.
쓰레드는 start()를 호출하여 사용할 수 있습니다. main 쓰레드에서 start() 호출 시 새롭게 Call Stack(콜스택)을 생성하고, 새롭게 생성된 호출 스택에 가장 먼저 run()이 호출되며 각각의 쓰레드가 독립적인 작업을 수행하게 됩니다. 여러 개의 쓰레드가 생성된 경우 실행 중인 쓰레드를 제외한 나머지 쓰레드는 실행 대기 상태로 놓이게 됩니다. 이후 실행 중이던 쓰레드의 작업이 종료되면 스케줄러가 우선 순위를 고려하여 작업을 실행합니다.
참고로 자바에서 프로그램 실행 시 사용되는 main 메서드의 경우도, main 메서드 호출 시 main 쓰레드가 실행되는 것입니다.
한 번 사용된 쓰레드는 재사용할 수 없습니다. 동일 쓰레드에 대해 start()를 두 번 호출할 경우 IllegalThreadStateException 에러가 발생 할 수 있습니다. 때문에 인스턴스를 새롭게 생성하여 start()를 호출해야 합니다.
* 싱글 코어에서 단순히 CPU 만을 사용하는 계산 작업인 경우 멀티쓰레드보다 싱글쓰레드로만 구현하는 것이 더욱 효율적이다. 멀티쓰레드의 경우 쓰레든 간 작업 전환을 하는 컨텍스트 스위칭으로 인한 시간이 소요되기 때문에, 입출력 등의 작업이나 서로 동일한 자원이 아닌 서로 다른 자원에 대해 작업을 하는 경우에 멀티 쓰레드를 사용하는 것이 효율적일 수 있다.
쓰레드의 상태
- NEW : 쓰레드가 새롭게 생성된 상태로 아직 start()를 통해 실행은 되지 않은 상태
- RUNNABLE : 실행 중 또는 실행 가능한 상태
- BLOCKED : 동기화 블럭에 의해 일시 정지된 상태
- WAITING, TIMED_WAITING : 쓰레드의 작업이 실행 가능하지 않은 상태
- TERMIANTED : 쓰레드의 작업이 종료된 상태
* 일시 정지 상태와 실행 대기 상태를 엄연히 다릅니다. 일시 정지 상태가 풀리면 실행 대기 상태로 올라갈 수 있습니다.
* 쓰레드 실행 중에 사용자의 입출력 작업이 발생하는 경우 일시 정지 상태가 되며, 입출력 작업이 완료 되면 일시 정지 상태의 쓰레드는 다시 실행 대기 상태가 됩니다.
쓰레드의 우선 순위
여러 쓰레드가 실행 되는 경우 쓰레드의 실행 시간은 쓰레드의 우선 순위에 따라 달라지게 됩니다. 쓰레드의 우선 순위의 범위는 1에서 10까지 존재하며, 숫자가 높을 수록 우선 순위가 높습니다. 예를 들어 파일 전송과 채팅이 동시에 가능한 메신저의 경우, 일반적으로 파일 전송 보다 메시지 전송의 우선 순위를 더욱 높게 두는 방식을 취하여 파일 전송을 보낸 후 아직 전송 중인 상태에서 메시지를 하더라도 우선 순위가 메시지 전송이 더욱 높아 메시지 전송이 먼저 수행됩니다. 쓰레드의 우선 순위는 priority라는 속성에 의해 결정되며, setPriority(int newPriority)를 통해 변경할 수 있습니다.
* 기본적으로는 우선 순위에 따라 쓰레드의 실행 시간이 정해지며, 운영 체제, 운영 체제가 취하는 스케쥴링 방식 등에 따라 작업의 처리 속도가 달라질 수 있습니다.
- 쓰레드는 쓰레드 그룹이라는 방식을 통해 유사한 작업 간 그룹으로 묶어 관리할 수 있습니다. 기본적으로 쓰레드 그룹을 지정하지 않은 경우 자신을 생성한 쓰레드와 같은 그룹에 속하게 됩니다. 때문에 자바 프로그래밍 시 쓰레드 그룹을 지정하지 않은 쓰레드는 main쓰레드의 하위 그룹으로 속하게 됩니다.
쓰레드 프로그래밍
여러 개의 쓰레드를 생성하는 멀티 쓰레드로 동작하는 프로그램의 경우 프로그래밍 시에 동기화와 쓰레드 간의 적절히 자원과 시간이 할당될 수 있도록 스케쥴링 하는 것이 중요합니다.
쓰레드 스케쥴링을 위해 개발자는 아래와 같은 메서드를 사용할 수 있습니다.
- static void sleep() : 지정된 시간 동안 쓰레드를 일시 정지시키고, 지정된 시간 경과 후에 해당 쓰레드는 자동적으로 대기 실행 상태가 됩니다.
- void join() : 지정된 시간 동안 쓰레드가 실행되도록 합니다.
- void interrupt() : 일시 정지 상태인 쓰레드를 깨워서 다시 실행 대기 상태에 올려 둡니다.
void stop() : 쓰레드를 즉시 종료 시킵니다.void suspend() : 쓰레드를 일시 정지 시킵니다.void resume() : suspend에 의해 일시 정지 상태의 쓰레드를 다시 실행대기상태에 올려 둡니다.- static void yield() : 실행 중인 쓰레드가 자신의 실행 시간을 다른 쓰레드에게 양보하고, 자신은 실행 대기 상태가 됩니다.
* stop, suspend, resume의 경우 deprecated되어 사용이 권장되지 않습니다.
[ Sleep ] 일정 시간 동안 쓰레드를 정지시킨다.
sleep을 통해 일시 정지된 쓰레드는 일시 정지된 시간이 경과되거나, interupt에 의해 호출되어 다시 실행 대기 상태로 변경될 때에, InterruptedException에 의해 일시 정지 상태가 풀리기 때문에 sleep 사용 시에는 반드시 try-catch 문을 사용하여 작성하여 주어야 합니다. sleep은 또한 언제나 현재 실행 중인 쓰레드를 기준으로 하여 일시 정지 상태가 됩니다.
멀티 쓰레드 프로그래밍 시 또 중요하게 고려해야할 점은 동기화입니다. 예를 들어 동일 자원에 대하여 작업을 실행하는 둘 이상의 쓰레드가 존재하는 상황에서 쓰레드 간 작업이 동일 자원에 대해 번갈아가면 발생한 경우 작업의 결과에 대한 신뢰성이 떨어지게 된다. 이러한 문제를 극복하기 위해 등장한 것이 임계 영역과 잠금(lock)입니다. 쉽게 풀어 이야기하면 공유 데이터를 사용하는 부분을 임계 영역으로 지정하고, 해당 자원에 대한 lock을 획득한 쓰레드만이 임계 영역에서 작업이 가능하도록 하는 것입니다.
* 자바를 통한 프로그래밍 시에는 java.util.concurrent.locks 또는 java.util.concurrent.atomic 을 사용하여 동기화를 지원하고 있습니다.
[ join ] 다른 쓰레드의 작업이 완료될 때까지 대기하도록 만들어 준다.
join()를 호출한 쓰레드는 대상 쓰레드의 작업이 완료될 때까지 대기합니다. join()의 경우 동시성 문제나 작업의 실행 순서 보장 등을 위해 사용됩니다.
Ex)
class ThreadEx19_1 extends Thread {
public void run () {
for(int i =0;i<300;i++) {
System.out.print(new String("-"));
}
}
}
class ThreadEx19_2 extends Thread {
public void run() {
for(int i = 0; i<300;i++){
System.out.print(new String("|"));
}
}
}
- join을 사용하지 않은 경우
public class ThreadEx19 {
static long startTime = 0;
public static void main(String[] args) {
ThreadEx19_1 th1 = new ThreadEx19_1();
ThreadEx19_2 th2 = new ThreadEx19_2();
th1.start();
th2.start();
startTime = System.currentTimeMillis();
System.out.print("소요시간: " + (System.currentTimeMillis() - ThreadEx19.startTime));
}
}
출력값
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------소요시간: 3|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------------------------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- join()을 사용한 경우
public class ThreadEx19 {
static long startTime = 0;
public static void main(String[] args) {
ThreadEx19_1 th1 = new ThreadEx19_1();
ThreadEx19_2 th2 = new ThreadEx19_2();
th1.start();
th2.start();
startTime = System.currentTimeMillis();
try {
th1.join();
th2.join();
}catch (InterruptedException e) {}
System.out.print("소요시간: " + (System.currentTimeMillis() - ThreadEx19.startTime));
}
}
출력값
|||||||||||||||||||||---||||||||||||||||||||||||||||||||-----------------------|||||||||||||||||----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|||||||||||------------------------------------------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||소요시간: 29
위의 두 결과를 보면 join을 사용하지 않는 경우 정확한 소요시간을 측정할 수 없던 반면 join을 사용한 경우 모든 작업이 완료된 후 소요시간을 출력하는 것을 확인할 수 있습니다. 이는 2개 이상의 쓰레드가 병렬로 처리되면서 어떠한 작업이 먼저 완료될 지 알 수 없으며, 소요시간을 출력하는 시점에 아직 쓰레드가 실행 중일 수도 있어 정확한 소요시간을 측정하지 못하게 됩니다. 반면에 join()을 사용하여 작업의 실행 순서를 보장함으로써 모든 쓰레드의 작업이 완료된 후 main 쓰레드의 System.out.print를 호출하여 소요 시간을 출력하게 됩니다.
join()을 사용한 경우의 코드를 보면, main 쓰레드가 각각 th1과 th2 쓰레드의 join()을 호출한 쓰레드로 th1과 th2의 작업이 완료될 때까지 대기 상태가 되며, th1과 th2가 대상 쓰레드가 됩니다.
쓰레드의 동기화
쓰레드의 동기화란 2개 이상의 쓰레드가 실행되는 멀티 쓰레드 환경에서, 쓰레드 간 작업에서 동일 데이터를 이용하여 발생하는 데이터 무결성 문제 등의 문제를 극복하기 위해 사용되는 개념입니다. 쓰레드간 동일 데이터를 사용하는 코드 영역에 대해 임계 영역(critical section)으로 지정하고, 동시에 해당 쓰레드가 동일 데이터에 대해 작업하지 못하도록 조정하는 것입니다. 이때 동일 데이터가 사용되는 코드를 임계 영역이라하고, 쓰레드가 해당 데이터 사용 시 락(lock)을 획득하게 됩니다. 쓰레드는 임계 영역에 대해 락을 획득한 경우에만 해당 데이터에 대한 작업이 가능합니다.
* 멀티 쓰레드 동시성 문제
예를 들어 아래와 같은 코드 시랭시 코드 상으로는 잔액이 출금 금액보다 큰 경우에만 출금가능 하도록 되어 있지만 실제 출력값은 음수로 나오게 되는데 이는 특정 쓰레드가 해당 코드 부분의 작업을 완료하기 이전에 다른 쓰레드가 해당 코드 영역에 접근하여 작업을 실행 함으로써 발생한 문제입니다. 이를 해결하기 위해 synchronized를 사용할 수 있습니다.
public class ThreadEx21 {
public static void main(String[] args){
Runnable r = new RunnableEx21();
new Thread(r).start();
new Thread(r).start();
}
}
class Account {
private int balance = 1000;
public int getBalance() {
return balance;
}
public synchronized void withdraw(int money) {
if(balance>=money) {
try{
Thread.sleep(1000);
}catch (InterruptedException e) {}
balance -= money;
}
}
}
class RunnableEx21 implements Runnable {
Account acc = new Account();
public void run () {
while(acc.getBalance() >0) {
int money = (int)(Math.random() *3+1) *100;
acc.withdraw(money);
System.out.println("balance: " +acc.getBalance());
}
}
}
출처 :
자바의 정석(책)
'JAVA' 카테고리의 다른 글
[ Java ] - 난수 생성 (0) | 2023.10.18 |
---|---|
[ Java ] - 프로세스와 쓰레드 - 02) (0) | 2023.10.17 |
[ JAVA ] - 파일 작업하기 (파일 읽기) (0) | 2023.05.11 |
[ JAVA ] - 커스텀 어노테이션 만드는 법 (0) | 2023.05.06 |
[ Java ] - 상속 (instanceOf / 멤버변수가 같을 때 ) (0) | 2023.01.02 |