스터디 포스트 >  자바 조금 더 알기

자바의 스레드와 동시성 소개

박두원 멘토
백엔드 개발자이고, 기술 공부를 좋아합니다.

자바 조금 더 알기 - 3주차

 
💡
자바의 함수형 프로그래밍에 대해 이해하고, 사용하는 법을 익힌다.
 

스레드

스레드 : 프로세스가 할당받은 자원을 이용하는 실행의 단위
멀티 스레드 : 하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행하는 것

스레드를 생성하는 3가지 방법

1. extends Thread

public class ExtendsThread extends Thread {  // Thread 자체가 생성
  public void run() {
    // 수행시킬 로직
  }
}

--------------------------------------------------------------------------------

// 실행
Thread extendsThread = new ExtendsThread();
extendsThread.start();
 

2. implements Runnable

class ImplementsRunnable implements Runnable { // Thread가 수행할 Runnable
  @Override
  public void run() {
    System.out.println("하이");
  }
}

--------------------------------------------------------------------------------

// 실행 
Runnable implementsRunnable = new ImplementsRunnable();
Thread thread2 = new Thread(implementsRunnable); // Thread에 Runnable 객체를 넣어줘야 한다.
thread2.start();
 

3. Runnable 람다 함수 사용

Thread thread3 = new Thread(() -> System.out.println("thread3")); // 클래스 생성할 필요 없음
thread3.start();
 

tips

  • thread.run()은 동기로 동작하므로, 병렬로 실행시키고 싶으면 thread.start()를 실행해야 한다.
 

스레드 상태

  • NEW : 스레드 객체가 생성, 아직 start() 메소드가 호출되지 않은 상태
  • RUNNABLE : 실행 상태로 언제든지 갈 수 있는 상태. 다른 스레드가 우선적으로 실행되고 있는 상태
  • RUNNING : cpu를 점유하고 있는 상태. 스레드의 작업을 수행한다.
  • BLOCKED/WAITING : 외부 자원을 기다리고 있는 상태거나, 실행이 완료되지 않은 다른 스레드로부터 데이터를 받아야하는 상태
  • TERMINATED/DEAD : 실행이 완료된 상태
 

스레드 우선 순위

Thread newThread = new Thread();
newThread.setPriority(3);

---------------- Thread class ----------------
public static final intMIN_PRIORITY = 1;
public static final intNORM_PRIORITY = 5;  // default priority
public static final intMAX_PRIORITY = 10;
  • setPriority로 우선순위 설정(제안) 가능하다. (숫자가 높을수록 높은 우선 순위를 제안할 수 있다.)
  • 우선 순위를 설정했다고 항상 해당 우선순위가 되진 않고, 제안 정도의 의미이다. (힌트의 느낌)
  • 우선 순위가 더 높다고하여 우선 순위가 높은 작업이 완전히 끝난 뒤에야 다음 작업을 진행하는 것은 아니다.
 

스레드의 제어

join

지정한 스레드의 작업이 모두 완료(die)될 때까지 기다린다.
Thread thread1 = new Thread();
thread1.start();

thread1.join();  // thread1이 모든 작업을 끝낼 때까지 대기한다.

Thread thread2 = new Thread(); // thread1의 작업이 모두 끝난 후에 작업 수행
thread2.start();
 

sleep & yield

Thread.sleep(1000); // 숫자는 milli seconds 단위로, 1초간 잠자기가 된다.
Thread.yield(); // 현재의 스레드가 이용가능한 이 상태를 양보하거나 양도하겠다는 의미
  • yield()는 스케줄러에 대한 힌트로, 반드시 수행된다는 보장은 없다. (힌트 정도의 역할)
 

synchronized 키워드

오직 하나의 thread만이 해당 메서드를 수행할 수 있다.
// synchronized 메서드
public synchronized int size() {
    return count;
}
  • synchronized 메서드 안의 코드 중 한 줄이라도 어떤 스레드가 수행하고 있다면, 다른 스레드는 해당 메서드를 수행하지 못하고 대기한다. → 단, 하나의 스레드만 접근 가능하기 때문에 오버헤드가 발생할 가능성이 높다.
  • 특정 자원을 여러 스레드가 동시에 접근 가능할 때 사용가능하다.
 

Executor Service

execute 메서드

Thread thread1 = new Thread();
Thread thread2 = new Thread();

ExecutorService singleExecutor = Executors.newSingleThreadExecutor(); //싱글 스레드로 작업 수행
singleExecutor.execute(thread1); // 먼저 수행
singleExecutor.execute(thread2); // thread1 작업이 끝난 후에 수행된다. (executorService가 싱글 스레드이므로)

