본문 바로가기
Programming/Java, Spring

자바 멀티 스레드 프로그래밍

by Renechoi 2023. 6. 12.

 

1. 프로세스와 스레드 

 

 

스레드 

- Java 프로그램은 하나의 프로세스로 만들어져 실행

- 프로세스는 실행중인 프로그램

- 스레스는 실행 중인 프로그램 내에 존재하는 소규모 실행 흐름 

- 스레드는 경량 프로세스 

 

멀티 스레드

- 하나의 프로세스 내부에서 여러 스레드가 만들어져 동시 실행 될 수 있음 

- 자바 프로그램은 하나의 스레드(main)에서 시작함

- main 스레드에서 자식 스레드를 만들어 시작시킬 수 있음

- 그러면 여러 스레드가 동시에 독립적으로 실행되고 종료 

  프로세스 스레드 멀티스레드
실행 단위 하나의 프로그램 또는 작업 단위 프로세스 내부에서 실행되는 작은 실행 단위 하나의 프로세스 내에서 동시에 실행됨
자원 공유 운영체제에 의해 독립적인 자원 할당 프로세스 내부 자원 공유 프로세스 내부 자원 공유
독립성 독립적인 메모리 공간을 가짐 부모 스레드와 같은 메모리 공간을 공유 부모 스레드와 같은 메모리 공간을 공유
생성 비용 많은 비용 비용 적음 비용 적음

 

 

2. Thread 클래스 

 

- 스레드의 생성과 관리를 위한 메서드 제공

- 스레드 생성을 위해 Thread 유형의 객체가 필요 

 

생성자:

Thread()
- Thread 클래스의 기본 생성자로, 새로운 스레드 객체를 생성

Thread(Runnable target)
- Runnable 인터페이스를 구현한 객체를 대상으로 스레드를 생성
- Runnable 인터페이스의 run() 메서드를 스레드가 실행

Thread(Runnable target, String name)
- Runnable 인터페이스를 구현한 객체를 대상으로 스레드를 생성하며, 스레드의 이름을 지정

 

 

주요 메서드:

start()- 스레드를 실행하고, 스레드의 run() 메서드를 호출
- 스레드는 자체적으로 실행되며, 다른 스레드와 동시에 실행될 수 있음 

run()
- 스레드의 실행 로직이 구현되는 메서드
- start() 메서드가 호출되면 자동으로 실행

join()
- 현재 스레드가 다른 스레드의 종료를 기다릴 수 있도록 함
- join() 메서드가 호출된 스레드는 대기하다가 대상 스레드가 종료되면 다음으로 진행

sleep(long millis)
- 현재 실행 중인 스레드를 지정된 시간 동안 일시 정지시킴
- 지정된 시간이 경과하거나 interrupt() 메서드가 호출되면 스레드는 다시 실행

interrupt()
- 스레드가 일시 정지 상태일 때 interruptedException을 발생시킴
- 스레드의 실행을 중단시키거나 종료할 때 사용



스레드의 실행 방법은 두 가지가 있다. 첫 번째는 Thread 클래스를 상속하는 것이고 두 번째는 Runnable 인터페이를 구현하는 것이다. 

 

Thread 클래스를 상속하는 방식 

 

class MyThread1 extends Thread {
   public void run() {
      for (int i = 0; i < 10; i++)
         System.out.println(getName());
   }
}

public class ThreadTest1 {
   public static void main(String[] args) {
      Thread t1 = new MyThread1();
      t1.start();
      Thread t2 = new MyThread1();
      t2.start();
      System.out.println("main");
   }
}

 

Thread 클래스를 상속받는 클래스 A를 정의한다.

여기서 run() 메서드는 재정의되었다. 

-> A 유형의 객체를 생성하고 start()를 호출 

 

 

 

Runnable 인터페이스를 구현하는 방식 

 

class MyThread2 implements Runnable {
   public void run() {
      for (int i = 0; i < 10; i++)
         System.out.println(Thread.currentThread().getName());
   }
}

