쓰레드의 실행 제어

쓰레드의 상태

상태 설명
NEW 쓰레드가 실행되고 아직 start()가 호출되지 않은 상태
RUNNABLE 실행 중 또는 실행 가능한 상태
BLOCKED 동기화블럭에 의해서 일시정지된 상태 ( lock이 풀릴 때까지 기다리는 상태 )
WAITING,
TIMED_WAITING
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은 (unrunnable) 일시정지 상태, TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다.
TERMINATED 쓰레드의 작업이 종료된 상태

 

쓰레드의 스케줄링과 관련된 메서드

메서드 설명
static void sleep(long mills),
static void sleep(long mills, int nanos)
지정된 시간( 천분의 일초 단위 )동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다. (현재 실행중인 쓰레드를 정지시킴)
void join(),
void join(long mills),
void join(long mills, int nanos)
지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
void interrupt() sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지상태를 벗어나게 된다.
void stop() 쓰레드를 즉시 종료시킨다.
void suspend() 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다.
void resume() suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.
static void yield() 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행대기상태가 된다.

 

쓰레드의 동기화( synchronization )

한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 뜻합니다.

 

임계 영역(critical section)과 잠금(락,lock)을 사용한 동기화 

  • 쓰레드가 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 합니다. 그리고 해당 쓰레드임계 영역 내의 모든 코드를 수행하고 벗어나서 lock반납해야만 다른 쓰레드반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 됩니다.

 

JAVA에서의 쓰레드 동기화 방법

1. synchronized를 이용한 동기화

  • 가장 간단한 방법으로 synchronized 키워드를 사용하여 임계 영역을 설정하여 동기화하는 방법입니다.
  • 임계 영역을 설정하기만 하면 lock획득반납이 자동적으로 수행됩니다. 
  • 메서드 전체를 임계 영역으로 지정하는 방법과 메서드 내의 특정한 영역을 임계 영역으로 지정하는 방법이 있습니다.

메서드 앞에 synchronized를 붙이는 방법

// 메서드 전체를 임계 영역으로 지정
public synchronized void func(){
	...
}
  • 메서드 전체임계 영역으로 지정하는 방법
  • 쓰레드synchronized메서드가 호출되는 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환합니다.

메서드 내의 코드 일부에 synchronized를 붙이는 방법

// 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수){
	...
}
  • 메서드 내의 특정한 영역을 임계 영역으로 지정하는 방법
  • 메서드 내의 코드 일부를 블럭 {}으로 감싸고 블럭 앞에 'synchronized(참조변수)'를 붙여 synchronized 블럭을 만드는 방법입니다.
  • 쓰레드는 이 블럭의 영역 안으로 들어가면서부터 지정된 객체의 lock을 획득하고, 블럭을 벗어나면 lock을 반납합니다.
임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 락을 거는 것보다 synchronized블럭으로 임계 영역을 최소화하는 것을 권장합니다.

 

동기화 전 코드

public class ThreadEx21 {
    public static void main(String[] args) {
        RunnableEx22 r = new RunnableEx22();
        new Thread(r).start();
        new Thread(r).start();
    }
}

class Account{
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

    public  void withdraw(int money) {
        if (balance >= money) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            balance -= money;
        }
    }
}

class RunnableEx22 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(Thread.currentThread().getName() + "  balance : " + acc.getBalance());
        }
    }
}
  • 계좌의 잔금을 확인하여 출금하는 메서드 withdraw()를 구현했습니다.
  • 여기서 동기화를 해주지 않았기 때문에, 하나의 쓰레드에서 조건식 if( balance >= money )를 통과하고 출금하기 직전에 다른 쓰레드가 끼어들어 출금을 먼저 하는 경우가 발생할 수 있습니다. 

실행 결과

Thread-0  balance : 800
Thread-1  balance : 600
Thread-0  balance : 300
Thread-1  balance : 300
Thread-0  balance : 0
Thread-1  balance : -300	// 잔금이 음수가 되는 문제가 발생
  • Thread1withdraw()를 호출하여 조건문을 통과한 순간 Thread0가 출금을 해서 잔금이 음수가 되는 문제가 발생합니다.

synchronized 키워드를 이용하여 동기화한 코드

