4 minute read

목표

자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.

학습할 것 (필수)

Thread 클래스와 Runnable 인터페이스

쓰레드를 생성하는 방법 두가지

  • Thread 클래스 이용(상속) Thread 클래스로부터 직접 상속받아 쓰레드 특징을 가지는 클래스를 생성하여 쓰레드를 사용하는 방법
class ThreadTest extends Thread {
    ... 
    public void run(){ // Thread 클래스의 run() 메소드 오버라이딩
        ...
    }
}

...

ThreadTest thread = new ThreadTest();
thread.start(); // start() 메소드에 의해 호출 됨
  • Runnable 인터페이스 이용(구현)

쓰레드의 특성을 가져야 하는 클래스가 이미 다른 클래스로부터 상속을 받고 있다면 Runnable 인터페이스를 이용해야 한다.

Runnable 인터페이스에는 run() 메소드 하나만 선언되어 있으며, JDK에 의해 제공되고 있는 라이브러리 인터페이스이다.

public interface Runnable {
    public abstract void run();
}

...

public class TestRunnable implements Runnable {
    ...
	public void run() {
        ...
	}
    ...
}

...

TestRunnable tr = new TestRunnable(); // 객체 생성
Thread t = new Thread(tr); // 쓰레드 객체 생성
t.start(); // 쓰레드 시작

or 

TestRunnable tr = new TestRunnable();
new Thread(tr).start();

Thread 클래스는 java.lang 패키지에 있다.

메소드 이름 설명
static void sleep(long msec) throws InterruptedException msec에 지정된 밀리초(milliseconds) 동안 대기
static void sleep(long msec, int nsec) throws InterruptedException mesc에 지정된 밀리초 +nsec에 지정된 나노초(nanoseconds) 동안 대기
String getName() 쓰레드의 이름을 반환
void setName() 쓰레드의 이름을 s로 설정
void start() 쓰레드 시작시킨다. run() 메소드 호출
final int getPriority() 쓰레드의 우선순위를 반환
final void setPriority(int p) 쓰레드의 우선순위를 p값으로 설정
boolean isAlive() 쓰레드가 실행 가능 상태, 실행상태, 대기상태에 있으면 true를 그렇지 않으면 false를 반환
void join() throws InterruptedException 쓰레드가 끝날 때까지 기다림
void run() 쓰레드가 실행할 부분을 기술하는 메소드. 하위 클래스에서 오버라이딩되어야 한다.
void suspend() 쓰레드가 일시 정지된다. resume()에 의해 다시 시작될 수 있다.
void resume() 일시 정지된 쓰레드를 다시 시작시킨다.

쓰레드의 상태

쓰레드는 탄생하고 소멸될 때까지의 생명주기(life cycle)를 가진다. 쓰레드가 생성되어 종료될 때까지 다섯가지의 상태를 가진다.

쓰레드가 생성되면 그 쓰레드는 메모리를 할당받고 생성 상태에 있게 된다.

생성된 쓰레드는 start() 메소드에 의해 실행 가능 상태로 들어가게 된다. 실행 가능 상태는 실행 상태가 아니며 OS로부터 CPU가 할당되기를 기다리는 상태를 의미한다.

CPU가 할당되면 쓰레드는 run() 메소드가 수행되어 실행 상태가 된다. 실행 상태의 쓰레드는 CPU로부터 할당된 시간이 종료되거나 yield() 메소드가 수행되면 다시 실행 가능 상태로 바뀌게 된다.

쓰레드가 wait(), sleep(), join() 메소드를 수행되면, 쓰레드는 대기상태로 전환되며 수행이 중지된다. 대기상태의 쓰레드가 해체되어 다시 실행 준비 상태로 전환되기 위해서는 join 할 스레드가 종료되거나, sleep time이 종료되거나, notify(), notifyAll() 메소드가 수행되거나, 입출력 등이 종료되는 행위가 발생해야 한다.