public class ThreadTest2 {
   public static void main(String[] args) {
      Thread t1 = new Thread(new MyThread2(), "thd0");
      t1.start();
      Thread t2 = new Thread(new MyThread2(), "thd1");
      t2.start();
      System.out.println("main");
   }
}
 

 

 

Runnable 인터페이스를 구현하는 클래스 B를 정의한다.

마찬가지로 run() 메서드를 구현 

 

B의 객체를 인자로 사용하여 Thread 유형의 객체를 생성하고 start()를 호출한다. 

 

 

이와 같은 멀티스레드는 실행 결과가 매번 달라질 수 있다. 

각 스레드는 정해진 순서 없이 독립적으로 실행되기 때문이다. 

 

 

 

 

 

 

 

3. 스레드의 상태 

 

- 보통 1개의 cpu를 사용하여 여러 스레드가 수행됨 

- cpu를 얻어 실행되고 최종적으로 종료될 때까지 여러 상태 변화를 겪음 

 

상태 설명
Startable 객체가 생성되었으나 start() 실행전
Runnable start() 메서드가 호출되었으나 cpu 획득 전
Running cpu를 얻어 실행 중
Not Running cpu를 잃고 중단된 상태
Blocked, Waiting, Timed_Waiting
Dead run() 메서드가 종료된 상태

 

스레드의 상태 전이 

 

 

스레드의 상태 제어를 위한 메서드 

 

- void setPriority(int newPriority) : 스레드의 우선순위를 변경. 높은 우선순위를 가지는 스레드가 cpu를 얻을 확률이 높음 

- static void sleep(long millis) throws InterruptedException: 현재 실행 중인 스레드가 정해진 시간 동안 실행을 멈추고 Not Running 상태로 들어감 

- static void yield(): 현재 실행중인 스레드가 잠시 실행을 멈추고 Runnable 상태로 들어감 -> cpu를 다른 스레드에게 양보하는 것 

- void join() throws InterruptedException :

  - 스레드가 종료될 때까지 기다림

  - 현재 실행 중이었던 스레드는 Not Running 상태로 들어감

  - void join(long millis)는 최대 millis 시간 동안 기다림 

  - 기다리는 중에 다른 스레드가 이 스레드를 깨워주면 InterruptedException을 받으면서 리턴됨 

- void interrupt()

  - 스레드를 인터럽트 시킴

  - 스레드가 wait(), join(), sleep()에 의해 중단된 상태였다면 그 상태에서 깨어나 Runnable 상태가 됨 

- void wait() throws InterruptedException

- void wait(long millis) throws InterruptedException

  - 객체를 처리 중인 스레드를 정해진 시간 동안 중지시킴

  - 다른 스레드가 해당 객체에 대해 notify() 메서드를 실행시켜 주면 이 스레드가 깨어날 수 있음

  - 이 메서드는 synchronized 메서드의 내부에서만 호출 가능 

- void notify() 

  - wait()를 호출하여 중단된 스레드를 깨워줌

  - 이 메서드는 synchronized 메서드의 내부에서만 호출 가능 

 

 

 

스레드를 생성하고 조인(join) 메서드를 사용하여 메인 스레드가 두 개의 생성된 스레드의 종료를 기다리는 예제 코드 

 

public class JoinTest2 {
   public static void main(String args[]) throws InterruptedException {
      Thread t1 = new MyThread3();
      t1.start();
      Thread t2 = new MyThread3();
      t2.start();
      t1.join();
      t2.join();
      System.out.println("main");
   }
}

class MyThread3 extends Thread {
   public void run() {
      for (int i = 0; i < 1000; i++) {
         System.out.println(getName());
         Thread.yield();
      }
   }
}

- Thread t1, Thread t2는 각각 MyThread3 객체를 생성한다. MyThread3는 Thread 클래스를 상속받은 사용자 정의 스레드이다. 

- start() 메서드를 통해 t1과 t2 스레드를 시작한다. 각 스레드는 run() 메서드를 실행하게 된다.

