Java의 정석 - Thread

원태연·2022년 5월 30일
0

Java의 정석

목록 보기
15/19
post-thumbnail

Thread

현재 실행중인 프로그램을 의미하는 프로세스 수행하는데 필요한 데이터와 메모리, CPU등의 자원 그리고 쓰레드로 구성되어 있다.

이때, 쓰레드는 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이다. 모든 프로세스는 하나 이상의 쓰레드가 존재하고, 둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스라고 한다.

실제로는 CPU의 코어는 한 번에 하나의 작업만 수행할 수 있지만, CPU가 매우 빠르게 여러 작업을 번갈아 수행함으로써 여러 작업을 하는 것처럼 보이는 것이다. 그래서 멀티쓰레드가 동시 작업을 한다는 장점이 있지만, 코어가 이 작업, 저 작업을 왔다갔다하는 시간(context switching)이 필요하기에 싱글 쓰레드 보다 더 낮은 성능을 보일 수 있다.

멀티 쓰레드

  • 멀티쓰레드의 장점
    • CPU의 사용률 향상
    • 자원의 효율적인 사용
    • 사용자에 대한 응답성 향상
    • 작업 분리를 통한 코드이 간결성
  • 멀티쓰레드의 단점
    • 동기화(synchronization) 이슈
    • 교착상태(deadlock) 이슈

멀티 쓰레드의 구현

  1. Thread클래스 상속
  2. Runnable인터페이스 구현 (권장)
class Thread_1 extends Thread{
    @Override
    public void run(){
        for(int i = 0; i < 100; i++){
            System.out.println("Thread_1");
        }
    }
}

class Thread_2 implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("Thread_2");
        }
    }
}

public class playGround{
    public static void play() {
      
      Thread_1 thread_1 = new Thread_1();
      

        Runnable thread_2 = new Thread_2();
      	Thread t2 = new Thread(thread_2);
      //Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 후,
      //Thread클래스의 인스턴스를 생성할 때 생성자의 매개변수로 제공해야 한다.
      
      //Thread thread_2 = new Thread(new Thread_2());

        thread_1.start();
        t2.start();
    }
}

위 코드를 실행하면 Thread_1Thread_2가 순차적이 아니라 동시적으로 작동한다는 것을 알 수 있다.

이때, run()메소드를 직접 실행하지 않고 start()를 사용한다는 점을 주목하자.

start()메소드는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(공간)을 생성한 다음 run()을 호출하기 때문에, start()를 통해 실행하도록 하자.

만약, 해당 쓰레드를 두 번이상 호출하고자 한다면, IllegalThreadStateException이 발생할 것이다.

thread_1.start();
//thread_1.start(); ERROR
//IllegalThreadStateException 발생
thread_1 = new Thread_1();
thread_1.start();

이러한 상황이 필요하다면, 새로운 쓰레드를 다시 생성해서 start()해야한다.

쓰레드의 우선순위

.setPriority(int priority);

위 메소드를 통해 쓰레드들의 우선순위를 책정할 수 있다.

priority는 1~10까지 지원하며 상대적인 값이고, 기본값은 5이다. 하지만 우선순위를 보장받을 순 없다. 상대적으로 먼저 실행될 확률이 높을 뿐, 무조건 반영되지 않는다.

데몬쓰레드

일반 쓰레드의 작업을 돕는 보조적인 역할의 쓰레드이다.

일반 쓰레드가 종료되면 강제적으로 종료된다는 특징이 있으며, 가비지 컬렉터, 자동저장, 화면 자동갱신등의 기능을 수행한다.

데몬 쓰레드는 일반 쓰레드가 종료되면 강제적으로 종료된다는 특성 때문에, 무한 루프를 통해 실행하고, 특정 조건식에서 작업을 수행하도록 구성하면 좋다.

.setDaemon(boolean on);

데몬 쓰레드는 위와 같이 선언 할 수 있는데, 데몬 쓰레드가 보조하는 일반 쓰레드가 실행되기 전에 실행되어야 한다.

class Thread_2 implements Runnable {

    public static void plays(){
        Thread thread = new Thread(new Thread_2());
        thread.setDaemon(true); //데몬 쓰레드 설정
        thread.start();    			//쓰레드 실행

        for (int i = 1; i <= 20; i++){
            try{
                Thread.sleep(1000);
            } catch (InterruptedException e){}
            System.out.println(i);
        }

    }

    @Override
    public void run() {
        while(true) {
            try{
                Thread.sleep(5 * 1000);

            }catch(InterruptedException e){}
            System.out.println("5 second passed");
          //5초마다 실행
        }
    }
}

public class playGround{
    public static void play() {
        Thread_2.plays();
    }
}
/**
1
2
3
4
5 second passed
5
6
7
*/

쓰레드의 상태

  • NEW : 쓰레드가 생성되고 start()가 호출되지 않은 상태

  • RUNNABLE : 실행 중 또는 실행 가능한 상태
    start()를 통해 RUNNABLE상태

  • BLOCKED : 동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)

  • WAITING, TIMED_WAITING : 쓰레드가 종료되지 않고 일시정지된 상태

  • TERMINATED : 쓰레드의 작업이 종료된 상태
    stop()을 통해 소멸(terminated)상태

쓰레드의 실행 제어

쓰레드의 실행을 제어할 수 있는 메서드들을 통해 효율적인 멀티쓰레드 프로그램을 작성하자.

  • static void sleep(long millis, int nanos?): 지정된 시간동안 쓰레드를 재운다. 시간이 지나면 자동으로 RUNNABLE 상태가 됨

  • void join(long millis?, int nanos?) : 지정된 시간동안 쓰레드가 실행된다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아온다.

  • void interrupt(): 깨우기. 일시정지 상태인 쓰레드를 깨워서 실행대기상태로 만듦

  • static void yield() : 실행 중 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 됨