// synchronized 키워드를 사용하여 withdraw 메서드 동기화
public synchronized void withdraw(int money) {
  if (balance >= money) {
  	try {
  	Thread.sleep(1000);
     } catch (InterruptedException e) {}
	balance -= money;
    }
}
  • withdrawsynchronized 키워드를 사용하여 한 번에 하나의 쓰레드만 코드 영역을 수행할 수 있게 변경하였습니다.

실행 결과

Thread-0  balance : 700
Thread-1  balance : 400
Thread-0  balance : 200
Thread-1  balance : 0
Thread-0  balance : 0

 

2. wait() & notify()를 이용한 더 효율적인 동기화

synchronized로 동기화하는 경우 공유 데이터보호할 수 있다는 장점이 있습니다. 하지만 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요합니다.
( 예를 들어 계좌에 출금할 돈이 부족해서 한 쓰레드가 보유한 채로 돈이 입금될 때까지 오랜 시간을 보낸다면, 다른 쓰레드들은 모두 해당 객체의 기다려야 합니다. ) 
이런 상황을 개선하기 위해 고안된 것이 바로 wait()notify()입니다. 

 

wait() 

  • Object 클래스에 정의되어 있으며, 동기화블록 내에서만 사용할 수 있습니다. 
  • 호출하면 실행 중이던 쓰레드는 해당 객체의 락을 반납하고, 해당 객체의 waiting pool에서 통지를 기다립니다.
  • 매개변수가 있는 wait()은 매개변수로 지정된 시간동안만 기다립니다. 

notify()

  • Object 클래스에 정의되어 있으며, 동기화블록 내에서만 사용할 수 있습니다. 
  • 해당 객체의 waiting pool에 있던 모든 쓰레드 중에서 임의의 쓰레드에게 통지를 하는 메서드입니다.
    ( 대기했던 순서와 상관없이 작업을 중단했던 쓰레드 중 하나가 다시 을 얻어 작업을 진행하게 합니다. )

notifyAll() 

  • Object 클래스에 정의되어 있으며, 동기화블록 내에서만 사용할 수 있습니다. 
  • 해당 객체의 waiting pool에 기다리고 있는 모든 쓰레드에게 통지를 하는 메서드입니다.
    ( 모든 쓰레드가 통지를 받지만 결국 lock하나의 쓰레드만 얻을 수 있습니다. )
 waiting pool객체마다 존재하는 것이므로 notifyAll()이 호출된다고 해서 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는 것은 아닙니다. 

 

기아 현상( starvation )과 경쟁 상태( race condition )

  • notify()임의의 쓰레드를 깨우기 때문에 어떤 쓰레드는 매우 오랫동안 기다리게 될 수도 있습니다.
    이처럼 쓰레드계속 대기 상태에 머무는 것을 '기아 현상( starvation )'이라고 합니다. 
  • notifyAll()을 사용하면 모든 쓰레드에게 통지를 하여 기아 현상을 해결할 수 있습니다.
    하지만 모든 쓰레드가 하나의 lock을 얻기 위해 경쟁하게 되며, 이처럼 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것을 '경쟁 상태( race condition )'라고 합니다. 
  • 경쟁 상태개선하기 위해서는 임의의 쓰레드에게 통지하는 것이 아니라 쓰레드구별해서 통지하는 선별적인 통지가 필요합니다. 

3. Lock & Condition을 이용한 선별적인 동기화

lock 클래스의 종류

ReentrantLock 재진입이 가능한 lock, 가장 일반적인 배타 lock
ReentrantReadWriteLock 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가한 lock

ReentrantLock

  • 가장 일반적인 lock입니다.
  • 'reentrant(재진입할 수 있는)'이 붙은 것처럼 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와서 이후의 작업을 수행할 수 있습니다.

ReentrantReadWriteLock

  • 읽기를 위한 lock쓰기를 위한 lock을 제공합니다.
  • 읽기 lock이 걸려있으면, 다른 쓰레드가 읽기 lock중복해서 걸고 읽기를 수행할 수 있습니다.
    ( 읽기는 내용을 변경하지 않으므로 동시에 여러 쓰레드가 읽어도 문제가 되지 않습니다. )
  • 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용되지 않습니다
    ( 반대의 경우도 마찬가지로 쓰기 lock이 걸린 상태에서 읽기 lock을 거는 것은 허용되지 않습니다. )

