Backend/Java

멀티쓰레드 프로그래밍

가은파파 2021. 3. 4. 01:42

목표

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

학습할 것 (필수)

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

Thread 클래스와 Runnable 인터페이스

쓰레드를 정의하는 방법은 2가지 방법이 있습니다.

Thread를 상속받는 방법과 Runnable의 인터페이스를 받아서 정의하는 방법이 있습니다.

//Thread 클래스 상속
import java.util.Random;

public class MyThread extends Thread {

  private static final Random random = new Random();

  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    System.out.println("- " + threadName + " has been started");
    int delay = 1000 + random.nextInt(4000);
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("- " + threadName + " has been ended (" + delay + "ms)");
  }

}

//Runnable 인터페이스 활용
import java.util.Random;

public class MyRunnable implements Runnable {

  private static final Random random = new Random();

  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    System.out.println("- " + threadName + " has been started");
    int delay = 1000 + random.nextInt(4000);
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("- " + threadName + " has been ended (" + delay + "ms)");
  }

}

public class ThreadRunner {

  public static void main(String[] args) {
    // create thread objects
    Thread thread1 = new MyThread();
    thread1.setName("Thread #1");
    Thread thread2 = new MyThread();
    thread2.setName("Thread #2");

    // create runnable objects
    Runnable runnable1 = new MyRunnable();
    Runnable runnable2 = new MyRunnable();

    Thread thread3 = new Thread(runnable1);
    thread3.setName("Thread #3");
    Thread thread4 = new Thread(runnable2);
    thread4.setName("Thread #4");

    // start all threads
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
  }

}

만약 Thread 클래스의 메서드가 오버라이딩이 필요한 경우 Thread를 상속받아서 사용한다.

 

위의 예제 코드를 보시면 Thread 클래스를 확장하는 것이 실행 방법이 미세하게 더 간단하다는 것을 볼 수 있습니다. 하지만 자바에서는 다중 상속을 하용하지 않기 때문에, Thread 클래스를 확장하는 클래스는 다른 클래스를 상속받을 수 없습니다. 반면에 Runnable 인터페이스를 구현했을 경우에는 다른 인터페이스를 구현할 수 있을 뿐만 아니라, 다른 클래스도 상속받을 수 있습니다. 따라서 해당 클래스의 확장성이 중요한 상황이라면 Runnable 인터페이스를 구현하는 것이 더 바람직할 것입니다. 실제로 많은 개발자들이 대부분의 상황에서 Thread 클래스를 확장하기보다는 Runnable 클래스를 구현하는 것을 선호합니다.

 

쓰레드는 순서대로 작동하지 않는다. 그 순서는 OS

 

start()과 run() : start()가 새로운 호출 스택을 생성하여 run()을 호출해준다.


# 쓰레드의 상태

스레드의 일반적인 실행 상태

상태 열거 상수 설명
객체 생성 NEW 스레드 객체가 생성, 아직 start() 메소드가 호출되지 않은 상태
실행 대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시 정지 WAITING 다른 스레드가 통지할 때까지 기다리는 상태
TIMED_WAITING 주어진 시간 동안 기다리는 상태
BLOCKED 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
종료 TERMINATED 실행을 마친 상태

스레드의 실행과 일시 정지 상태

안전상의 이유료 stop()은 deprecated되었다.


# 쓰레드의 우선순위

쓰레드는 우선순위라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.

 

예를 들어 파일 전송기능이 있는 메신저의 경우, 파일다운로드를 처리하는 쓰레드보다 채팅 내용을 전송하는 쓰레드의 우선순위가 더 높아야 사용자가 채팅을 하는데 불편함이 없을 것이다. 대신 파일다운로드 작업에 걸리는 시간은 더 길어질 것이다.

이처럼 시각적인 부분이나. 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선 순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.

만약 A,B 두 쓰레드에게 거의 같은 양의 실행시간이 주어지지만, 우선순위가 다르다면 우선순위가 높은 A에게 상대적으로 B보다 더 많은 양의 실행시간이 주어지고 결과적으로 더 빨리 작업이 완료될 수 있다.

class ThreadPriority {
	public static void main(String args[]) {
		A th1 = new A();
		B th2 = new B();
		th1.setPriority(4); // defalut 우선순위 5
		th2.setPriority(7);
		System.out.println("Priority of th1(-) : " + th1.getPriority() );
		System.out.println("Priority of th2(|) : " + th2.getPriority() );
		th1.start();
		th2.start();
	}
}

