[JAVA] 5주차 : Thread

INHEES·2023년 8월 16일

Java

목록 보기
9/13

JAVA 5주차 문법 : Thread

Process

프로세스 작업 단위

  • 프로세스 는 "실행중인 프로그램" 을 의미한다.
  • OS 위에서 실행되는 모든 프로그램은 OS 가 만들어준 프로세스 위에서 실행된다.
  • 프로세스(리소스)를 할당 받을때 프로그램의 code, Data 그리고 메모리 영역(Stack, Heap) 를 받는다.

프로세스 구조

  • Data 는 프로그램이 실행중 저장 가능한 초기화된 데이터의 저장공간이다.
  • Memory 영역
    • stack : 지역변수, 리턴 변수를 저장하는 공간
    • heap : 동적으로 필요한 변수를 저장하는 공간 ex) new(), malloc()

프로세스 : OS 로부터 자원을 할당받는 작업의 단위
쓰레드 : 프로세스가 할당받은 자원을 이용하는 실행의 단위

Data의 저장공간과 Memory 영역은 다른 의미이다.

Thread

  • 스레드 란 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다.
  • 모든 프로세스에는 한개 이상의 스레가 존재하여 작업을 수행한다.

쓰레드의 생성

  • 프로세스가 작업중인 프로그램에서 실행요청이 들어오면 쓰레드를 만들어 명령을 처리한다.
  1. Runnable 인터페이스를 구현하는 방법
    • 필요한 클래스를 상속 받기에 확작성에 유리하다.
  2. Thread 클래스를 상속받는 방법

두 방법 모두 run() 메서드에 작성하면 된다.

  // 1.
    public class TestThread extends Thread {
                    @Override
                    public void run() {
                                // 쓰레드 수행작업
                    }
    }
  // 2.
  public class TestRunnable implements Runnable {
                  @Override
                  public void run() {
                              // 쓰레드 수행작업 
                  }
  }

  TestThread thread = new TestThread(); // 쓰레드 생성
  thread.start() // 쓰레드 실행

// 람다식 표현 
  public class Main {
      public static void main(String[] args) {
          Runnable task = () -> {...};

          Thread thread1 = new Thread(task);
          thread1.setName("thread1");
          Thread thread2 = new Thread(task);
          thread2.setName("thread2");

          thread1.start();
          thread2.start();
      }
  }
  • 람다식 블록안에 run() 쓰일 수행할 작업을 쓰면 된다.

쓰레드의 자원

  • 쓰레드 들은 실행을 위한 프로세스 내 주소공간이나 Heap(메모리 공간)을 공유 받는다.
  • 쓰레드 각자 Stack(메모리공간) 도 할당받는다.

Java Thread

  • JVM 프로세스 위에서 실행된다.
  • Java Main 쓰레드부터 실행되며 JVM에 의해 실행된다.

Multi Thread

  • java는 메인 쓰레드가 main() 메서드를 실행시키면도 시작이 된다.
  • JVM 의 메인 쓰레드가 종료되면, JVM 도 같이 종료된다.
  • 작업별로 쓰레드를 생성하여 병렬로 코드를 실행 가능하다.

멀티 쓰레드 장점

  1. 동시 작업이 가능하기에 성능이 좋아진다.
  2. 스택을 제외한 모든 영역에서 메모리를 공유하기에 자원을 효율적으로 사용한다.
  3. 기능별로 쓰레드를 분리하는 비동기 특징이 있다.

멀티 쓰레드 단점

  1. 공유되는 자원에서 충돌이 발생하는 동기화 문제가 일어난다.
  2. 쓰레드들 끼리 서로 작업이 종료되길 무한히 기다리는 교착상태(데드락)이 발생한다.

Daemon Thread

background 에서 실행되는 낮은 우선순위를 가지는 쓰레드이다.

  • 보조적인 역할을 담담하며 메모리 영역을 정리해주는 가비지 컬렉터(GC) 가 있다.
  thread.setDaemon(true); // true로 설정시 데몬스레드로 실행됨

사용자 Thread

foreground 에서 실행되는 높은 우선순위를 가진 쓰레드이다.

  • 프로그램 기능을 담담하며 대표적으로 메인 쓰레드 가 있다.
  • 기존에 우리가 만들었던 쓰레드들이 모두 사용자 쓰레드 이다.

JVM 은 사용자 쓰레드의 작업이 긑나면 데몬 쓰레드도 자동을 종료한다.

쓰레드 우선순위

