개발/Java Study

10주차 : 멀티쓰레드 프로그래밍

박비버 2021. 2. 20. 21:21

1. Thread 클래스와 Runnable 인터페이스

  • Thread 클래스 상속받아서 구현하면 다른 클래스를 상속받을 수 없어서 Runnable을 사용하는 것이 일반적
  • Runnable 을 구현한 클래스의 인스턴스를 생성한 후 생성자 Thread(Runnable target)를 통해서 Runnable 인터페이스를 구현한 인스턴스를 참조하도록 되어있음
  • start() 호출 해야마 쓰레드가 실행됨
class MyThread extends Thread {
    public void run() {}
}
class MyThread implements Runnable {
    public void run() {}
}
class ThreadEx1_1 extends Thread {
    public void run() {
        for(int i=0; i<5; i++){
            System.out.println(getName()); // Thread에 getName 들어있음
        }
    }
}

class ThreadEx1_2 implements Runnable {
    public void run() {
        for(int i=0; i<5; i++){
            System.out.println(Thread.currentThread().getName()); 
            // Thread.currentThread() 현재 실행중인 Thread의 참조를 반환
        }
    }
}

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();
    }
}


2. 쓰레드의 상태

상태 설명
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)
지정된 시간동안 실행함
void interrupt() sleep, join에 의해 일시 정지인 쓰레드를 실행 대기로 전환
void stop() 쓰레드 즉시 종료
void suspend() 쓰레드 일시정지
void resume() suspend에 의해 일시정지 상태의 쓰레드를 실행대기로 전환
static void yield() 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 실행 대기로 전환

 

 

3. 쓰레드의 우선순위

  • 우선순위(priority)라는 속성(멤버변수)를 가지고 있음
  • 숫자가 높을수록 우선순위가 높음
  • main 메서드의 우선순위는 5. main메서드에서 생성된 쓰레드의 우선순위는 자동적으로 5
  • 쓰레드의 우선순위와 관련된 구현이 JVM마다 다를 수 있음
void setPriority(int newPriority)
int getPriority()


4. Main 쓰레드

  • 프로그램 시작시 가장 먼저 실행되는 쓰레드
  • 멀티 스레드는 메인 쓰레드가 종료 되더라도 실행중인 다른 쓰레드가 있다면 프로세스가 종료 되지 않음
public static void main(String args[]) {

}


5. 동기화 (Synchronization)

  • 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업함
  • 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는것

synchronized를 이용한 동기화

  • synchronized 가 호출된 시점 부터 해당 메서드가 포함된 객체의 lock을 얻어서 작업을 수행하다가 메서드가 종료되면 lock을 반환
  • lock을 걸고자 하는 객체를 참조변수로
  • critical section은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 임계영역을 최소화 해서 효율적으로 돌아가도록 해야함
1. 메서드 전체를 임계 영역(critical section)으로 지정
public synchronized void calcSum() {
    //...
}

2. 특정한 영역을 임계 영역으로 지정
synchronized (객체의 참조변수) {
    //...
}
class ThreadEx22 {
	public static void main(String args[]) {
		Runnable r = new RunnableEx22();
		new Thread(r).start();
		new Thread(r).start();
	}
}

class Account {
	private int balance = 1000; // private으로 해야 동기화가 의미가 있다.

	public  int getBalance() {
		return balance;
	}

	public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화
		if(balance >= money) { // 이 구분을 통과하고 출금하기 직전에 다른 쓰레드가 끼어들면 잔고가 마이너스됨
			try { Thread.sleep(1000);} catch(InterruptedException e) {}
			balance -= money;
		}
	} // withdraw
}

class RunnableEx22 implements Runnable {
	Account acc = new Account();

	public void run() {
		while(acc.getBalance() > 0) {
			// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
			int money = (int)(Math.random() * 3 + 1) * 100;
			acc.withdraw(money);
			System.out.println("balance:"+acc.getBalance());
		}
	} // run()
}
  • 아래와 같이 설정도 가능함
	public void withdraw(int money){
		synchronized(this){
			if(balance >= money) {
				try { Thread.sleep(1000);} catch(InterruptedException e) {}
				balance -= money;
			}
		}
	} // withdraw

 

wait()과 notify()

  • critial section의 코드를 더이상 진행할 수 없으면, wait() 호출하여 쓰레드가 락을 반납하고 기다리게 함
  • 락이 반납되면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 진행 할 수 있음
  • notify() 를 호출해서 작업을 중단했던 쓰레드가 다시 lock을 얻어서 진행 (재진입, reentrance)
  • 오래 기다린 쓰레드가 락을 얻는다는 보장이 없음
  • 운이 나쁘면.. 오래기다려야할 수 있음, 기아현상(starvation) -> notifyAll()을 사용해서 해결 가능
  • race condition : 여러 쓰래드가 lock을 얻기 위해 서로 경쟁하는 것

 

Lock과 Condition을 이용한 동기화

  • ReentrantLock : 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 critial section으로 들어와서 이후의 작업을 수행
  • ReentrantReadWriteLock : 읽기 lock이 걸려 있으면 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행, 읽기 lock이 걸린 상태에서 쓰기lock은 허용되지 않음
  • StampedLock : lock을 걸거나 해지할 때 stamp(long 타입 정수값) 사용, optimistic reading lock이 추가됨 
  • optimistic reading lock : 읽기 lock이 걸려 있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야하는 데에 비해 optimistic reading lock은 쓰기 lock에 의해 바로 풀림
ReentrantLock 재진입이 가능한 Lock 가장 일반적인 배타 lock
ReentrantReadWriteLock 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가

 

volatile

  • 멀티 코어 프로세서에는 코어마다 별도의 캐시를 가지고 있음
  • 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아서 메모리 값과 다른 경우 발생 => 쓰레드가 계속 실행됨
  • 변수 앞에 volatile을 붙이면, 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어옴, 불일치 해결됨
volatile long balance;

synchronized int getBalance() {
    return balance;
}

synchronized void withdraw (int money) {
    if(balance >= money) {
        balance -= money;
    }
}

 

6. 데드락

  • 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태

출처 : 

자바의 정석, 남궁 성 지음