StampedLock

  • lock을 걸거나 해지할 때 '스탬프( long타입의 정수값 )'를 사용하며, 읽기쓰기를 위한 lock외에 '낙관적 읽기 lock(optimistic reading lock)'이 추가된 lock입니다.
  • 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야하지만 '낙관적 읽기 lock'은 쓰기 lock에 의해 바로 풀린다는 특징이 있습니다.
    따라서 '낙관적 읽기 lock'를 사용할 때는 다른 쓰기 lock에 의해 낙관적 읽기가 실패했는지 확인하고 실패했다면 읽기 lock을 얻어서 다시 읽어 오도록 해야합니다.
    ( 즉, 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때 쓰기가 끝난 후에 읽기 lock을 거는 것입니다. )

StampedLock을 사용한 낙관적 읽기의 예

int getBalance(){
	long stamp = lock.tryOptimisticRead();	// 낙관적 읽기 lock을 겁니다.
    
    int curBalance = this.balance;		// 공유 데이터인 balance를 읽어옵니다.
	
    if(!lock.validate(stamp)){			// 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인합니다.
    	stamp = lock.readLock();		// 만약 lock이 풀린 경우, 읽기 lock을 얻기 위해 기다립니다.
    
    	try{
        	curBalance = this.balance;	// 공유 데이터를 읽어옵니다.
        } finally{
        	lock.unlockRead(stamp);		// 읽기 lock을 풉니다.
        }
    }
}

 

ReentrantLock의 생성자

ReentrantLock()
ReentrantLock(boolean fair)
  • 생성자매개변수true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드lock획득할 수 있게 공정(fair)하게 처리합니다.
  • 공정하게 처리하려면 어떤 쓰레드가 가장 오래 기다렸는지 확인하는 과정을 거칠 수밖에 없으므로 성능은 떨어집니다.
  • lock클래스들은 synchronized 키워드를 사용할 때와 달리 수동으로 lock의 잠금과 해제를 해야합니다.

lock 잠금 & 해제 메서드

void lock()		lock을 잠그는 메서드
void unlock()		lock을 해지하는 메서드
boolean isLocked()	lock이 잠겼는지 확인하는 메서드
  • 임계 영역에 들어가기 전 lock()을 호출하고, 임계 영역을 벗어날 때 unlock() 메서드를 호출하면 됩니다. 
  • 임계 영역 내에서 예외가 발생하거나 return 문으로 빠져나가게 되면 lock이 풀리지 않을 수 있으므로 unlock()은 일반적으로 try - finally문으로 감싸는 방식을 사용합니다.
ReentrantLock lock = new ReentrantLock();
lock.lock();	// lock을 잠그기
try{
	// 임계 영역
}finally{
	lock.unlock();	// lock을 해지하기
}

 

응답성이 중요한 경우 사용하는 tryLock()

boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
  • lock()lock을 얻을 때까지 쓰레드블락(block)시키므로 쓰레드응답성이 나빠질 수 있습니다.
  • tryLock() 메서드는 lock()과 달리, 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않습니다.
    또는 지정된 시간만큼만 기다립니다.
  • lock을 얻으면 true, 얻지 못하면 false를 반환합니다.

ReentrantLock과 Condition

  • Conditionwait() & notify()와 달리 쓰레드의 종류에 따라 구분하여 통지를 할 수 있는 기능입니다.
  • 이미 생성된 lock으로부터 newCondition()을 호출하여 생성할 수 있습니다.
  • 쓰레드를 대기시키거나 깨울 때 메서드로 wait() & notify()대신 await() & signal()을 사용합니다.
  • 경우에 따라 여러 개의 Condition을 생성하여 세분화할 수 있습니다. 
    ex)
    Condition forCook = lock.newCondition();  // Cook을 위한 Condition
    Condition forCust  = lock.newCondition();  // Customer을 위한 Condition
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

// 스레드 대기
lock.lock();
try {
    while (조건이 충족되지 않을 때) {
        condition.await();
    }
    // 조건이 충족되면 작업 수행
} finally {
    lock.unlock();
}

// 스레드가 조건을 충족했을 때 다른 스레드를 깨울 때
lock.lock();
try {
    condition.signal(); // 하나의 스레드를 깨움
    // 또는
    condition.signalAll(); // 모든 스레드를 깨움
} finally {
    lock.unlock();
}

