//Thread 생성자
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
...
//관련 메서드
//쓰레드 자신이 속한 쓰레드 그룹을 반환한다 (Thread class)
ThreadGroup getThreadGroup()
//처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다. (ThreadGroup class)
//overriding하여 다른 동작을 하도록 할 수 있다
void uncaughtException(Thread t, Throwable e)
public void run(){
//일반쓰레드가 종료되면 자동적으로 종료되기 때문에 무한루프 사용해도 괜찮다.
while(true){
try{
Thread.sleep(3 * 1000); //3초마다 실행
} catch(InterruotedException e){}
//autoSave의 값이 true이면 autoSave()를 호출한다.
if(autoSave){
autoSave();
}
}
}
boolean isDaemon() //쓰레드가 데몬 쓰레드인지 확인한다 데몬이면 true
void setDaemon(boolean on) //쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경
💡
setDaemon()
은 반드시start()
를 호출하기 전에 실행되어야 한다. 그렇지 않으면IllegalThreadStateException
발생한다.
start()
를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐 (Queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.yield()
를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.suspend()
, sleep()
, wait()
, join()
, I/O block에 의해 일시정지상태가 될 수 있다. notify()
, resume()
, interrupt()
가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.💡 쓰레드의 동기화
한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것
synchronized로 임계영역 (lock이 걸리는 영역)을 설정하는 방법 2가지
1. 메서드 전체를 임계 영역으로 지정
: 반환타입 앞에 synchronized
키워드를 붙여준다.
public synchronized void calcSum(){
//...
}
synchronized(객체의참조변수){
//...
}
Account class
에서 출금 메서드 (withdraw()
)에서 잔고가 출금하려는 금액보다 클 때에만 출금이 가능하도록 되어있다. 하지만 synchronized
키워드를 사용하지 않는다면 멀티쓰레드로 실행하면 음수가 나오는 것을 볼 수 있다.
이는 한 쓰레드가 if문 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다.
이처럼 한 쓰레드의 작업이 다른 쓰레드에 의해서 영향을 받는 일이 발생할 수 있기 때문에 동기화가 반드시 필요하다.
public class SynchronizedEx2 {
public static void main(String[] args) {
Runnable r = new SynchronizedEx2_1();
new Thread(r).start();
new Thread(r).start();
}
}
class Account {
private int balance = 1000;
public (synchronized) int getBalance() {
return balance;
}
public (synchronized) void withdraw(int money) {
if (balance >= money) {
//조건문을 통과하고 다른 쓰레드에게 넘겨주기 위해 sleep()을 이용
try {Thread.sleep(1000);} catch (InterruptedException e) {}
balance -= money;
}
}//withdraw
}
class SynchronizedEx2_1 implements Runnable{
Account acc = new Account();
@Override
public void run() {
while (acc.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println("잔액: " + acc.getBalance());
}
}//run
}
//result
잔액: 700
잔액: 700
잔액: 500
잔액: 500
잔액: 200
잔액: -100
wait()
, notify()
동기화로 공유 데이터를 보호하는 것은 좋으나, 특정 쓰레드가 객체의 락을 가진 상태로 오래 있는 것은 비효율적이다. 다른 쓰레드들이 해당 객체를 기다리느라 다른 작업들도 원활히 진행되지 않을 것이기 때문이다.
동기화의 효율을 높이기 위해 wait(), notify()를 사용
wait()
: 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.notify()
: waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.notifyAll()
: waiting pool에서 대기중인 모든 쓰레드를 깨운다.notifyAll()
을 더 많이 사용한다.class Account{
int balance = 1000;
public synchronized void withdraw(int money){
/* 잔고가 부족할 경우 wait()를 호출하여 lock을 풀고 waiting pool에
들어가면서 락을 다른 쓰레드에게 양보하게 된다. */
while(balance < money){
try{
wait();
}catch(InterruptedException e){ }
}
balance -= money;
}
/* 다른 쓰레드에 의해서 deposit()메서드가 호출되어 잔고가 증가하면서 notify()를
호출하면 객체의 waiting pool에서 기다리고 있던 쓰레드를 깨우게 된다. */
public synchronized void deposit(int money){
balance += money;
notify();
}
}
1) wait() & notify()는 쓰레드의 종류를 구분하지 않고, 공유 객체의 waiting pool에 같이 몰아넣는다. 그리고 notify()를 대상을 구분하지 않고 통지한다는 문제가 있다.
불필요하게 자신이 원하는 상태가 아닐 때도 깨어나서 lock을 얻기 위해 경쟁하게 된다. 이처럼 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것을 ‘경쟁 상태 (race condition)’라고 하는데, 이 경쟁 상태를 개선하기 위해서는 쓰레드를 구별해서 통지하는 것이 필요하다.
2) 지독히 운이 나쁘면 쓰레드는 계속 통지 받지 못하고 오랫동안 기다리게 되는데, 이를 ‘기아 (starvation) 현상’이라고 한다. 이 현상을 막으려면 notify() 대신 notifyAll()을 사용한다.
이러한 문제를 해결하기 위해 Lock
과 Condition
을 이용할 수 있다.