# Table of Contents

# Thread

스레드(Thread)를 사용하면 동시성 처리를 할 수 있다.

# Thread 사용법

Java에서 두 가지 방법으로 Thraed를 생성할 수 있다.

# Runnable

첫 번째 방법은 Runnable인터페이스를 구현하는 것이다.

class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("name: " + Thread.currentThread().getName());
    }
}
public class Main {

    public static void main(String[] args) {

        Thread thread = new Thread(new MyRunnable());

        System.out.println("name: " + Thread.currentThread().getName());
        thread.start();
    }
}

다음과 같이 단축할 수도 있다.

public class Main {

    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("name: " + Thread.currentThread().getName());
            }
        });
        
        thread.start();
    }
}

# Thread

두 번째 방법은 Thread클래스를 상속하는 것이다.

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("name: " + Thread.currentThread().getName());
    }
}
public class Main {

    public static void main(String[] args) {

        Thread thread = new MyThread();
        
        System.out.println("name: " + Thread.currentThread().getName());
        thread.start();
    }
}

# Thread의 상태

스레드는 크게 네 가지 상태를 가지고 있다.

# Runnable

스레드를 생성하고 start()를 호출하면 Runnable상태가 된다. 주목할 점은 Runnable상태는 아직 스레드가 실행 중인 상태가 아니다.

# Running

Running은 스레드가 실행 중인 상태다. RunnableRunning상태를 구분한 이유는 여러 스레드가 존재할 수 있기 때문이다. Thread클래스의 start()를 호출하면 Thread가 순서대로 Queue에 들어가며, 여기에서 Runnable상태로 대기하고 있다. 그리고 가장 먼저 들어온 Thread가 Running상태로 실행된다.

# Blocked

스레드가 일시중지 상태로 들어간다.

# Terminated

스레드가 종료되어 메모리에서 사라진다.

# 스레드 실행 제어

멀티 스레드 환경에서는 다양한 함수로 실행을 제어할 수 있다.

  • sleep()
  • join()
  • suspend()
  • resume()
  • yield()

# sleep()

sleep()은 현재 스레드를 일정 시간동안 일시중지시킨다. sleep()을 호출하기 전 상태가 다음과 같다면 sleep()을 호출하면 다음 상태가 된다. 일정 시간이 지나면 다음 상태가 된다. 예제를 살펴보자.

public class Main {

    public static void main(String[] args) {
        System.out.println("Before Sleep");

        try {
            // 현재 스레드를 5초 동안 일시중지
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("After Sleep");
        }
    }
}

결과는 다음과 같다.

Before Sleep    // 앱 실행 시 바로 출력
After Sleep     // 5초 뒤 출력

# join()

join()은 다른 스레드가 종료될 때까지 기다린다.

join()을 사용하지 않는 예제를 살펴보자. WorkerThread는 5초 뒤에 로그를 출력한다

class WorkerThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("run() from WorkerThread");
        }
    }
}

Main클래스는 다음과 같다.

public class Main {

    public static void main(String[] args) {

        WorkerThread workerThread = new WorkerThread();
        workerThread.start();

        System.out.println("run() from MainThread");
    }
}

결과는 다음과 같다.

run() from MainThread       // Main Thread에서 바로 출력
run() from WorkerThread     // Worker Thread에서 5초 뒤 출력

이제 join()을 추가해보자.

public class Main {

    public static void main(String[] args) {

        WorkerThread workerThread = new WorkerThread();
        workerThread.start();

        try {
            // 다음 코드를 실행하지 않고 WorkerThread가 종료될 때 까지 기다린다.
            workerThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("run() from MainThread");
    }
}

결과는 다음과 같다.

run() from WorkerThread     // 5초 뒤 출력
run() from MainThread

# suspend()

suspend()는 스레드를 일시정지시킨다. suspend()을 호출하기 전 상태가 다음과 같다면 suspend()을 호출하면 다음 상태가 된다.

# resume()

resume()는 스레드를 재개한다. resume()을 호출하기 전 상태가 다음과 같다면 resume()을 호출하면 다음 상태가 된다.

# stop()

stop()는 스레드를 종료시킨다. stop()을 호출하기 전 상태가 다음과 같다면 stop()을 호출하면 다음 상태가 된다.

# 예제

예제를 살펴보자. WorkerThread는 1초에 한번씩 로그를 출력한다.

class WorkerThread extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("run() from WorkerThread");
            }
        }
    }
}
public class Main {