public enum State {
    NEW, // 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
    RUNNABLE, // 실행 중 또는 실행 가능한 상태
    BLOCKED, // 동기화 블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)
    WAITING, // 다른 쓰레드가 notify(), notifyAll()을 불러주길 기다리고 있는 상태(동기화)
    TIMED_WAITING, // 쓰레드의 작업이 종료되지는 않았지만, 실행가능하지 않은 일시정지 상태
    TERMINATED; // 쓰레드의 작업이 종료된 상태
}

쓰레드의 우선순위

자바에서는 쓰레드에 우선순위를 부여하여 우선순위가 높은 쓰레드에 실행의 우선권을 주어 실행 시킬 수 있다. Thread.setPriority() 메소드는 우선순위를 부여하는 메소드이다. 쓰레드 우선순위를 지정하기 위한 다음과 같다.

public final static int MIN_PRIORITY = 1; // 우선순위 값으로 1을 가진다. 우선순위 가장 낮음
public final static int NORM_PRIORITY = 5; // 우선순위 값으로 5를 가진다. 
public final static int MAX_PRIORITY = 10; // 우선순위 값으로 10을 가진다. 우선순위 가장 높음

Main 쓰레드

모든 자바 어플리케이션은 메인 쓰레드가 main() 메소드를 실행하면서 시작된다.

public static void main(String args[]){ // 메인 쓰레드 시작
    ... 
} // 메인 쓰레드 끝

동기화

임계영역(critical section): 다수개의 쓰레드가 접근 가능한 영역이면서, 한순간에는 하나의 쓰레드만 사용할 수 있는 영역을 의미한다.

자바에서는 임계영역 지정을 위한 synchronized 메소드를 제공한다. 쓰레드는 임계영역인 synchronized 메소드에 들어가면서 lock을 얻고 이 메소드를 벗어나면서 lock을 양보한다.

자바에서는 쓰레드 사이의 통신을 위한 메소드를 java.lang.Object 클래스에 제공하고 있다. Object 클래스에서 제공되는 메소드 중 wait(), notify(), notifyAll() 메소드가 쓰레드 사이의 통신에 이용된다.

  • lock 클래스 종류
종류 설명
java.util.concurrent.locks.ReentrantLock * 재진입이 가능한 lock
* 가장 일반적인 배타 lock
* lock 인터페이스의 구현 클래스
java.util.concurrent.locks.ReentrantReadWriteLock * 읽기에는 공유적이고, 쓰기에는 배타적인 lock
* lock 인터페이스의 구현 클래스
java.util.concurrent.locks.StampedLock * ReentrantLock에 낙관적인 lock 기능을 추가

데드락

교착상태(Deadlock): 두 개의 lock이 서로 간의 리소스를 대기하는 상태, 어떤 작업도 실행되지 못하고 계속 서로 상대방의 작업이 끝나기만 바라는 무한정 대기 상태

public class DeadlockSample {
    public static Object lock1 = new Object();
    public static Object lock2 = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(){
            public void run(){
                synchronized (lock1){
                    System.out.println("Thread 1: Holding lock 1...");
                    try { Thread.sleep(10); }
                    catch (InterruptedException e) {}
                    System.out.println("Thread 1: Waiting for lock 2...");
                    synchronized (lock2){
                        System.out.println("Thread 1: Holding lock 1 & 2...");
                    }
                }
            }
        };

        Thread thread2 = new Thread(){
            public void run(){
                synchronized (lock2){
                    System.out.println("Thread 2: Holding lock 2...");
                    try { Thread.sleep(10); }
                    catch (InterruptedException e) {}
                    System.out.println("Thread 2: Waiting for lock 1...");
                    synchronized (lock1) {
                        System.out.println("Thread 2: Holding lock 1 & 2...");
                    }
                }
            }
        };

        thread1.start();
        thread2.start();
   }
}
Thread 1: Holding lock 1...
Thread 2: Holding lock 2...
Thread 1: Waiting for lock 2...
Thread 2: Waiting for lock 1...

thread1이 lock2를 기다리고, thread2가 lock1을 기다리고 있어, 메인 쓰레드가 종료되지 않는다.


참고 서적

알기 쉽게 해설한 Java


live-study with whiteship