ExecutorService multiExecutor = Executors.newFixedThreadPool(2); // thread 2개 할당
multiExecutor.execute(thread1);
multiExecutor.execute(thread2); // thread1과 thread2가 함께 수행된다.
  • newFixedThreadPool에 지정된 숫자에 따라 스레드 갯수가 할당된다.
  • 스레드를 5개 할당했다고 하면 동시에 5개의 작업이 수행되고, 각 작업이 각 스레드에서 종료되면 대기중인 다음 작업을 수행한다. → 즉, 동시에 수행되는 작업수는 5개까지이며, 다음 작업을 하려면 각 스레드에서 수행되고 있던 작업이 끝나야한다.
 

submit 메서드

class CallableTask implements Callable<String> {  // callback 받을 내용이 있으므로, Callable 클래스 implements 

  @Override
  public String call() throws Exception { // 위의 Callable<??> 제네릭 타입에 따라 return type이 바뀐다.
    Thread.sleep(1000);
    return "hello";
  }
}

--------------------------------------------------------------------------------

// 실행
Future<String> future = executorService.submit(new CallableTask());
String result = future.get(); // 여기서 future의 작업이 끝날 때까지 기다린다.
System.out.println("job finish"); // future의 작업이 모두 끝난 후에 출력된다.
  • Future는 특정 시점에서 결과가 나올 것을 약속하는, 계약의 개념이다.
  • Runnable은 return type이 void인 반면, Callable은 지정한 반환하려는 타입을 return 해준다.
  • future.get()을 하면, future의 작업이 끝날 때까지 기다린다.
 

invokeAll 메서드

ExecutorService executorService = Executors.newFixedThreadPool(1);

// 하나의 CallableTask마다 3초의 작업 시간이 걸린다고 가정한다.
List<CallableTask> tasks = List.of(new CallableTask(), new CallableTask(), new CallableTask());

List<Future<String>> futures = executorService.invokeAll(tasks); // 모든 Callable 작업을 마칠 때까지 기다린다.

for (Future<String> future : futures) {
  System.out.println(future.get()); // future.get()할 때마다 하나씩 작업을 수행할 것 같지만, 그렇지 않다.
}
  • invokeAll 메서드는 모든 Callable 작업을 마칠 때까지 기다린다. (전달된 작업이 전부 끝날 때까지 holding)
    • executorService에 스레드를 1개만 할당했으면 총 9초의 작업 시간이 걸리고, 9초 후에 모든 결과를 한번에 반환한다.
    • 만약 executorService에 스레드를 3개 할당했으면 각 Callable 작업이 스레드를 하나씩 점유할 수 있으므로, 모든 Callable 작업이 병렬로 수행되어 총 3초의 작업시간이 소요된다.
  • future.get()할 때마다 하나씩 작업을 수행할 것 같지만, 그렇지 않다.
  • invokeAll(tasks, long timeout, TimeUnit unit) 을 사용하여 지정된 timeout 까지 완료되지 못한 작업은 취소 처리 시킬 수도 있다.
 

invokeAny 메서드

ExecutorService executorService = Executors.newFixedThreadPool(1);

List<CallableTask> tasks = List.of(new CallableTask(), new CallableTask(), new CallableTask());

String result = executorService.invokeAny(tasks); // 하나의 결과만 도출된다.
  • 가장 빨리 수행된 작업이 반환되기 때문에 결과는 1개만 있으며, 실행이 첫번째로 끝난 것의 값을 얻고 나머지 작업들은 취소한다.
  • 어떤 작업이 가장 먼저 끝날지는 매 실행마다 다르기 때문에, 어떤 결과가 나올지를 확신하기는 어렵다.
 
[ 참고 자료 ]
 
 
 
본 스터디는 Udemy의 <【한글자막】 완전 초보자를 위한 Java 프로그래밍 : 단기간에 Java 완벽 정복> 강의를 활용해 진행됐습니다. 강의에 대한 자세한 정보는 아래에서 확인하실 수 있습니다.
 
 
프밍 스터디는 Udemy Korea와 함께 합니다.
 
원하는 스터디가 없다면? 다른 스터디 개설 신청하기
누군가 아직 원하는 스터디를 개설하지 않았나요? 여러분이 직접 개설 신청 해 주세요!
이 포스트는
"자바 조금 더 알기" 스터디의 진행 결과입니다
진행중인 스터디
자바 조금 더 알기
자바의 기본적인 문법(변수, if, for 등)을 알고 계신 분들과 함께 자바에 대해 조금 더 공부해보는 스터디를 진행하려고 합니다. 평소 제네릭, 함수형 프로그래밍, 스레드와 동시성, synchronized 등에 대한 궁금증을 가지고 계셨던 분들 함께 공부해요!
박두원 멘토
백엔드 개발자이고, 기술 공부를 좋아합니다.