쓰레드는 작업의 중요도에 따라 각 쓰레드의 우선순위를 부여할 수 있다.다.

  • 우선순위가 높을 수록 더 많은 작업 시간을 부여 받는다.
  • 기본 값은 보통 우선순위로 5 이다.
  • 이 우선순위의 범위는 OS 가 아니라 JVM 에서 설정한 것이다.
  Thread thread1 = new Thread(task1);
  thread1.setPriority(8);

  int threadPriority = thread1.getPriority();
  System.out.println("threadPriority = " + threadPriority);

하지만 우선순위가 높다고 반드시 쓰레드가 먼저 종료되는 것이 아니다.
확률이 높은것이지 실제로 코드로 결과를 보면 확연한 차이가 들어나지 않는다.

쓰레드 그룹

관련된 쓰레드들 끼리 묶어서 다룰 수 있다.

  • JVM 이 시작되면 system 그룹이 생성 되어 포함된다.
  • 메인 쓰레드는 system 그룹 하위에 있는 main 그룹에 포함된다.
  • 우리가 생성한는 쓰레드들은 main 쓰레드 하위에 포함된다. 그룹이 지정되지 않으면 자동으로 main 그룹에 포함된다.
  // ThreadGroup 클래스로 객체를 만듭니다.
  ThreadGroup group1 = new ThreadGroup("Group1");

  // Thread 객체 생성시 첫번째 매개변수로 넣어줍니다.
  // Thread(ThreadGroup group, Runnable target, String name)
  Thread thread1 = new Thread(group1, task, "Thread 1");

  // Thread에 ThreadGroup 이 할당된것을 확인할 수 있습니다.
  System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName());

쓰레드 상태

  • 쓰레드는 실행과 대기를 반복하며 run() 메서드를 수행한다.
  • run() 메서드가 종료되면 실행이 멈춘다.
  • 일시정지 된 쓰레드가 실행 상태로 넘어가기 위해서는 아래와 같은 순서를 지켜야한다.

상태Enum설명
객체생성NEW쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태
실행대기RUNNABLE실행 상태로 언제든지 갈 수 있는 상태
일시정지WAITING다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태
일시정지TIMED_WAITING주어진 시간 동안 기다리는 상태
일시정지BLOCKED사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태
종료TERMINATED쓰레드의 작업이 종료된 상태

쓰레드 제어

쓰레드를 제어 가능한 Thread 클래스 안의 기능들을 사용할 수 있다.

  • Thread 는 static method 이기에 Thread.method 처럼 사용한다.

sleep()

  try {
      Thread.sleep(2000); // 2초
  } catch (InterruptedException e) {
      e.printStackTrace();
  }
  • 특정 쓰레드를 지목해서 멈추는 것은 불가능하다.
  • sleep 상태에서 interrupt()를 만나면 InterruptedException이 발생한다.

interrupt()

   while (!Thread.currentThread().isInterrupted()) 
   {...}
   
  Thread thread = new Thread(task, "Thread");
          thread.start();
          thread.interrupt();
          System.out.println("thread.isInterrupted() = "
          + thread.isInterrupted());
  • 쓰레드가 start() 된 후 동작하다 interrupt()를 만나 실행하면 interrupted 상태가 true가 됩니다.
  • !Thread.currentThread().isInterrupted() 로 interrupted 상태를 체크해서 처리하면 오류를 방지할 수 있습니다.

join()

정해진 시간동안 지정한 쓰레드가 작업하는 것을 기다린다.
만약 시간을 정하지 않으면 작업이 끝날 때까지 기다린다.

  Thread thread = new Thread(task, "thread");

  thread.start();

  try {
      thread.join();
  } catch (InterruptedException e) {
      e.printStackTrace();
  }

yield()

