[작성일: 2023. 02. 09]
프로세스와 스레드(process & thread)
- 프로세스: 실행 중인 프로그램. 자원(resources)과 스레드로 구성됨.
- 스레드: 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 최소한 하나의 스레드를 가지고 있음.
- 프로세스 : 스레드 = 공장 : 일꾼
- 싱글 스레드 프로세스: 자원 + 스레드
- 멀티 스레드 프로세스: 자원 + 스레드 + 스레드 +...+ 스레드
- 하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 스레드를 생성하는 것이 더 적은 비용이 듦.
멀티스레드의 장단점
- 대부분의 프로그램이 멀티스레드로 작성되어 있음.
장점 | - 시스템 자원을 보다 효율적으로 사용할 수 있음. - 사용자에 대한 응답성(responseness)이 향상됨. - 작업이 분리되어 코드가 간결해짐. - 두 가지 작업을 동시에 진행할 수 있음. |
단점 | - 동기화(synchronization)에 주의해야 함. - 교착상태(dead-lock)가 발생하지 않도록 주의해야 함. - 각 스레드가 효율적으로 고르게 실행될 수 있게 해야함.(기아현상) - 프로그래밍 할 때 고려해야 할 사항들이 많음. - 싱글 스레드보다는 실행속도가 느림. |
스레드의 구현과 실행
- Thread 클래스 상속(Java는 단일 상속)
class MyThread extends Thread {
public void run() { //Thread 클래스의 run()을 오버라이딩
/* 작업 내용 */
}
}
MyThread t1 = new MyThread(); // 스레드 생성
t1.start() // 스레드 실행
- Runnable 인터페이스 구현
class MyThread2 implements Runnable {
public void run() {
}
}
Runnable r = new MyThread2();
Thread t2 = new Thread(r); // Thread(Runnable r), 스레드의 생성자에 r을 넣음.
// Thread t2 = new Thread(new MyThread2()); 한 줄로 줄이기
t2. start();
- 스레드를 생성한 후에 start()를 호출해야 스레드가 작업을 시작함.
Thread1_1 t1 = new Thread1_1(); // 스레드 t1을 생성함.
Thread1_1 t2 = new Thread1_1(); // 스레드 t2을 생성함.
t1.start(); // 스레드 t1을 실행시킴.
t2.start(); // 스레드 t2을 실행시킴.
// OS의 스케줄러가 실행순서 결정함.
main 스레드
- main 메서드의 코드를 수행하는 스레드
- 스레드는 <사용자 스레드>와 <데몬 스레드(보조)> 두 종류가 있음.
- 실행 중인 사용자 스레드가 하나도 없을 때 프로그램이 종료됨.
스레드의 I/O 블락킹(blocking)
- input(입력) , output(출력) , 입출력 시 작업 중단
- 싱글 스레드로 작성하면 사용자로부터 입력을 기다리는 구간은 아무 일도 하지 않음.
import javax.swing.JOptionPane;
class ex {
public static void main(String[] args) throws Exception {
Thread_1 th1 = new Thread_1();
th1.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은" + input "입니다");
}
}
class Thread_1 extends Thread {
public void run() {
for(int i=10; i>0; i--) {
System.out.println(i);
try {
sleep(1000); // 1초간 시간을 지연함.
} catch(Exception e) {}
}
} // run()
}
스레드의 우선순위
- 작업의 중요도에 따라 스레드의 우선순위를 다르게 하여 특정 스레드가 더 많은 작업시간을 갖게 할 수 있음.
- 우선순위는 스레드가 실행된 후에도 변경할 수 있음.
- 우선순위가 높은 경우의 스레드가 먼저 실행 종료될 확률이 높음.
void setPriority(int newPriority) // 스레드의 우선순위를 지정한 값으로 변경함
int getPriority() // 스레드의 우선순위를 반환함.
public static final int MAX_PRIORITY = 10 // 최대 우선순위
public static final int MAX_PRIORITY = 1 // 최소 우선순위
public static final int MAX_PRIORITY = 5 // 보통 우선순위(기본값)
스레드 그룹
- 서로 관련된 스레드를 그룹으로 묶어서 다루기 위한 것
- 모든 스레드는 반드시 하나의 스레드 그룹에 포함되어 있어야 함.
- 스레드 그룹을 지정하지 않고 생성한 스레드는 main 스레드 그룹에 속함.
- 자신을 생성한 스레드(부모 스레드)의 그룹과 우선순위를 상속받음.
ThreadGroup getThreadGroup() 스레드 자신이 속한 스레드 그룹을 반환함.
void uncaughtException(Thread t, Throwable e) 처리되지 않은 예외에 의해 스레드 그룹의 스레드가 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출됨.
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)
스레드 메서드
메서드 | 설명 |
ThreadGroup(String name) | 지정된 이름의 새로운 스레드 그룹을 생성 |
ThreadGroup(ThreadGroup parent, String name) | 지정도니 스레드 그룹에 포함되는 새로운 스레드 그룹을 생성 |
int activeCount() | 스레드 그룹에 포함된 활성상태에 있는 스레드 수를 반환 |
int activeGroupCount() | 스레드 그룹에 포함된 활성상태에 있는 스레드 그룹의 수를 반환 |
void checkAccess() | 현재 실행중인 스레드가 스레드 그룹을 변경 할 권한이 있는지 체크 |
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 setDaemon(boolean daemon) | 스레드 그룹을 데몬 스레드 그룹으로 설정/해제 |
void setMaxPriority(int pri) | 스레드 그룹의 최대 우선순위를 설정 |
데몬 스레드(daemon thread)
- 일반 스레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행함.
- 일반 스레드가 모두 종료되면 자동적으로 종료됨.
- 가비지 컬렉터, 자동저장, 화면 자동갱신 등에 사용됨.
- 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성함.
boolean isDamon() 스레드가 데몬 스레드인지 확인함. 데몬 스레드면 true 반환
void setDaemon(boolean on) 스레드를 데몬 스레드로 또는 사용자 스레드로 변경함. 매개변수 on을 true로 지정하면 데몬 스레드가 됨.
* setDaemon(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 함. 그렇지 않으면 예외 발생.
public void run() {
while(true) {
try {
Thread.sleep(3 * 1000); // 3초마다
} catch(InterruptedException e) {}
// autoSave의 값이 true면 autoSave() 호출
if(autoSave) autoSave();
}
}
public void autoSave() {
System.out.println("작업 파일이 저장되었습니다.");
}
스레드의 상태
상태 | 설명 |
NEW | 스레드가 생성되고 아직 start()가 호출되지 않은 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화 블럭에 의해서 일시정지된 상태 lock이 풀릴 때까지 기다리는 상태 |
WAITING. TIMED_WAITING |
스레드의 작업이 종료되지는 않았지만 실행가능하지 않은 일시정지 상태. TIMED_WAITING은 일시정지시간이 지정된 경우를 의미함. |
TERMINATED | 스레드의 작업이 종료된 상태 |
스레드의 실행제어
- 스레드의 실행을 제어할 수 있는 메서드가 제공됨.
- 메서드를 사용하여 효율적인 프로그램을 작성할 수 있어야 함.
메서드 | 설명 |
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()에 의해 일시정지상태인 스레드를 깨워서 실행대기상태로 만듦. 해당 스레드에서는 Interrupted Exception이 발생함으로써 일시정지 상태를 벗어나게 됨 |
void stop() | 스레드를 즉시 종료시킴. |
void suspend() | 스레드를 일시정지시킴. resume()을 호출하면 다시 실행대기상태가 됨. |
void resume() | suspend()에 의해 일시정지 상태에 있는 스레드를 실행대기 상태로 만듦. |
static void yield() | 실행 중에 자신에게 주어진 실행시간을 다른 스레드에게 양보(yield)하고 자신은 실행대기상태가 됨. |
sleep()
- 현재 스레드를 지정된 시간 동안 멈추게 함.
- static 메서드(자기 자신한테만 동작함. 남을 재울 수 없음.)
- 특정 스레드를 지정해서 멈추게 하는 것은 불가능함.
static void sleep(long millis) // 천분의 일초 단위
static void sleep(long millis, int nanos) // 천분의 일초 + 나노초
- 예외처리를 꼭 해줘야 함. (InterruptedException이 발생하면 깨어남.)
try {
Thread.sleep(1, 500000); // 스레드를 0.0015초 동안 멈추게 함.
} catch (InterruptedException e) {} // 필수 예외처리
// 메서드 만드는 게 좋음
void delay(long millis) {
try {
Thread.sleep(millis);
} catch(InterruptedException e ) {}
} // delay(15); 등으로 호출
Interrupt()
- 대기상태(Waiting)인 스레드를 실행대기 상태(Runnable)로 만듦.
void interrupt() 스레드의 interrupted 상태를 false에서 true로 변경함.
boolean isInterrupted() 스레드의 interrupted 상태를 반환함.
static boolean interrupted() 현재 스레드의 interrupted 상태를 알려주고 false로 초기화함.
class Thread { ... boolean interrupted = false; ... boolean inInterrupted() { return interrupted; } boolean interrupt() { interrupted = true; } }
suspend(), resume(), stop()
void suspend() 스레드를 일시정지 시킴.
void resume() suspend()에 의해 일시정지된 스레드를 실행대기상태로 만듦.
void stop() 스레드를 즉시 종료시킴.
** 메서드를 작성해서 사용해야 함.이 메서드들은 교착상태에 빠지기 쉬워서 사용권장하지 않음.(dead-lock)class Thread implements Runnable { boolean suspended = false; // 일시정지 상태인가? boolean stopped = false; public void run() { while(!stopped) { if(!suspended) { /*스레드가 수행할 코드 작성 */ } } } public void suspend() { suspended = true; } public void resume() { suspended = false; } public void stop() { stopped = true; }
join()
- 지정된 시간 동안 특정 스레드가 작업하는 것을 기다림.
- 예외처리를 해줘야 함.(InterruptedException이 발생하면 작업 재개)
void join() 작업이 모두 끝날 때까지
void join(long millis) 천분의 일초 동안
void join(long millis, int nanos) 천분의 일초 + 나노초 동안
public static void main(String[] args) { Thread1 th1 = new Thread1(); Thread1 th2 = new Thread1(); th1.start(); th2.start(); startTime = System.curruntTimeMillis(); try { th1.join(); // main스레드가 th1의 작업이 끝날 때까지 기다림. th2.join(); // main스레드가 th2의 작업이 끝날 때까지 기다림. } catch(InterruptedException e) {} System.out.print("소요시간: " + (System.curruntTimeMillis() - Thread3.startTime)); } // main 종료
yield()
- 남은 시간을 다음 스레드에게 양보하고, 자신(현재 스레드)은 실행 대기함.
- yield()와 interrupt()를 적절히 사용하면 응답성과 효율성을 높일 수 있음.
- OS 스케쥴러한테 통보하는 것.(yield()가 반드시 동작한다는 보장이 없음.)
스레드의 동기화(synchronization)
- 멀티 스레드 프로세스에서는 다른 스레드의 작업에 영향을 미칠 수 있음.
- 진행 중인 작업이 다른 스레드에게 간섭받지 않게 하려면 동기화 필요
- 동기화: 한 스레드가 진행 중인 작업을 다른 스레드가 간섭하지 못하게 막는 것
- 동기화를 하려면 간섭받지 않아야 하는 문장들을 임계영역으로 설정
- 임계영역은 락(lock)을 얻은 단 하나의 스레드만 출입할 수 있음.(객체 1개당 락 1개)
synchronized를 이용한 동기화
- synchronized로 임계영역(lock이 걸리는 영역)을 설정하는 방법 2가지
- 임계영역이 많을 수록 성능이 떨어지므로 개수를 최소화하는 것이 좋음.
✏️ 메서드 전체를 임계영역으로 지정(성능 떨어짐)
public synchronized void calcSum() { /* 임계영역 다른 스레드가 간섭할 수 없게 묶어줌 */ }
✏️ 특정한 영역을 임계영역으로 지정
synchronized(객체의 참조변수) { /* 임계영역 */ } public void withderaw(int money) { synchronized(this) { if(balance >= money) { try { Thread.sleep(1000); } catch (Exception e) {} balnce -= money; // 동기화 하면 절대 음수가 나오지 않음. } } // synchronized(this) 끝 }
wait(), notify()
- 동기화의 효율을 높이기 위해 wait(), notify()를 같이 사용함.
- Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있음.
- wait() : 객체의 lock을 풀고 스레드를 해당 객체의 waiting pool에 넣음.
- notify : waiting pool에서 대기 중인 스레드 중 하나를 깨움.(순서 X, 랜덤)
- notifyAll() : saiting pool에서 대기 중인 모든 스레드를 깨움.
class Account {
int balance = 1000;
public synchronized void withdraw(int money) { // 출금, 동기화
while(balance < money) {
try {
wait();
// 대기 - 락을 풀고 기다림. 통지를 받으면 락을 재 획득함.
} catch(InterruptedException e) {}
}
balance -= money;
} // withdraw 끝
public synchronized void deposit(int money) { // 입금, 동기화
balance += money;
notify(); // 통보. 대기중인 스레드 하나에게 알림.
}
}
🐣 해당 게시글은 자바의 정석(남궁성 님) 영상으로 함께 공부하며 요약/정리한 글입니다.
🐣 입문 개발자가 작성한 글이므로 틀린 내용이나 오타가 있을 수 있습니다.