[Java]멀티쓰레드 프로그래밍

정석용·2023년 4월 12일
0

Java

목록 보기
10/15
post-thumbnail

Thread(쓰레드)란?

우리가 사용하는 프로그램은 하나의 프로세스이다. 프로그램을 실행하면 OS로부터 자원을 할당받아 프로세스가 된다.

프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리 등의 자원과 쓰레드로 구성되어 있다. 프로세스의 자원을 이용해서 실제 작업을 수행하는 것이 바로 쓰레드이다.

Thread의 구현과 실행
자바에서는 쓰레드를 관리하기 위한 메서드와 변수들을 java.lang.Thread 클래스에서 제공한다.

쓰레드의 구현 방법으로는 Thread클래스 상속받는 방법과 Runnable 인터페이스를 구현하는 방법이 있다.

Thread 클래스를 상속
생성

public class HelloThread extends Thread{
    @Override
    public void run() { // 쓰레드가 수행할 작업을 작성
        System.out.println("hello");
    }
}

실행

public static void main(String[] args) {
        HelloThread thread = new HelloThread();
        thread.start();
    }

thread 클래스를 상속한 경우 객체 인스턴스를 생성 한 뒤 strar()매서드를 호출.

Runnable 인터페이스 구현

생성

public class HelloRunnable implements Runnable {
    @Override
    public void run() { // 쓰레드가 수행할 작업을 작성
        System.out.println("hello");
    }
}

실행

public static void main(String[] args) {
        HelloRunnable runnable = new HelloRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }

Runnable 인터페이스를 구현한 경우 객체 인스턴스를 thread 객체 생성자에 전달 한 뒤 start()메서드를 호출.

자바에서 쓰레드를 실행하기 위해서는 Runnable 인터페이스를 구현해야 한다.
runnable 인터페이스는 추상 메서드 run() 하나만 있는 함수형 인터페이스로 run() 메서드를 오버라이딩 하여 쓰레드를 실행할 수 있다.

start()메서드로 실행하는 이유는 멀티 쓰레드 프로그래밍을 할 수 있긴 때문이다.

쓰레드의 상태

쓰레드는 객체가 생성, 실행, 종료되기까지 다양한 상태를 가진다.
각 쓰레드는 Thread.state 상태로 정의되어있다.
Thread.state 내부에는 6개의 문자열 상수(NEW, RUNNABLE, TERMINATED, TIMED_WAITING, BLOCKED, WAITING)가 저장되어 있다.

  • new : new 키워드로 쓰레드의 객체가 생성된 상태
  • TERMINATED : run()메서드의 작업 내용이 모두 완료돼 쓰레드가 종료된 상태
  • BLOCKED : 먼저 실행중인 쓰레드가 key를 반납할 때까지 기다리고 있는 상태
  • TIMED_WAITING : 주어진 시간 동안 기다리는 상태
  • RUNNABLE : 실행 상태로 언제든지 갈 수 있는 상태
  • WAITING : 다른 스레드가 통지할 때까지 기다리는 상태

쓰레드 상태 제어

실행중인 스레드의 상태를 변경하는 것

  • interrupt() : 일시 정지 상태의 스레드에 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다.
  • notify(), notifyAll() : 동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.
  • Deprecated (notify(), notifyAll()로 대체), sleep(long millis)
    , sleep(long millis, int nanos) : 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
  • join(), join(long millis), join(long millis, int nanos) : join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면 join() 메소드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 한다.
  • wait(), wait(long millis), wait(long millis, int nanos) : 동기화(synchronized) 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기 상태로 갈 수 있다.
  • yield() : 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고, 실행 대기 상태가 된다.

스레드의 우선 순위

스레드에 대해 허용되는 우선 순위 값은 1에서 10사이이다.

Java에서 우선 순위를 가져오고 설정하는 메서드

  • public final int getPriority() : 주어진 스레드의 우선 순위를 반환합니다.
  • public final void setPriority(int newPriority) : 스레드의 우선 순위를 newPriority 값으로 변경합니다. 이 메소드는 newPriority 매개변수의 값이 minimum(1) 및 maximum(10) 제한을 초과하는 경우 IllegalArgumentException을 발생시킵니다.

우선 순위를 제일 높게 설정한다고 항상 먼저 실행됨을 보장 할 수는 없다.

Main Thread

메인 메소드가 실행이 되면 코드 한줄 한줄 순차적으로 시작하게 되고, return 을 만나거나, main 메서드의 끝이오면 종료하게 된다.

모든 자바 앱에서 메인 쓰레드는 main 메소드를 통해서 실행하게 된다.

public class App {
    public static void main(String[] args) {
        System.out.println("Main Thread 시작");

        for (int i = 0; i < 10001; i++) {
            System.out.println("hello");
        }

        return; // 종료
    }
}

이런 메소드가 종료되면, 프로세스 자체도 종료된다.하지만 멀티 스레드를 구상 하였을때 메인 메소드가 끝이 나더라도 쓰레드 마지막이 끝나야 프로세스가 종료된다.

데몬 스레드

main 스레드를 보조하는 스레드. 메인스레드가 종료되면 데몬 스레드도 강제적으로 종료.

스레드 동기화

싱글 스레드, 즉 메인 스레드 한개로 구성된 앱에서는 문제가 되지 않겠지만, 만약 멀티 스레드 상황에서 객체를 생성한다면, 여러개의 스레드가 객체를 건들 수 있다.
이런 상황에서 지금 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드가 작업이 끝날 때 까지 객체에 잠긍을 걸어서 다른 스레드가 사용할 수 없도록 한다.

임계영역
멀티 스레드에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역이라고 한다.
자바는 이런 임계 영역을 지정하기 위해서 동기화 메소드와 동기화 블록을 제공하게 된다. 동기화 메소드를 만드는 방법은 메소드 선언에 synchronized 키워드를 붙이면 된다. (동기화 블록을 선언하는 것도 좋은 방법이다.)

스레드 데드락

둘 이상의 스레드가 lock을 획득하기 위해 기다리는데, 이 lock을 잡고 있는 스레드도 똑같이 다른 lock을 기다리며 서로 블록 상태에 놓이는 것을 말한다. 데드락은 다수의 쓰레드가 같은 lock을, 동시에, 다른 명령에 의해 획득하려 할 때 발생한다.

EX.

public class TreeNode {
 
  TreeNode parent   = null;  
  List     children = new ArrayList();

  public synchronized void addChild(TreeNode child){
    if(!this.children.contains(child)) {
      this.children.add(child);
      child.setParentOnly(this);
    }
  }
  
  public synchronized void addChildOnly(TreeNode child){
    if(!this.children.contains(child){
      this.children.add(child);
    }
  }
  
  public synchronized void setParent(TreeNode parent){
    this.parent = parent;
    parent.addChildOnly(this);
  }

  public synchronized void setParentOnly(TreeNode parent){
    this.parent = parent;
  }
}

TreeNode 클래스의 예제를 이용해 알아보자.

스레드1이 parent.addchild메소드를, 스레드2는 child.setparent메소드를 각각 동시에, 같은 parent와 child 인스턴스에 호출한다면, 데드락이 발생할 수 있다.

https://wisdom-and-record.tistory.com/48
https://connie.tistory.com/12
https://handr95.tistory.com/37

profile
오늘도 성장중

0개의 댓글