- join() 메서드를 호출하는 부분: 메인 스레드가 t1과 t2의 종료를 기다리도록 한다. 조인 메서드를 호출하면 현재 스레드(여기서는 메인 스레드)는 해당 스레드(t1 또는 t2)의 종료를 기다리며, 해당 스레드가 종료될 때까지 실행이 중지된다.

- 그 결과 t1과 t2 스레드가 모두 종료된 후에  "main"을 출력한다.

 

세 개의 스레드를 생성하고 조인(join) 및 인터럽트(interrupt)를 사용하여 스레드의 상태를 제어하는 예시 코드 

 

class MyThread extends Thread {
   Thread thdNext = null;

   public MyThread(String szName) {
      super(szName);
   }

   public void run() {
      for (int i = 0; i < 100; i++) {
         try {
            Thread.sleep(1000000);
         } catch (InterruptedException e) {
            System.out.print(getName() + " ");
            if (thdNext.isAlive())
               thdNext.interrupt();
         }
      }
   }

   public void setNextThread(Thread t) {
      thdNext = t;
   }
}

public class JoinAndInterruptTest {
   public static void main(String args[]) {
      MyThread my_thread1 = new MyThread("thd1");
      MyThread my_thread2 = new MyThread("thd2");
      MyThread my_thread3 = new MyThread("thd3");
      my_thread1.setNextThread(my_thread2);
      my_thread2.setNextThread(my_thread3);
      my_thread3.setNextThread(my_thread1);
      my_thread1.start();
      my_thread2.start();
      my_thread3.start();
      try {
         my_thread1.interrupt();
         my_thread1.join();
         my_thread2.join();
         my_thread3.join();
      } catch (InterruptedException e) {
         System.out.println(e);
      }
      System.out.println("main");
   }
}

 


- run() 메서드: 반복문을 통해 0부터 99까지의 숫자를 출력하고, Thread.sleep() 메서드를 호출하여 1000000 밀리초(1000초) 동안 스레드를 일시 정지시킨다. 일시 정지 중에 InterruptedException이 발생하면 현재 스레드의 이름을 출력하고, 다음 스레드가 여전히 실행 중이라면 해당 스레드를 인터럽트한다.

- setNextThread() 메서드: 다음에 실행될 스레드를 설정한다. 다음 스레드를 매개변수로 받아 thdNext 변수에 할당한다.

- main() 메서드: MyThread 객체를 생성하고, setNextThread() 메서드를 사용하여 다음에 실행될 스레드를 설정한다. 각 스레드를 시작한 후, interrupt() 메서드를 호출하여 첫 번째 스레드를 인터럽트시킨다. 그리고 조인 메서드를 사용하여 각 스레드의 종료를 기다린다. 만약 InterruptedException이 발생하면 예외를 처리하고 "main"을 출력한다.

위의 코드는 세 개의 스레드가 서로 순환하면서 실행되는 예시이다. 반복문 한 한 수행마다 sleep이 호출된다. 각 스레드는 일시 정지 중에 인터럽트가 발생하면 다음 스레드를 인터럽트하여 실행 흐름을 제어한다. 메인 스레드는 조인을 사용하여 각 스레드의 종료를 기다리고, 인터럽트를 통해 스레드의 실행을 제어하며, "main"을 출력하여 마무리한다.

 

 

 

 

 

 

4. 스레드 동기화 

 

 

스레드 간섭 

 

여러 개의 스레드들이 하나의 공유 객체에 동시 접근하는 경우 일관성이 깨지는 문제가 발생한다. 

 

Counter 객체를 공유하는 두 개의 스레드 간에 경쟁 상태(race condition)가 발생하는 스레드 간의 간섭 예제 코드 

 

 


class Counter {
   private int c = 0;

   public void increment() {
      c++;
   }

   public void decrement() {
      c--;
   }

   public int value() {
      return c;
   }
}

class MyThread5 implements Runnable {
   Counter c;