남은 시간을 다음 쓰레드에게 양보하고 쓰레드 자신은 실행 대기 상태가 된다.

  public class Main {
      public static void main(String[] args) {
          Runnable task = () -> {
              try {
                  for (int i = 0; i < 10; i++) {
                      Thread.sleep(1000);
                      System.out.println(Thread.currentThread().getName());
                  }
              } catch (InterruptedException e) {
                  Thread.yield();
              }
          };

          Thread thread1 = new Thread(task, "thread1");
          Thread thread2 = new Thread(task, "thread2");

          thread1.start();
          thread2.start();

          try {
              Thread.sleep(5000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

          thread1.interrupt();

      }
  }
  • thread1과 thread2가 같이 1초에 한번씩 출력 후 5초뒤에 thread1에서 InterruptedException이 발생하면서 Thread.yield(); 이 실행되어 thread1은 실행대기 상태로 변경되면서 남은 시간은 thread2에게 리소스가 양보된다.

synchronized()

  • 한 쓰레드가 진행중인 작업을 다른 쓰레드의 침범을 막는것을 쓰레드 동기화(synchronization) 이라고 한다.
  • 작업중인 코드(메서드, 특정영역)를 임계영역 으로 설정한다.
  • 임계영역은 Lock 을 가진 단 하나의 쓰레드만 출입이가능하다.
  public synchronized void asyncSum() {
        ...침범을 막아야하는 코드...
  }
  ////////////////////////////////////
  synchronized(해당 객체의 참조변수) {
		try{
        } catch(Error e){
        	...
        }
}
  

wait() & notify()

  • wait() 을 호출하여 쓰레드가 Lock을 반납하고 기다린다.
  • wait()waiting pool 에서 대기한다.
  • 추후에 작업을 진행할 수 있는 상황이 되면 notify()를 호출하여 Lock을 얻을 수 있다.
  • notify()waiting pool 에 있는 임의의 쓰레드만 통지한다.

Lock

synchronized 는 같은 메서드 내에서만 Lock 을 걸 수 있다는 제약이 있다. 때문에 Lock 클래스 를 활용한다.

ReentrantLock

  • 재진입가능한 Lock, 특정 조건에서 Lock 을 풀고, 나중에 다시 Lock 을 얻는다.
  • 스레드가 이미 락을 가지고 있더라도 락을 유지하며 계속 실행할 수 있어 데드락이 발생하지 않는다.
  public class MyClass {
      private Object lock1 = new Object();
      private Object lock2 = new Object();

      public void methodA() {
          synchronized (lock1) {
              methodB();
          }
      }

      public void methodB() {
          synchronized (lock2) {
              // do something
              methodA();
          }
      }
  }

ReentrantReadWriteLock

  • 읽기, 쓰기를 위한 각각의 Lock을 따로 제공한다.
  • 읽기에는 공유적이고, 쓰기에는 배타적인 성격을 가진다.

StampedLock

  • ReentrantReadWriteLock에 낙관적인 Lock의 기능을 추가한다.
  • 낙관적인 Lock 이란 데이터를 변경하기 전에 락을 걸지 않는 것이다.
  • 쓰기작업을 수행하려 할때 데이터가 이미 변경된경우 다시 읽기 작업을 수행하여 새로운 값을 읽어들이고, 변경 작업을 다시 수행한다.
    • 단 데이터 변경에 있어 충돌이 일어날 가능성이 적은 상황에서 사용한다.
    • 낙관적인 읽기 Lock 은 쓰기 Lock 에 의해 해제 가능하다.
    • 읽기와 쓰기의 충돌이 일어날 때 쓰기후 읽기 Lock 을 건다.

Condition

wait() & notify() 의 문제점인 waiting pool 안의 쓰레드를 구분하지 못하는데 그것의 해결책이 Condition 이다.

  • JDK 5 에서는 java.util.concurrent.locks 패키지에서 Condition 인터페이스를 제공한다.
  • Condition 인터페이스는 ReentrantLock 클래스와 함께 사용됩니다.
  • Conditionawait()signal()를 사용한다.
  private ReentrantLock lock = new ReentrantLock();

  // lock으로 condition 생성
  private Condition condition1 = lock.newCondition();
  private Condition condition2 = lock.newCondition();

  private ArrayList<String> tasks = new ArrayList<>();

  // 작업 메서드
  public void addMethod(String task) {
          lock.lock(); // 임계영역 시작

          try {
              while(tasks.size() >= MAX_TASK) {
                      String name = Thread.currentThread().getName();
                      System.out.println(name+" is waiting.");
                      try {
                          condition1.await(); // wait(); condition1 쓰레드를 기다리게 합니다.
                          Thread.sleep(500);
                      } catch(InterruptedException e) {}	
              }

              tasks.add(task);
              condition2.signal(); // notify();  기다리고 있는 condition2를 깨워줍니다.
              System.out.println("Tasks:" + tasks.toString());
          } finally {
              lock.unlock(); // 임계영역 끝
          }
      }

정리

이번시간에는 Thread 클래스의 활용법과 정확히 알지 못했던 개념들을 정리할 수 있는 시간이였다. 무엇보다 코드에 적용하는 연습을 해서 얻는게 많았던 시간이였다. 다음 시간에는 람다, 스트림, Optional 에 대해 알아본다.

profile
이유를 찾아보자

0개의 댓글