    public static void main(String[] args) {

        WorkerThread workerThread = new WorkerThread();
        workerThread.start();

        try {
            // 현재 스레드를 5초 일시정지
            Thread.sleep(5 * 1000);

            // WorkerThread를 일시정지
            workerThread.suspend();

            // 현재 스레드를 5초 일시정지
            Thread.sleep(5 * 1000);

            // WorkerThread를 재개
            workerThread.resume();

            // 현재 스레드를 5초 일시정지
            Thread.sleep(5 * 1000);

            // WorkerThread를 종료
            workerThread.stop();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

결과는 다음과 같다.

// 5초 뒤부터 1초 마다 출력
run() from WorkerThread
run() from WorkerThread
run() from WorkerThread
run() from WorkerThread

// 5초 동안 일시정지

// 다시 1초 마다 출력
run() from WorkerThread
run() from WorkerThread
run() from WorkerThread
run() from WorkerThread
run() from WorkerThread
run() from WorkerThread

// 종료

# yield()

yield()는 현재 실행 중인 스레드를 중지하거나 종료하진 않는다. 다만 대기중인 스레드가 먼저 실행되도록 양보한다. yield() 호출 전 상태가 다음과 같다면 yield() 호출 후 상태는 다음과 같아진다.

# 스레드 동기화

멀티 스레드 환경에서는 여러 스레드가 한 데이터을 동시에 조작할 수 있다. 이 때문에 결과를 예측할 수 없게 될 수도 있다. 이 때문에 하나의 스레드가 데이터를 조작할 때 다른 스레드가 데이터를 조작할 수 없도록 공유데이터에 락(Lock)을 걸 수 있다. 이를 동기화(Synchronization)이라고 한다.

예제를 살펴보자. 잔고 데이터를 가지고 있는 계좌인 Account클래스는 다음과 같다.

class Account {

    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

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

WorkerThread클래스는 다음과 같다. 이 스레드는 잔고가 0원 이상일 때만 200원 인출을 요청한다.

class WorkerThread extends Thread {
    private Account account;

    WorkerThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        while (account.getBalance() > 0) {
            account.withdraw(200);
            System.out.println("Balance: " + account.getBalance());
        }
    }
}

Main클래스는 다음과 같다.

public class Main {

    public static void main(String[] args) {
        Account account = new Account();

        WorkerThread t1 = new WorkerThread(account);
        WorkerThread t2 = new WorkerThread(account);

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

애플리케이션을 실행하면 다음과 같이 출력된다.

Balance: 600
Balance: 600
Balance: 400
Balance: 200
Balance: -200
Balance: -200

논리적으로는 출금(withdraw())을 호출할 때 출금액이 잔액보다 크면 잔액에서 출금을 마이너스하면 안된다.

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

그러나 두 스레드가 공유 데이터에 동시에 접근하기 때문에 잔액이 마이너스 값으로 출력된다. 따라서 동기화가 필요하다.

동기화는 두 가지 방법으로 할 수 있다.

# synchronized method

공유 데이터에 접근하는 메소드에 synchronized 키워드를 붙이면 된다.

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

특정 스레드가 synchronized메소드를 실행할 때 메소드 안에서 접근하는 데이터에 락(Lock)을 건다. 락이 걸린 상태에서 다른 스레드가 이 메소드를 호출하면 락이 풀릴 때까지 기다리게 된다.

# synchronized data

공유 데이터 자체에 synchronized 키워드를 붙일 수도 있다.

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

# 데드락

동기화 과정에서 가장 조심해야할 부분이 데드락이다. 데드락(Deadlock)은 두 스레드가 서로 상대방이 락을 풀 때까지 기다리고 있기 때문에 결과적으로 아무것도 하지 못하는 상태를 가리킨다.

이때 Object 클래스에 정의된 wait(), notify(), notifyAll()을 사용하면 동기화의 효율을 높일 수 있다.

# wait()

한 스레드가 자원에 락을 걸고 오래 기다리는 대신 다른 스레드에게 제어권을 넘겨주고 대기 상태로 들어간다.

# notify()

다른 스레드는 작업을 하다가 notify()를 호출하며, 대기 상태에 들어갔던 스레드가 다시 실행 상태가 된다.

Account클래스 예제에 wait()notify()를 적용하면 다음과 같다.

class Account {

    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

    // 인출
    public synchronized void withdraw(int money) {
        while (balance < money) {
            try {
                Thread.sleep(1000);
                wait()
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        balance -= money;
    }

    // 예금
    public synchronized void deposit(int money) {
        balance += money;
        notify();
    }
}