프로세스와 쓰레드

  • 프로세스는 '실행 중인 프로그램(program)' 입니다.
  • 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 됩니다.
  • 프로세스는 '프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 + 쓰레드' 로 구성되어 있습니다.
  • 프로세스의 자원을 이용해 실제로 작업을 수행하는 것이 바로 Thread입니다.
  • 모든 프로세스하나 이상의 쓰레드를 가지며, 둘 이상의 쓰레드를 가진 프로세스를 '멀티쓰레드 프로세스( multi-threaded process)'라고 합니다.

멀티쓰래딩의 장단점

멀티쓰레딩의 장점
1. CPU의 사용률을 향상시킨다.
2. 자원을 보다 효율적으로 사용할 수 있다.
3. 사용자에 대한 응답성이 향상된다.
4. 작업이 분리되어 코드가 간결해진다.

멀티쓰레딩의 단점
1. 동기화( Synchronization ), 교착상태( Deadlock )와 같은 문제들이 발생할 수 있다.

 

쓰레드의 구현과 실행

쓰레드의 구현

  • 쓰레드구현하는 방법은 다음과 같이 두 가지가 있습니다.
    1. Thread 클래스를 상속 받는 방법 
    2. Runnable 인터페이스를 구현하는 방법
  • 쓰레드를 구현할 때 어느 쪽을 선택해도 별 차이가 없지만 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 것이 일반적인 방법입니다.
public class ThreadEx1 {

    public static void main(String[] args) {
        ThreadEx1_1 t1 = new ThreadEx1_1();

        Runnable r = new ThreadEx1_2();
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

class ThreadEx1_1 extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName());
        }
    }
}

class ThreadEx1_2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName());
        }
        
    }
}
  • 위 코드에서 확인할 수 있듯이 Thread 클래스를 상속받은 경우와 Runnable 인터페이스를 구현한 경우의 인스턴스 생성 방법다릅니다.
  • Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성하여 Thread 클래스의 생성자의 매개변수전달해야 합니다.
  • Thread 클래스를 상속받는 경우, 자손 클래스에서 조상인 Thread 클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread 클래스의 static 메서드인 currentThread() 를 호출하여 쓰레드에 대한 참조를 얻어 와야 합니다.
  • 쓰레드이름은 다음과 같은 생성자메서드를 통해 지정 또는 변경할 수 있으며, 쓰레드이름을 지정하지 않으면 ' Thread - 번호 '의 형식으로 이름이 정해집니다.
Thread(Runnable target, String name)
Thread(String name)
void setName(String name)

 

쓰레드의 실행 - start( )

  • 쓰레드를 실행하는 메서드입니다. 
  • start() 메서드를 호출한다고 쓰레드가 바로 실행되는 것이 아니며, OS 스케쥴러에 의해 자기 차례가 된 경우 쓰레드가 실행됩니다.
  • 하나의 쓰레드에 대해 start()한 번만 호출될 수 있습니다. 하나의 쓰레드에 대해 start()두 번 이상 호출하는 경우 IllegalThreadStateException이 발생합니다.

start() 와 run()

run() 

  • main 메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아닌 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐입니다.

main 메서드에서 run()을     호출했을 때의 호출 스택

start() 

  • 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택( call stack )을 생성한 다음 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 합니다.
  • 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드생성하고 실행시킬 때마다 새로운 호출스택생성되고 쓰레드종료되면 작업에 사용된 호출스택은 소멸됩니다.

(1)                                                                                                                 (2)
(3)                                                                                                                    (4)

  • 새로운 쓰레드를 생성하고 start()를 호출한 후 호출스택의 변화
    (1) main 메서드에서 쓰레드의 start()를 호출합니다.
    (2) start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성합니다.
    (3) 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행합니다.
    (4) 이제 호출 스택이 2개이므로 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행됩니다.

main 쓰레드

  • main 메서드의 작업을 수행하는 쓰레드입니다.
  • 프로그램을 실행하면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업이 수행되도록 합니다.

 