depricate

void stop() : 쓰레드 즉시 종료

void suspend() : 쓰레드 일시정지. resume()을 통해 실행대기 상태로 변환

void resume(): suspend()에 의해 일시정지 상태에 있는 쓰레드를 실행대기 상태로 만듦

static이 붙은 메서드는 쓰레드 자기 자신에게만 호출이 가능하다

Sleep()

static void sleep(long millis, int nanos?)

sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 다른 메서드에 의해 호출되면, (InterruptedException)이 발생한다. --> 필수 예외처리

try{
  Thread.sleep(5 * 1000);
} catch (InterruptedException e){}

sleep()메서드는 항상 예외처리가 필요하기 때문에 새로운 메서드를 사용하는 것이 권장된다

void delay(long millis){
  try{
  	Thread.sleep(5 * 1000);
	} catch (InterruptedException e){}
}

delay(5 * 1000);

sleep()메서드는 static메서드이기 때문에 항상 자기 자신을 대상으로 수행한다.

만일 th1, th2라는 두 쓰레드가 존재했을때, th1에서 th2.sleep(5 * 1000)을 호출해도 th1이 일시정지 상태가 된다.

그래서 항상 Thread.sleep()을 사용하는 것이 좋다.

Interrupted()

void interrupt()
boolean isInterrupted()
static boolean interrupted()

interrupt()는 쓰레드에게 작업을 멈추라고 요청한다. 일시정지의 상태를 말하며 종료는 아니다.

진행중인 쓰레드는 일시정지로, 일시정지 상태의 쓰레드는 InterruptedException을 발생시켜 실행대기 상태로 바뀐다.

class Thread_1 implements Runnable{

    @Override
    public void run() {
        while(!Thread.interrupted()){ //Thread.interrupted()은 interrupt가 실행되지 전까진 false이다
            System.out.println("Hello");
        }
    }
}

public class playGround{
    public static void play() {
        Thread thread = new Thread(new Thread_1());
        thread.start();
        for(int i = 0 ; i< 10000; i++){} //시간 지연
        thread.interrupt(); //interrupt 
    }
}

//Hello
//Hello

join()

void join(long millis?, int nanos?)

다른 쓰레드의 작업을 기다린다.

join()에 시간을 지정하지 않으면 해당 쓰레드의 작업이 모두 끝날때까지 기다린다. 먼저 작업이 수행되어야 할 필요가 있을때 join()을 사용한다.

sleep() 메서드와 동일하게, join() 메서드도 (InterruptedException)이 발생한다. --> 필수 예외처리

예시

class Thread_1 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            System.out.print("-");
        }
    }
}

public class playGround{
    public static void play() {
        Thread thread = new Thread(new Thread_1());
        thread.start();
        long startTime = System.currentTimeMillis();
        try{
            thread.join(); //thread가 종료되어야 함
        } catch (InterruptedException e){}

        System.out.println("소요시간 : " + (System.currentTimeMillis() - startTime) + "ms");
      //5ms
    }
}
class Thread_1 implements Runnable{

    @Override
    public void run() {
        while(true){}
    }
}

public class playGround{
    public static void play() {
        Thread thread = new Thread(new Thread_1());
        thread.start();
        long startTime = System.currentTimeMillis();
        try{
            thread.join(2000);
        } catch (InterruptedException e){}

        System.out.println("소요시간 : " + (System.currentTimeMillis() - startTime) + "ms");
      //2000ms

    }
}

쓰레드의 동기화

멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있기 때문에, 진행중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 '동기화'가 필요하다.

쓰레드의 동기화란?

  • 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것

임계영역(critical section)과 잠금(lock)

공유 데이터를 사용하는 코드 영역을 임계영역으로 지정해 놓고, 이 영역에 접근 할 수 있는 lock을 가진 하나의 쓰레드만 임계영역의 코드를 수행 할 수 있게 해둔 것. 열쇠가 하나 뿐인 코드 영역이라고 생각하면 쉽다.

synchronized

  1. 메서드 전체를 임계 영역으로 지정

    public synchronized void criticalMethod(){};
  2. 특정한 영역을 임계 영역으로 지정

    synchronized(객체의 참조변수){}

예제

class Thread_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("balance = " + acc.getBalance());
        }
    }
}
class Account {
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

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

public class playGround{
    public static void play() {
        Thread thread_1 = new Thread(new Thread_1());
        thread_1.start();

        Thread thread_2 = new Thread(new Thread_1());
        thread_2.start();
    }
}

/*
balance = 900
balance = 900
balance = 600
balance = 600
balance = 400
balance = 300
balance = 200
balance = 0
balance = 200
balance = 0
*/

두 쓰레드가 withdraw()를 동시에 호출하면서 접근하고, balacne를 공유하고 있기 때문에, synchronized를 통한 동기화가 필요하다.

동기화는 멀티쓰레드의 안정성을 부여하지만 멀티쓰레드의 장점을 얻기 어렵다.

wait(), notify()

동기화 블록 내에서만 사용이 가능하다

  • wait() : 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다
  • notify() : waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
  • notifyAll() : 전부 깨운다.

만약 동기화 블록 내에서 lock을 가지고 수행중인 쓰레드가 해당 동기화 블록을 빠져나오지 못하는 상황이 생긴다면, 다른 쓰레드들은 그 메서드를 절대 수행 할 수 없을 것이다. 그래서 wait()을 통해 빠져나오지 못하는 쓰레드가 가지고 있는 lock을 풀고 대기하다가, notify()를 통해 lock을 재획득하여 수행하면 된다.

profile
앞으로 넘어지기

0개의 댓글