프로세스(process)란 실행 중인 프로그램(program)
이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
Program + PCB = Process
PCB(Process Control Block)는 운영체제가 프로세스를 제어하기 위해 정보를 저장해 놓는 곳으로, 프로세스의 상태 정보
를 저장하는 자료구조이다.
프로세스가 생성될 때마다 고유의 PCB가 생성되고, 프로세스가 완료되면 PCB도 함께 제거 된다.
=> PCB를 이용하여 CPU에서 문맥 교환(Context Switch)을 한다.
프로세스 자원을 이용해서 실제로 작업을 수행하는 것
이다.
쓰레드를 구현하는 방식은 2가지가 있다.
둘다 run() 메소드를 오버라이딩 하는 방식을 사용한다.
public class MyThread implements Runnable {
@Override
public void run() {
// 수행 코드
}
}
public class MyThread extends Thread {
@Override
public void run() {
// 수행 코드
}
}
이 때 Thread 클래스는 extends로 상속하기 때문에 다른 클래스를 상속받을 수 없고, Runnable 인터페이스는 implement로 상속하기 때문에 여러 인터페이스 상속 가능
인터페이스로 상속하는 방법이 재사용성(reuseability)
이 높고 코드의 일관성(consistency)
를 유지할 수 있기 때문에 좀더 객체지향적 코드이다.
public class MyThread {
public static void main(String[] args) {
Thread_Ex2 t1 = new Thread_Ex2(); // 방법1
Runnable r = new Thread_Ex1(); // 방법2
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
class Thread_Ex1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
System.out.println(Thread.currentThread().getName()); // 현재 실행중인 Thread를 반환한다.
}
}
}
class Thread_Ex2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
System.out.println(getName());
}
}
}
쓰레드의 실행은 run() 호출이 아닌 start() 호출로 해야한다.
ex) t1.start();
start()와 run()의 실행과정
main메소드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메소드를 호출하는 것일뿐이다.
반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 한다.
여기서 호출 스택이란 실질적인 명령어들을 담고 있는 메모리로, 하나씩 써내서 실행하는 역할을 한다. 즉, 두 가지 작업을 동시에 하기 위해서는 두 개의 호출 스택이 필요
모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 중요되면 작업에 사용된 호출스택은 사라진다.
주의!!
한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 뜻이다.
ThreadEx1 t1 = new ThreadEx1();
t1.start();
t1.start(); // 에러 발생
ThreadEx1 t1 = new ThreadEx1();
t1.start();
t1 = new ThreadEx1(); // 다시 생성
t1.start(); // OK
NEW : 쓰레드가 생성되었지만 쓰레드가 아직 실행할 준비가 되지 않음
RUNNABLE : 쓰레드가 실행되고 있거나 실행준비되어 스케줄링을 기다리는 상태
WAITING : 다른 쓰레드가 notify(), notifyAll()을 불러주기 기다리고 있는 상태(동기화)
TIMED_WAITING : 쓰레드가 sleep(n) 호출로 인해 n 밀리초동안 잠을 자고 있는 상태
BLOCK : 쓰레드가 I/O작업을 요청하면 자동으로 쓰레드를 BLOCK 상태로 만듬
TERMINATED : 쓰레드가 종료한 상태
public class Thread implements Runnable {
public static final int MIN_PRIORITY = 1; // 가장 높은 우선 순위
public static final int NORM_PRIORITY = 5; // 일반 쓰레드의 우선 순위
public static final int MAX_PRIORITY = 10; // 가장 낮은 우선 순위
}
모든 쓰레드는 1 ~ 10 사이의 우선순위를 가진다. 디폴트는 5이다.
두개의 쓰레드가 동시성에 따라 2개의 작업을 번갈아가며 실행할 때 우선순위가 높은 쓰레드가 더 많은 시간을 할당받는다.
하지만, 우선순위는 되도록 사용하지 않는 것이 좋다.
JVM는 모든 쓰레드가 끝날 때까지 기다린다.
만약 쓰레드가 무한정 실행된다면??
JVM은 무한루프를 돌것이다. 이를 해결하기 위해 데몬 쓰레드가 등장하였다.
어떤 쓰레드를 데몬 쓰레드로 지정하면, 그 쓰레드가 수행하고 있던, 수행되지 않고 있든 상관없이 JVM은 끝날 수 있다.(단, start()가 수행되기 전에 데모 쓰레드로 지정되어야 한다.)
사용 예제로는 주요 쓰레드를 모리터링하는 쓰레드를 만들 때 사용된다.
이때 주요 쓰레드가 끝이 났을 때 모니터링 쓰레드가 데몬이라 프로세스가 종료될 수 있다.
멀티스레드로 구현을 하다보면, 동기화는 필수적이다.
동기화가 필요한 이유는, 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문이다.
스레드의 동기화를 위해선, 임계 영역(critical section)과 잠금(lock)을 활용한다.
임계영역을 지정하고, 임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려주는 개념으로 이루어져있다.
따라서 임계구역 안에서 수행할 코드가 완료되면, lock을 반납해줘야 한다.
synchronized를 활용해 임계영역을 설정할 수 있다.
서로 다른 두 객체가 동기화를 하지 않는 메소드를 같이 오버라이딩해서 이용하면, 두 스레드가 동시에 진행되므로 원하는 출력 값을 얻지 못한다.
이때 오버라이딩되는 부모 클래스의 메소드에 synchronized 키워드로 임계영역을 설정해주면 해결할 수 있다.
//synchronized : 스레드의 동기화. 공유 자원에 lock
public synchronized void saveMoney(int save){ // 입금
int m = money;
try{
Thread.sleep(2000); // 지연시간 2초
} catch (Exception e){
}
money = m + save;
System.out.println("입금 처리");
}
public synchronized void minusMoney(int minus){ // 출금
int m = money;
try{
Thread.sleep(3000); // 지연시간 3초
} catch (Exception e){
}
money = m - minus;
System.out.println("출금 완료");
}
쓰레드가 서로 협력관계일 경우에는 무작정 대기시키는 것으로 올바르게 실행되지 않기 때문에 사용
이 두 메소드는 동기화 된 영역(임계 영역)내에서 사용되어야 한다.
동기화 처리한 메소드들이 반복문에서 활용된다면, 의도한대로 결과가 나오지 않는다. 이때 wait()와 notify()를 try-catch 문에서 적절히 활용해 해결할 수 있다.
/**
* 스레드 동기화 중 협력관계 처리작업 : wait() notify()
* 스레드 간 협력 작업 강화
*/
public synchronized void makeBread(){
if (breadCount >= 10){
try {
System.out.println("빵 생산 초과");
wait(); // Thread를 Not Runnable 상태로 전환
} catch (Exception e) {
}
}
breadCount++; // 빵 생산
System.out.println("빵을 만듦. 총 " + breadCount + "개");
notify(); // Thread를 Runnable 상태로 전환
}
public synchronized void eatBread(){
if (breadCount < 1){
try {
System.out.println("빵이 없어 기다림");
wait();
} catch (Exception e) {
}
}
breadCount--;
System.out.println("빵을 먹음. 총 " + breadCount + "개");
notify();
}
조건 만족 안할 시 wait(), 만족 시 notify()를 받아 수행한다.
위와 같이 메소드에 synchronized를 사용하는 방법은 성능상에 문제가 발생할 수있다. 예를 들어, 메소드의 코드가 30줄인데 동시성 관리해야하는 코드는 1줄일 수 있다.
이를 해결하기 위해 synchronized 블록
이 나왔다.
public class CommonCalculate {
private int amount;
Object lock = new Object();
public void plus(int value) {
synchronized (lock) {
amount += value;
}
}
public synchronized void minus(int value) {
synchronized (lock) {
amount -= value;
}
}
}
이런 식으로 한번에 하나의 쓰레드만 접근할 수 있게 해줄 수 있다.