싱글쓰레드와 멀티쓰레드

  • 두 개의 작업하나의 쓰레드로 처리하는 경우와 두 개의 쓰레드로 처리하는 경우를 가정해보겠습니다.
  • 하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드로 작업 하는 경우에는 짧은 시간동안 두 개의 쓰레드가 번갈아 가면서 작업을 수행합니다.
  • 두 개의 쓰레드로 작업을 하는 경우 쓰레드 간 작업 전환( context switching )에 시간이 걸리기 때문에, 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 멀티 쓰레드보다 싱글 쓰레드로 프로그래밍 하는 것이 더 효율적일 수 있습니다.
  • 두 개의 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적입니다. ( 예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력을 필요로 하는 경우가 이에 해당합니다. )

하나의 쓰레드로 사용자 입력과 출력 처리하는 코드

class ThreadExA {

    public static void main(String[] args) throws Exception {
        String input = JOptionPane.showInputDialog("사용자 값 입력.");
        System.out.println("입력하신 값은 " + input + "입니다. ");

        for (int i = 10; i > 0; i--) {
            System.out.println(i);
            try {
                Thread.sleep(1000); // 1초마다 숫자를 출력하기 위함
            } catch (Exception e) {

            }
        }
    }
}

출력 결과 - 사용자의 입력을 마치기 전까지는 화면에 숫자가 출력되지 않습니다.

두 개의 쓰레드로 사용자 입력과 출력 처리하는 코드

class ThreadB{
    public static void main(String[] args) {
        CustomThread thread = new CustomThread();
        thread.start();

        String input = JOptionPane.showInputDialog("사용자 값 입력.");
        System.out.println("입력하신 값은 " + input + "입니다.");
    }
	
    // 내부 클래스
    private static class CustomThread extends Thread{
        public void run() {
            for (int i = 10; i > 0; i--) {
                System.out.println(i);

                try {
                    sleep(1000);
                } catch (Exception e) {

                }
            }
        }
    }
    
}

출력 결과 - 사용자의 입력과 숫자 출력을 두 개의 쓰레드로 나누어 처리했기 때문에, 사용자가 입력을 마치지 않아도 화면에 숫자가 출력됩니다.

 

데몬 쓰레드 ( daemon thread )

  • 데몬 쓰레드는 다른 일반 쓰레드( 데몬 쓰레드가 아닌 쓰레드 )의 작업을 돕는 보조적인 역할을 하는 쓰레드입니다.
  • 일반 쓰레드가 모두 종료되면 데몬 쓰레드강제적으로 자동 종료됩니다.
  • 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신 등이 데몬 쓰레드에 해당합니다.
  • 데몬 쓰레드무한루프조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성합니다.
  • 데몬 쓰레드일반 쓰레드작성방법과 실행방법이 같으며 쓰레드를 생성한 후 실행하기 전에 setDaemon(true)를 호출하여야 합니다.
  • 데몬 쓰레드생성한 쓰레드는 자동적으로 데몬 쓰레드가 됩니다.
boolean isDaemon() : 쓰레드가 데몬 쓰레드인지 확인하는 메서드. 데몬 쓰레드이면 true를 반환합니다.

void setDaemon(boolean on) : 쓰레드를 데몬 쓰레드 또는 사용자 쓰레드로 변경하는 메서드입니다.

 

데몬 쓰레드 설정 코드

class ThreadExDaemon implements Runnable{

    static boolean autoSave = false;

    public static void main(String[] args) {
        Thread thread = new Thread(new ThreadExDaemon());
        thread.setDaemon(true);
        thread.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 {
            	// 3초마다 확인
                Thread.sleep(3 * 1000);
            } catch (InterruptedException e) {}
			    
            if (autoSave) {
                autoSave();
            }
        }
    }
    
    public void autoSave() {
        System.out.println("작업파일이 자동저장되었습니다.");
    }
}
  •  3초마다 autoSave의 값을 확인해서 그 값이 true이면, autoSave()를 호출하는 일을 무한히 반복하는 데몬 쓰레드를 생성하였습니다.
  • 쓰레드데몬 쓰레드로 설정하지 않았다면, 무한루프로 인해 프로그램 강제종료를 하지 않는 한 종료되지 않을 것입니다.
  • setDaemon 메서드는 반드시 start()호출하기 전에 실행되어야 합니다.
    ( 그렇지 않으면IllegalThreadStateException이 발생합니다. )

지네릭스란?

  • Generics는 다양한 타입의 객체들을 다루는 메서드컬렉션 클래스컴파일 시 타입 체크 ( Complie - time type check )를 해주는 기능입니다.
  • 컴파일 시 객체의 타입을 체크하기 때문에 객체의 타입 안정성을 줄이고, 형변환의 번거로움을 줄여줍니다.
