Java의 정석 의 책을 읽고 정리한 내용입니다.
✏️ 프로세스란?
프로세스란 간단히 말해서실행 중인 프로그램
이다.
멀티쓰레드 프로세스(multi-threaded process) : 둘 이상의 쓰레드를 가진 프로세스
💡 참고
쓰레드를 프로세스라는 작업공간(공장)에서 작업을 처리하는 일꾼으로 생각하자!
✔️ 멀티쓰레딩의 장점
💡 참고
- 쓰레드를 가벼운 프로세스 즉, 경량 프로세스(LWP)라고 부르기도 한다.
- 교착상태란 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰 있는 상태를 말한다.
- 쓰레드를 구현하는 방법은
Thread
클래스를 상속받는 방법과Runnable
인터페이스를 구현하는 방법, 모두 2가지가 있다.Thread
클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에,Runnable
인터페이스를 구현하는 방법이 일반적이다.Runnable
인터페이스를 구현하는 방법은 재사용성(reusability
)이 높고 코드의 일관성(consistency
)을 유지할 수 있기 때문에 보다 객체지향적인 방법이라 할 수 있다.
1) Thread 클래스를 상속
class MyThread extends thread{
public void run(){/*작업 내용*/}// Thread 클래스의 run()을 오버라이딩
}
2) Runnable 인터페이스를 구현
class MyThread implements Runnable{
public void run(){/*작업 내용*/}// Runnable인터페이스의 추상메서드 run()을 구현
}
Runnable
인터페이스는 run()
메서드만 정의되어 있는 간단한 인터페이스이다. Runnable
인터페이스를 구현하기 위해서 해야 할 일은 추상 메서드인 run()
의 몸통{}
을 만들어 주는 것뿐이다.public interface Runnable{
public abstract void run();
}
Thread
클래스를 상속받으면, 자손 클래스에서 조상인 Thread
클래스의 메서드를 직접 호출할 수 있지만, Runnable
을 구현하면 Thread
클래스의 static
메서드인 currentThread()
를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출이 가능하다.
static Thread currentThread() - 현재 실행중인 쓰레드의 참조를 반환한다.
String getName() - 쓰레드의 이름을 반환한다.
Runnable
은 run()
밖에 없기 때문에 Thread
클래스의 getName()
을 호출하려면 Thread.currentThread().getName()
와 같이 해야한다.
class ThreadEx implements Runnable{
public void run(){
for(int i = 0; i < 5; i++){
// Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
System.out.println(Thread.currentThread().getName());
}
}
}
✔️ 쓰레드의 실행 - start()
start()
가 한 번만 호출될 수 있다.start()
를 호출해야 한다.start()
를 두 번 이상 호출하면 실행시에 IllegalThreadStateException
이 발생한다.ThreadEx1_1 t1 = new ThreadEx1_1();
t1.start();
t1.start(); // 예외 발생
→
ThreadEx1_1 t1 = new ThreadEx1_1();
t1.start();
t1 = new ThreadEx1_1(); // 다시 생성
t1.start(); // OK
1. main메서드에서 쓰레드의 start()를 호출한다.
2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.
3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
4. 이제는 호출스택이 2개가 되므로 스케쥴러가 정한 순서에 의해서 번갈아 가면서 실행된다.
✔️ main쓰레드
main
메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main
쓰레드라고 한다.main
메서드를 호출해서 작업이 수행되도록 하는 것이다.
💡 참고
컨텍스트 스위칭(context switching)
: 프로세스 또는 쓰레드 간의 작업 전환
✔️ 싱글코어와 멀티코어의 비교
💡 참고
- 병행(
concurrent
) : 여러 쓰레드가 여러 작업을 동시에 진행하는 것- 병렬(
parallel
) : 하나의 작업을 여러 쓰레드가 나눠서 처리하는 것
✔️ 싱글쓰레드 프로세스 (위)와 멀티쓰레드 프로세스(아래)의 비교
- 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다.
- 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.
- 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선 순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.
✔️ 쓰레드의 우선순위 지정하기
void setPriority(int newPriority) : 쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority() : 쓰레드의 우선순위를 반환한다.
public static final int MAX_PRIORITY = 10 // 최대 우선 순위
public static final int MIN_PRIORITY = 1 // 최소 우선 순위
public static final int NORM_PRIORITY = 5 // 보통 우선 순위
main
메서드를 수행하는 쓰레드는 우선순위가 5이므로 main
메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.class ThreadPriority {
public static void main(String args[]) {
A th1 = new A();
B th2 = new B();
th1.setPriority(7); // defalut 우선순위 5
System.out.println("Priority of th1(-) : " + th1.getPriority() );
System.out.println("Priority of th2(|) : " + th2.getPriority() );
th1.start();
th2.start();
}
}
class A extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("-");
for(int x=0; x < 10000000; x++);
}
}
}
class B extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("|");
for(int x=0; x < 10000000; x++);
}
}
}
Priority of th1(-) : 5
Priority of th2(|) : 7
th1
과 th2
모두 main
메서드에서 생성하였기 때문에 main
메서드를 실행하는 쓰레드의 우선순위인 5를 상속받았다. th2.setPriority(7)
로 th2
의 우선순위를 7로 변경한 다음에 start()
를 호출해서 쓰레드를 실행시켰다. th2
의 실행시간이 th1
에 비해 상당히 늘어났다.
- 쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 폴더를 생성해서 관련된 파일들을 함께 넣어서 관리하는 것처럼 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다.
생성자/메소드 | 설명 |
---|---|
ThreadGroup(String name) | 지정된 이름의 새로운 쓰레드 그룹을 생성 |
ThreadGroup(ThreadGroup parent, String name) | 지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹 생성 |
int activeCount() | 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드의 수를 반환 |
int activeGroupCount() | 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 그룹의 수를 반환 |
void checkAccess() | 현재 실행중인 쓰레드가 쓰레드 그룹을 변경할 권한이 있는지 체크. 만일 권한이 없다면 SecurityException을 발생시킨다. |
void destroy() | 쓰레드 그룹과 하위 쓰레드 그룹까지 모두 삭제한다. 단, 쓰레드 그룹이나 하위 쓰레드 그룹이 모두 비어있어야 한다. |
int enumerate(Thread[] list) int enumerate(Thread[] list, boolean recurse) int enumerate(ThreadGroup[] list) int enumerate(ThreadGroup[] list, boolean recurse) | 쓰레드 그룹에 속한 쓰레드 또는 하위 쓰레드 그룹의 목록을 지정된 배열에 담고 그 개수를 반환 두 번째 매개변수인 recurse의 값을 true로 하면 하위 쓰레드 그룹에 쓰레드 또는 쓰레드 그룹까지 배열에 담는다. |
int getMaxPriority() | 쓰레드 그룹의 최대우선순위를 반환 |
String getName() | 쓰레드 그룹의 이름을 반환 |
ThreadGroup getParent() | 쓰레드 그룹의 상위 쓰레드그룹을 반환 |
void interrupt() | 쓰레드 그룹에 속한 모든 쓰레드를 interrupt |
boolean isDaemon() | 쓰레드 그룹이 데몬 쓰레드그룹인지 확인 |
boolean isDestroyed() | 쓰레드 그룹이 삭제되었는지 확인 |
void list() | 쓰레드 그룹에 속한 쓰레드와 하위 쓰레드그룹에 대한 정보를 출력 |
boolean parentOf(ThreadGroup g) | 지정된 쓰레드 그룹의 상위 쓰레드그룹인지 확인 |
void setDeamon(boolean daemon) | 쓰레드 그룹을 데몬 쓰레드그룹으로 설정/해제 |
void setMaxPriority(int pri) | 쓰레드 그룹의 최대우선순위를 설정 |
✔️ 쓰레드를 쓰레드 그룹에 포함시키는 방법
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
main
쓰레드 그룹의 하위 쓰레드 그룹이 되며, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main
쓰레드 그룹에 속하게 된다.
✔️ 그 외에 Thread
의 쓰레드 그룹과 관련된 메서드
ThreadGroup getThreadGroup() // 쓰레드 자신이 속한 쓰레드 그룹을 반환한다.
void uncaughtException(Thread t, Throwable e) // 쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다.
- 데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.
- 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되는데, 그 이유는 데몬 쓰레드는 일반 쓰레드의 보조 역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다.
- 이 점을 제외하고는 데몬 쓰레드와 일반 쓰레드는 다르지 않다.
- 데몬 쓰레드의 예로는 가비지 컬렉션, 워드 프로세서의 자동저장, 화면 자동갱신 등이 있다.
- 데몬 쓰레드는 무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
- 데몬 쓰레드는 일반 쓰레드의 작성 방법과 실행 방법이 같으며 다만 쓰레드를 생성한 다음 실행하기 전에
setDaemon(true)
를 호출하기만 하면 된다.- 그리고 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.
boolean isDaemon() // 쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드이면 true 반환한다.
void setDaemon(boolean on) // 쓰레드를 데몬 쓰레드 또는 사용자 쓰레드로 변경한다. 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다.
class ThreadEx implements Runnable{
static boolean autoSave = false;
public static void main(String[] args) {
Thread t = new Thread(new ThreadEx());
t.setDaemon(true); // 이 부분이 없으면 종료되지 않는다.
t.start();
for(int i=1; i<=10; i++)
{
try{
Thread.sleep(1000);
}catch(InterruptedException e){}
System.out.println(i);
if(i==5)
autoSave = true;
}
System.out.println("프로그램을 종료합니다.");
}
@Override
public void run() {
while(true)
{
try{
Thread.sleep(3 * 1000);
}catch(InterruptedException e){}
if(autoSave)
autoSave();
}
}
private void autoSave() {
System.out.println("작업파일이 자동저장되었습니다.");
}
}
1
2
3
4
5
작업파일이 자동저장되었습니다.
6
7
8
작업파일이 자동저장되었습니다.
9
10
프로그램을 종료합니다.
setDaemon
메서드는 반드시 start()
를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException
이 발생한다.
💡 참고
getAllStackTraces()
를 이용하면 실행 중 또는 대기상태, 즉 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력할 수 있다.- 프로그램을 실행하면, JVM은 가비지컬렉션, 이벤트처리, 그래픽처리와 같이 프로그램이 실행되는데 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동적으로 생성해서 실행시킨다.
- 이들은
system 쓰레드 그룹
또는main 쓰레드 그룹
에 속한다.
- 효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.
✔️ 쓰레드의 스케줄링과 관련된 메서드
메서드 | 설명 |
---|---|
static void sleep(long millis), static void sleep(long millis, int nanos) | 지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기 상태가 된다. |
void join(), void join(long millis), void join(long millis, int nanos) | 지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다. |
void interrupt() | sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기 상태로 만든다. 해당 쓰레드에서는 interruptedException이 발생함으로써 일시정지 상태를 벗어나게 된다. |
void stop() | 쓰레드를 즉시 종료시킨다. |
void suspend() | 쓰레드를 일시정지 시킨다. resume()을 호출하면 다시 실행대기 상태가 된다. |
void resume() | suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기 상태로 만든다. |
static void yield() | 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행대기상태가 된다. |
✔️ 쓰레드의 상태
상태 | 설명 |
---|---|
NEW | 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다림) |
WAITING, TIMED_WAITING | 쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태. TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다. |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
💡 참고
쓰레드의 상태는Thread
의getState()
메서드를 호출해서 확인할 수 있다. JDK1.5부터 추가되었다.
✔️ 쓰레드의 상태가 어떻게 변화되는지 알아보자!
start()
를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐(queue
)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.yield()
를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.suspecd()
, sleep()
, wait()
, join()
, I/O block
에 의해 일시정지상태가 될 수 있다. I/O block
은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기상태가 된다.time-out
), notify()
, resume()
, interrupt()
가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.stop()
이 호출되면 쓰레드는 소멸된다.
💡 참고
번호의 순서대로 쓰레드가 수행되는 것은 아니다.
✔️ Sleep(long mills) - 일정시간 동안 쓰레드를 멈추게 한다.
static void sleep(long mills) //static = 자기자신
static void sleep(long mills, int nanos) //static = 자기자신
try{
Thread.sleep(1, 500000); // 쓰레드를 0.0015초 동안 멈추게 한다.
}catch(InterruptedException e){}
sleep()
에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다되거나 interrupt()
가 호출되면(InterruptedException
발생), 잠에서 깨어나 실행대기 상태가 된다.sleep()
을 호출할 때는 항상 try-catch
문으로 예외를 처리해줘야 한다.
th1.start();
th2.start();
try{
th1.sleep(2000);
}catch(InterruptedException e){}
System.out.println("<<main 종료>>");
sleep()
이 항상 현재 실행 중인 쓰레드에 대해 작동하기 떄문에 th1.sleep(2000)
과 같이 호출 하였어도 실제로 영향을 받는 것은 main
메서드를 실행하는 main
쓰레드 이다.sleep()
은 Thread.sleep(2000)
과 같이 해야 한다.
✔️ interrupt()와 interrupted() - 쓰레드의 작업을 취소한다.
interrupt()
는 쓰레드에게 작업을 멈추라고 요청한다. 단지 멈추라고 요청만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다.interrupt()
는 그저 쓰레드의 interrupted
상태(인스턴스 변수)를 바꾸는 것일 뿐이다.interrupted()
는 쓰레드에 대해 interrupt()
가 호출되었는지 알려준다.interrupt()
가 호출되지 않았다면 false
를, 호출되었다면 true
를 반환한다.
Thread th = new Thread();
th.start();
// ...
th.interrupt(); // 쓰레드 th에 interrupt()를 호출한다.
class MyThread extends Thread{
public void run(){
while(!interrupted()){ // interrupted()의 결과가 false인 동안 반복
...
}
}
}
void interrupt() : Thread의 interrupted 상태를 false에서 true로 변경
boolean interrupted() : Thread interrupted 상태를 반환
static boolean interrupted() : 현재 Thread의 interrupted 상태를 반환후, false로 변경
✔️ suspend(), resume(), stop()
suspend()
는 sleep()
처럼 쓰레드를 멈추게 한다. suspend()
에 의해 정지된 쓰레드는 resume()
을 호출해야 다시 실행대기 상태가 된다. stop()
은 호출되는 즉시 쓰레드가 종료된다.suspend()
와 stop()
이 교착상태(deadlock
)를 일으키기 쉽게 작성되어 있으므로 사용이 권장되지 않는다. 그래서 이 메서드들은 모두 deprecated
되었다.deprecated
: 전에는 사용되었지만, 앞으로 사용하지 않을 것을 권장한다.deprecated
된 메서드는 하위 호환성을 위해서 삭제하지 않는 것일 뿐이므로 사용해서는 안된다.
✔️ yield() - 다른 쓰레드에게 양보한다.
yield()
는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield
)한다. yield()
가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기상태가 된다.yield()
와 interrupt()
를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있다.
✔️ join() - 다른 쓰레드의 작업을 기다린다.
void join()
void join(long millis)
void join(long millis, int nanos)
join()
을 사용한다. join()
을 사용한다.try{
th1.join(); // 현재 실행중인 쓰레드가 쓰레드의 th1의 직업이 끝날때까지 기다린다.
} catch(InterruptedException e){}
join()
도 sleep()
처럼 interrupt()
에 의해 대기상태에서 벗어날 수 있으며, join()
이 호출되는 부분을 try-catch
문으로 감싸야 한다. join()
은 여러모로 sleep()
과 유사한 점이 많은데, sleep()
과 다른 점은 join()
은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static
메서드가 아니라는 것이다.
sleep()
을 이용해서 주기적으로 실행되도록 하다가 필요할 때마다 interrupt()
를 호출해서 즉시 가비지 컬렉션이 이루어지도록 하는 것이 좋다. join()
도 함께 사용해야한다는 것을 기억하자!