실행중인 프로그램.
프로그램을 실행하면 OS로부터 실행에 필요한 자원(Resource)을 할당받아 프로세스가 된다.
프로그램을 수행하는데 필요한 데이터와 메모리, CPU 등의 자원과 쓰레드로 구성되어있다.
흔히들 프로세스를 공장, 쓰레드를 일꾼에 비유한다.
프로세스 내에서 실제로 작업을 수행한다.
모든 프로세스는 최소 하나 이상의 쓰레드가 존재한다.
경량 프로세스(LWP, Light-weight process)라 하기도 한다.
둘 이상의 쓰레드를 가진 프로세스는 멀티 쓰레드 프로세스(multi-threaded process)라 한다.
한 프로세스 내에서 일꾼이 여러 명이라 여러 작업을 나눠서 효율적으로 처리한다.
대부분의 프로그램은 멀티 쓰레드.
싱글 쓰레드 방식은 동시에 다른 일을 할 수 없다.
class myThread extends Thread {
public void run() { // Thread 클래스의 run()을 오버라이딩
// 쓰레드가 수행 할 작업내용
}
}
class myThread2 implements Runnable {
public void run() { // Runnable 인터페이스의 추상메서드 run()을 구현
// 쓰레드가 수행 할 작업내용
}
}
MyThread t1 = new MyThread(); // 1번 방법으로 쓰레드 생성
Runnable r = new MyThread2(); // 2번방법으로 Runnable 인터페이스 만들고
Thread t2 = new Thread(r); // 2번방법으로 Thread 클래스의 생성자에 위에껄 넣음
Thread t3 = new Thread(new MyThread()); // 2번 방법을 한줄로 표현
start()를 호출해야만 쓰레드가 실행된다. ex) t1.start()
호출 되었다고 바로 실행되는건 아니고 실행 대기상태에 있다가 자신의 차례가 되면 실행된다.
실행 대기중인 쓰레드가 없으면 곧바로 실행된다.
실행이 종료된 쓰레드는 다시 실행할 수 없다. 하나의 쓰레드는 .start()가 한번만 호출된다.
두번 이상 호출하려면 쓰레드를 새로 생성해야 한다.
start()가 main 메서드에서 호출되면 start()는 새로운 쓰레드를 생성한다.
새 쓰레드가 작업하는 데 사용 될 새로운 호출 스택을 생성한다.
그 호출스택에서 run()이 호출되어 독립된 공간에서 작업을 수행한다.
이렇게 되면 호출 스택이 2개가 되므로 스케쥴러가 정한 순서에 의해 번갈아가며 실행된다.
쓰레드는 사용자 쓰레드와 데몬 쓰레드 두 종류가 있다.
실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.
단순히 CPU만 사용하는 계산작업이면 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 더 빠르고 효율적이다. 멀티쓰레드 환경에서는 쓰레드간 작업 전환(context switching)
에 시간이 걸리기 때문이다.
CPU가 싱글코어인 경우에는 멀티 쓰레드라도 하나의 코어가 번갈아가면서 작업을 수행하므로 두 작업이 겹치지 않는다. 하지만 멀티코어라면 동시에 두 쓰레드가 수행될 수 있으므로 두 작업이 겹쳐진다.
어떤 쓰레드를 얼마동안 실행할 것인지 결정하는 것은 OS의 프로세스 스케줄러가 결정한다. 그래서 쓰레드 실행 순서와 실행 시간은 일정하지 않다. Java가 OS 독립적이긴 하지만 OS 종속적인 부분이 몇개 있는데 쓰레드가 그 중 하나이다.
두 쓰레드가 서로 다른 자원을 사용하는 작업은 멀티쓰레드 프로세스가 더 효율적이다. 사용자로부터 데이터를 입력받는 작업이나 네트워크로 파일을 주고받는 작업, 프린트 인쇄 등 처럼 외부기기와의 입출력을 필요로하는 경우가 이에 해당한다.
I/O 블락킹
: 싱글 쓰레드 환경에서는 사용자로부터 입출력을 기다릴 때 작업이 중단된다.
하지만 멀티 쓰레드에서는 사용자 입력을 기다릴 때 다른 작업을 할 수 있다.
쓰레드는 우선순위(priority)라는 멤버변수(속성)를 가진다. 이 우선순위에 따라 쓰레드가 얻는 실행시간이 달라진다. 작업의 중요도에 따라 우선순위를 나눠 특정 쓰레드가 더 많은 작업시간을 갖게할 수 있다.
우선 순위의 범위는 1~10. 숫자가 높을수록 우선순위가 높다. main 쓰레드는 5. main에서 생성하는 쓰레드의 우선순위도 기본값은 5. .setPriority(7) 로 변경 가능
.setDeamon(true)
를 호출해야 한다.NEW
: 생성되고 아직 start()가 호출되지 않은 실행 전 상태
RUNNABLE
: 실행중 or 실행 가능한 상태
BLOCKED
: 동기화 블럭에 의해서 일시정지된 상태(lock이 풀릴 때 까지 기다리는 상태)
WAITING
: 쓰레드의 작업이 종료되진 않았지만 실행 가능하지 않은(unrunnable) 일시정지 상태
TIME_WAITING
: 일시정지 시간이 지정된 경우
TERMINATED
: 쓰레드의 작업이 종료 된 상태
쓰레드 프로그래밍이 어려운 이유는 동기화와 스케쥴링 때문이다. 효율적인 멀티쓰레드 프로그램을 만들기 위해서는 쓰레드가 낭비없이 사용되도록 정교한 스케쥴링을 짜야한다.
sleep으로 일시 정지가 된 쓰레드를 interrupt로 호출하면 InterruptedException이 발생해 즉시 잠에서 깨 실행상태가 된다. 그래서 sleep()은 항상 try-catch로 위의 예외를 처리해줘야 한다.
interrupt()는 쓰레드가 현재 블록되어 있는 상태에서 InterruptedException을 발생시켜 스레드를 중지시키는 역할을 한다. 즉시 중지하는 것이 아니라 쓰레드가 실행중인 작업이 끝날 때 까지 기다린 후 중지된다.
suspend()와 stop()은 쓰레드를 제어하는 가장 쉬운 방법이지만 교착상태(Deadlock)를 일으키기 쉬워 사용이 권장되지 않고 'deprecated'되었다.
멀티쓰레드 프로세스에서 여러 쓰레드가 같은 프로세스 내의 자원을 공유하기 때문에 서로의 작업에 영향을 주게 된다. 그래서 한 쓰레드가 특정 작업을 마치기 전 까지 다른 쓰레드에 의해 방해받지 않도록 해야하는데 방법이 임계영역(critical section)과 잠금(락,lock)이다.
공유 데이터를 임계영역으로 지정하고 lock을 획득 한 단 하나의 쓰레드만 이 영역에서 코드를 수행할 수 있게 한다. 해당 쓰레드가 작업을 완료하면 lock을 반납하면 다음 쓰레드가 임계영역에서 lock을 얻어 코드를 수행할 수 있다.
위 처럼 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화라고 한다.
가장 간단한 동기화 방법. 임계영역을 설정하는데 사용.
public synchronized void sum() 처럼 메서드 전체를 임계영역으로 저장하거나
synchronized(객체의 참조변수){..} 처럼 메서드 내 특정 영역을 임계영역으로 지정할 수 있다.
가능하면 후자로 임계영역을 최소화 하는 것이 성능면에서 효율적이다.
그리고 특정 쓰레드가 lock을 오래 보유하면 그 동안 다른 쓰레드가 lock을 기다리게 되므로 작업 속도가 느려진다. 동기화된 임계영역의 코드를 수행하다가 작업을 진행할 상황이 아니면 wait()을 호출해 일단 쓰레드가 락을 반납하고 기다리게 한 후 다른 쓰레드가 락을 얻게 한다. 그리고 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해 다시 작업을 진행하게 한다.
wait()과 notify()는 동기화 블록(synchronized)내에서만 사용할 수 있다.