Generics 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로, 코드가 간결해 진다.

 

지네릭 클래스의 선언

Class Box<T> {			// 지네릭 타입 T를 선언
	
    T item;
    
    void setItem(T item){ 
    	this.item = item;
    }
    
    T getItem(){
    	return this.item;
    }
}
  • T를 '타입 변수(type variable)'라고 하며, 'Type'의 첫 글자에서 따온 것이라고 합니다.
  • 타입 변수는 '임의의 참조형 타입'을 의미합니다. 
  • 타입 변수T가 아닌 다른 것을 사용해도 되며, 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋습니다.
    1. ArrayList<E>의 경우, 타입 변수 E는 'Element(요소)' 의 첫 글자를 따서 사용했습니다.
    2. Map<K, V>의 경우, 타입 변수 K는 'Key(키)'를 의미하고, 타입 변수 V는 'Value(값)'를 의미합니다.

지네릭 클래스 객체 생성하기

Box<String> box = new Box<String>();	// 실제 타입을 지정
b.setItem(new Object());		// 에러. String이외의 타입 지정 불가 ( 타입 안정성 )
b.setItem("ItemA");			// 정상적인 코드 : String 타입 입력
String item = b.getItem();		// 형변환이 필요 없음 ( 코드가 간결해짐 )
  • 지네릭 클래스타입 변수String 타입으로 지정했기 때문에 , String 타입이 아닌 경우 타입에러가 발생합니다.

지네릭스의 용어

Class Box<T> {}

Box<T>	: 지네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽습니다.
T	: 타입 변수 또는 타입 매개변수. ( T는 타입 문자 )
Box	: 원시 타입( raw type )

Box<String> box = new Box<String>();

Box<String>	: 타입 매개변수에 타입을 지정하는 것으로 '지네릭 타입 호출'이라고 합니다.
String		: 매개변수화된 타입( parameterized type )
  • 컴파일 이후에 Box<String>는 지네릭 타입이 제거되워 원시 타입인 Box로 바뀝니다. ( 아래의 지네릭 타입의 제거 부분 )

지네릭스의 제한

  • T인스턴스 변수로 간주되기 때문에 모든 객체에 대해 동일하게 동작해야하는 static 멤버타입 변수 T사용할 수 없습니다
  • 지네릭 타입의 배열을 생성하는 것은 허용되지 않습니다.
Class Box<T>{

	T[] itemArr;	// 지네릭 배열 타입의 참조변수 선언은 가능합니다.
    
    	T[] toArray(){
    	T[] tmpArr = new T[itemArr.leghth];	// 에러. 지네릭 배열 생성 불가
    	...
        return tmpArr;
    	}

}
지네릭 배열을 생성할 수 없는 이유는 new 연산자 때문입니다. new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 하지만, 위 코드에 정의된 Box<T> 클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없습니다.

 

제한된 지네릭 클래스

  • 지네릭 클래스에 저장할 수 있는 타입의 종류 제한할 수 있습니다.
  • 'extends' 키워드를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있습니다.
  • 인터페이스구현해야 한다는 제약이 필요할 때도 extends 키워드를 사용합니다.
  • 자손인터페이스 제약을 둘 다 주기 위해서는 '&' 기호를 사용합니다.
class FruitBox<T extends Fruit> {	// Fruit 클래스의 자손만 타입으로 지정 가능
	...
}
interface Eatable{}

class FruitBox<T extends Eatable>{...}	// 인터페이스를 구현해야 한다는 제약 추가

 

class FruitBox<T extends Fruit & Eatable>// T는 Fruit의 자손이면서 Eatable 인터페이스를 구현한 클래스
{...}

 

와일드 카드

static Juice makeJuice(FruitBox<Fruit> box){
...
}