   public MyThread5(Counter c) {
      this.c = c;
   }

   public void run() {
      for (int i = 0; i < 100000; i++) {

         c.increment();
      }
   }
}

class MyThread4 implements Runnable {
   Counter c;

   public MyThread4(Counter c) {
      this.c = c;
   }

   public void run() {
      for (int i = 0; i < 100000; i++) {
         c.decrement();
      }
   }
}

public class ThreadInterruptionTest {
   public static void main(String[] args) throws
      InterruptedException {
      Counter c = new Counter();
      Thread t1 = new Thread(new MyThread4(c));
      Thread t2 = new Thread(new MyThread5(c));
      t1.start();
      t2.start();
      t1.join();
      t2.join();
      System.out.println(c.value());
   }
}

 

- Counter 클래스: c라는 private 변수를 가지고 있는 카운터 클래스. increment() 메서드는 c 값을 증가시키고, decrement() 메서드는 c 값을 감소시킨다. 

- MyThread5 클래스: Runnable 인터페이스를 구현한 스레드 클래스. Counter 객체를 받아 생성자에서 할당받는다. run() 메서드는 반복문을 통해 100000번 동안 Counter 객체의 increment() 메서드를 호출하여 c 값을 증가시킨다.

- MyThread4 클래스: Runnable 인터페이스를 구현한 스레드 클래스. Counter 객체를 받아 생성자에서 할당받는다. run() 메서드는 반복문을 통해 100000번 동안 Counter 객체의 decrement() 메서드를 호출하여 c 값을 감소시킨다.

- main() 메서드: Counter 객체를 생성하고, 이를 공유하는 두 개의 스레드를 생성한다. 각 스레드를 시작한 후, 조인을 사용하여 각 스레드의 종료를 기다린다. 마지막으로 Counter 객체의 value() 메서드를 호출하여 최종적인 c 값을 출력한다.

 

 

위의 코드에서 문제점은 Counter 객체를 공유하고 있는 두 개의 스레드가 동시에 increment() 또는 decrement() 메서드를 호출할 수 있다는 것이다. 즉, 이로인해 스레드 간의 간섭이 발생하며 매번 다른 결과 값을 리턴하는 문제가 발생한다. 두 스레드가 동시에 increment() 또는 decrement()를 호출한다면, 경쟁 상태가 발생하여 예상치 못한 결과가 발생할 수 있다. 예를 들어, 한 스레드가 increment() 메서드를 호출하기 전에 다른 스레드가 decrement() 메서드를 호출하여 c 값이 잘못 감소되는 것이다.

 

 

스레드 동기화 

 

- 서로 다른 스레드들이 공유 자원을 다룰 때, 일관성을 유지하도록 하는 것. 

- 한번에 오직 한 개의 스레드만이 해당 공유 객체에 접근하도록 "동기화" 한다. 

 

- 상호 배제 원칙
- 키워드 synchronized -> 동기화 메서드 또는 동기화 블록 제공, 공유 자원에 대한 배타적인 작업을 독점하도록 한다.

 

synchronized 메서드 

- 한 번에 하나의 스레드에 의해서만 실행 가능

- synchronized 메서드를 실행하려면 메서드를 호출한 객체에 대한 lock을 얻어야 한다 

  -> 다른 스레드는 동일 객체에 대해 synchronized 메서드를 실행할 수 없게 된다 

- 일부 블록만 동기화하는 것도 가능하다 

  -> synchronized (객체) {...} 

  -> 객체는 공유자원으로 대개 this로 사용 

 

 

 

synchronized를 사용하여 수정한 Counter 객체 

public class Counter2 {
   private int c = 0;

   public void increment() {
      synchronized (this) {
         c++;
      }
   }

   public void decrement() {
      synchronized (this) {
         c--;
      }
   }

   public int value() {
      return c;
   }
}

 

 

 

 


참고 도서 : Java 프로그래밍 (김희천, 정재헌, KNOU press 출판) 

반응형