class A extends Thread {
	public void run() {
		for(int i=0; i < 300; i++) {
			System.out.print("-");
			for(int x=0; x < 10000000; x++);
		}
	}
}


class B extends Thread {
	public void run() {
		for(int i=0; i < 300; i++) {
			System.out.print("|");
			for(int x=0; x < 10000000; x++);
		}
	}	
}


# Main 쓰레드

모든 자바 어플리케이션은 Main Thread가 main() 메소드를 실행하면서 시작됩니다. 예외는 없습니다. 이러한 Main Thread 흐름 안에서 싱글 스레드가 아닌 멀티 스레드 어플리케이션은 필요에 따라 작업 쓰레드를 만들어 병렬로 코드를 실행할 수 있습니다. 싱글 스레드 같은 경우 메인 스레드가 종료되면 프로세스도 종료되지만, 멀티 스레드는 메인 스레드가 종료되더라도 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않습니다.

 

데몬 스레드

데몬 스레드는 메인 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드로 주 스레드가 종료되면 데몬 스레드 더는 존재 의미가 없기에 강제로 종료됩니다. 워드의 자동 저장 기능을 예로 들을 수 있습니다. 데몬 스레드를 만드는 방법은 스레드를 만들고 해당 스레드에 setDaemon(true); 메소드를 세팅하는 것입니다.


# 동기화

멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.

이를 막기 위해 '동기화'가 필요

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

 

동기화 방법

동기화하려면 간섭받지 않아야 하는 문장들을 '임계 영역'으로 설정

임계영역은 락(lock)을 얻는 단 하나의 쓰레드만 출입가능(객체 1개에 락 1개)

 

임계영역 설정은 2가지 방법이 있다.

//1. 메서드 전체를 임계 영역으로 지정
public synchronized void calcSum(){
}

//2.특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) {
}

동기화구역은 좁고 최대한 적은게 좋습니다.

 

동기화를 하게 되면 효율이 떨어진다. 그래서 wait()(기다리기)와 notify()(통보,알려주기) 를 통해 효율을 높인다.

 

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

 

lock을 쥐고 있으면 쓰레드 작업이 안되니 waiting pool에 대기하게 하고 lock을 오래 쥐는 일이 없게 함/


# 데드락

교착 상태(膠着狀態, 영어: deadlock)란 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 가리킨다. 예를 들어 하나의 사다리가 있고, 두 명의 사람이 각각 사다리의 위쪽과 아래쪽에 있다고 가정한다. 이때 아래에 있는 사람은 위로 올라 가려고 하고, 위에 있는 사람은 아래로 내려오려고 한다면, 두 사람은 서로 상대방이 사다리에서 비켜줄 때까지 하염없이 기다리고 있을 것이고 결과적으로 아무도 사다리를 내려오거나 올라가지 못하게 되듯이, 전산학에서 교착 상태란 다중 프로그래밍 환경에서 흔히 발생할 수 있는 문제이다.

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

 

 

# 10주차 리뷰

  • 일반적으로 컨테이너 jetty, Tomcat가 개발자가 작성해놓은 요청을 멀티쓰레드 프로그래밍을 해주기 때문에 멀티쓰레드를 사용하고 있다고 봐도 무방하다.
  • 아래와 같은 경우에 컨테이너를 타지 않은 상황이라 ExcutorService를 활용하여 멀티쓰레드 프로그래밍이 가능하다.
Class multiThread {
    ExcutorService service = Excutors.newFixedThreadPool(8); //thread갯수
    CountDownLatch latch = new CountDownLatch(15);

    for(int index = 1; index <= 15; index++){
        service.execute(
            //적용할 쓰레드 코드
            @Override
            public void run(){
                //해당코드
                latch.countDown();
            }
        );
    }
    latch.await();
    service.shutdown();
}
  • STM, Atomic
  • critical path
  • 경쟁상태(race condition)
    • 둘 이상의 입력이나 조작이 동시에 일어나 의도하지 않은 결과를 가져오는 경우를 말한다. 파일또는 변수와 같은 공유자원을 접근하는 하나 또는 그 이상의 프로세스들의 다중 접근이 제대로 제어되지 않은 것

 

출처