static Juice makeJuice(FruitBox<Apple> box){	// 지네릭 타입만 다른 메서드 중복 정의
...
}
  • 메서드매개변수지네릭 타입만이 다른 것만으로는 오버로딩이 성립되지 않습니다.
    ( 이 경우는 '메서드 중복 정의' 로 에러가 발생합니다. 
  • 위 경우에 사용하는 것이 '와일드 카드' 로 기호 '?' 로 표현하며 와일드카드는 어떠한 타입도 될 수 있습니다.
  • '?' 만으로는 Object 타입과 다를 것이 없으므로, 'extends'로 상한을, 'super'로 하한을 제한할 수 있습니다.
< ? extends T >	와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? super T >	와일드 카드의 하한 제한. T와 그 조상들만 가능
< ? >		제한 없음. 모든 타입이 가능합니다. < ? extends Object >와 같습니다.

 

지네릭 메서드

  • 메서드선언부지네릭 타입이 선언된 메서드를 지네릭 메서드라고 합니다. ( 지네릭 타입의 선언 위치반환 타입의 바로 앞입니다. )
  • 지네릭 클래스에 정의된 타입 매개변수지네릭 메서드에 정의된 타입 매개변수는 전혀 다른 별개의 것이므로 같은 참조 문자 T를 사용해도 같은 것이 아니라는 것에 주의해야 합니다. 
  • 지네릭 메서드지네릭 클래스가 아닌 클래스에도 정의될 수 있습니다.

지네릭 메서드 예시

class FruitBox<T>{
	...
	static <T> void sort ( List<T> list, comparator<? super T> c){
    	...
    }
    ...
}
  • 위 코드에서 지네릭 클래스 FruitBox에 선언된 타입 매개변수 T와 지네릭 메서드 sort()에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것입니다.
  • static 멤버에는 타입 매개변수를 사용할 수 없지만, 이처럼 메서드지네릭 타입을 선언하여 사용하는 것은 가능합니다.

 

복잡하게 선언된 지네릭 메서드

 

public static <T extends Comparable< ? super T>> void sort ( List<T> list)
  • Collections 클래스의 sort() 메서드입니다. 
  • 이해하기
    1. 타입 T를 요소로 하는 List 매개변수로 허용합니다. 
    2. (<T extends Comparable>) : 'T'는 Comparable구현한 클래스여야 합니다.
    3. (Comparable<? super T>) : 'T' 또는 그 조상의 타입을 비교하는 Comparable이어야 한다는 것을 의미합니다.( 만약 TStudent이고, Person자손이라면? <? super T>Student, Person, Object가 가능합니다. )

지네릭 타입의 제거

  •  컴파일러지네릭 타입을 이용해서 소스파일체크하고, 필요한 곳에 형변환을 넣어줍니다. 그리고 지네릭 타입제거합니다. 즉, 컴파일된 파일(*.class)에 지네릭 타입에 대한 정보가 남지 않습니다. 

지네릭 타입 제거 과정

 

1. 지네릭 타입의 경계( bound )를 제거합니다.

  • 지네릭 타입이 < T extends Fruit >라면 T는 Fruit로 치환됩니다. <T>인 경우는 T는 Object로 치환되며, 클래스 옆의 선언은 제거됩니다. 
Class Box <T extends Fruit> {
	void add (T t){
    	...
    }
}

-- 1. 지네릭 타입의 경계 (bound)를 제거한다. --
Class Box{
	void add (Fruit t){
    	...
    }
}

2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가합니다.

  • List의 get()은 Object 타입을 반환하므로 형변환이 필요합니다.
T get(int i){
	return list.get(i);
}


-- 2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다. -- 
Fruit get(int i ){
	return (Fruit)list.get(i);
}

내부 클래스란?

  • 내부 클래스는 클래스 내에 선언된 클래스입니다.
  • 내부 클래스를 선언한 클래스는 외부 클래스가 됩니다.
  • 두 클래스가 서로 긴밀한 관계에 있는 경우 사용하며, 다음 장점들을 가지고 있습니다. 
내부 클래스의 장점
-
내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
코드의 복잡성을 줄일 수 있다( 캡슐화 ).
class A {		// 외부 클래스
	...
    class B {	// 내부 클래스
    	...
    }
	...
}

내부 클래스의 종류와 특징

  • 내부 클래스의 종류는 변수선언위치에 따른 종류와 같습니다.
  • 인스턴스 클래스, 스태틱 클래스, 지역 클래스, 익명 클래스로 분류됩니다.
내부 클래스 특 징
인스턴스 클래스
(Instance class
외부 클래스멤버변수 선언위치에 선언하며, 외부 클래스인스턴스 멤버처럼 다루어집니다.
주로 외부 클래스인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언합니다.
스태틱 클래스
( Static class )
외부 클래스멤버변수 선언위치에 선언하며, 외부 클래스static 멤버처럼 다루어집니다.
주로 외부 클래스static 멤버, 특히 static메서드에서 사용될 목적으로 선언합니다.
지역 클래스
( Inner class
외부 클래스메서드초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있습니다.
익명 클래스
anonymous class )
클래스의 선언객체의 생성 동시에 하는 이름없는 클래스입니다. ( 일회용 )

내부 클래스의 선언

  • 내부 클래스의 선언위치는 변수의 선언위치와 동일합니다.
  • 내부 클래스의 선언위치에 따라 같은 선언위치의 변수와 동일한 유효범위( Scope )와 접근성( Accessibility )을 가집니다.
class Outer {
	class InstanceInner{}		// 인스턴스 클래스
    static class StaticInner{}	// 스태틱 클래스
	
    void mymethod(){
    	class LocalInner{}		// 지역 클래스
    }
    
}

내부 클래스의 제어자와 접근성

  • 내부 클래스멤버변수동일한 성질을 갖습니다. 따라서, 내부 클래스외부 클래스의 멤버와 같이 간주되고, 인스턴스 멤버와 static 멤버 간의 규칙이 내부 클래스에도 똑같이 적용됩니다.  
  • 내부 클래스도 클래스이기 때문에 abstractfinal와 같은 제어자를 사용할 수 있고, 멤버변수들처럼 private, protected 접근제어자도 사용할 수 있습니다.
class InnerEx{
	class InstanceInner{}		// 인스턴스 클래스
    static class StaticInner{} 		// 스태틱 클래스
    
    // 인스턴스멤버 간에는 서로 직접 접근이 가능합니다.
    InstanceInner iv = new InstanceInner();
    // static 멤버 간에는 서로 직접 접근이 가능합니다.
    static StaticInner cv = new StaticInner();
    
    static void staticMethod(){
    	// static멤버는 인스턴스멤버에 직접 접근할 수 없습니다.
//        InstanceInner obj1 = new InstanceInner();
  		StaticInner obj2 = new StaticInner();
        
        // 굳이 접근하려면 아래와 같이 외부 클래스 객체를 생성해야 합니다.
        // 인스턴스 클래스는 외부 클래스를 먼저 생성해야만 사용할 수 있습니다.
        InnerEx outer = new InnerEx();
        InstanceInner obj1 = outer.new InstanceInner();
    }
	
    void instanceMethod(){
    	// 인스턴스메서드에서는 인스턴스멤버와 static멤버 모두 접근이 가능합니다.
        InstanceInner obj1 = new InstanceInner();
  		StaticInner obj2 = new StaticInner();
        // 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없습니다.
//        LocalInner lv = new LocalInner();
    
    }
	
    void myMethod(){
    	class LocalInner{}
        LocalInner lv = new LocalInner();
    }
}

익명 클래스 ( anonymous class )

  • 클래스이름없으며, 클래스선언객체생성동시에 하기 때문에 단 한번만 사용될 수 있습니다.
  • 오직 하나의 객체만을 생성할 수 있는 일회용 클래스입니다.
  • 오직, 단 하나의 클래스상속받거나 단 하나인터페이스만을 구현할 수 있습니다.
    • 이름없기 때문에 생성자를 가질 수 없으며, 조상클래스의 이름이나 구현하려는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스상속받으며 동시에 인터페이스구현하거나 둘 이상의 인터페이스구현할 수 없습니다.
new 조상클래스이름(){
	// 멤버 선언
}
	또는
    
new 구현인터페이스이름(){
	// 멤버 선언
}

 익명 클래스 사용 예 1

class InnerEx{
	Object iv = new Object(){ void method(){} };	// 익명 클래스
    static Object cv = new Object(){ void method(){} }; // 익명 클래스
    
    void myMethod(){
    	Object lv = new object(){ void method(){} };	// 익명 클래스
    }

}
  • 익명 클래스컴파일하면 다음과 같은 클래스 파일이 생성됩니다.
  • 익명 클래스는 이름이 없기 때문에 '외부 클래스명$숫자.class'의 형식으로 클래스파일명이 결정됩니다.
InnerEx6.class
InnerEx6$1.class // 익명 클래스
InnerEx6$2.class // 익명 클래스
InnerEx6$3.class // 익명 클래스

+ Recent posts