[자바 스터디 10주차] 멀티쓰레드 프로그래밍

github.com/whiteship/live-study/issues/10

 

10주차 과제: 멀티쓰레드 프로그래밍 · Issue #10 · whiteship/live-study

목표 자바의 멀티쓰레드 프로그래밍에 대해 학습하세요. 학습할 것 (필수) Thread 클래스와 Runnable 인터페이스 쓰레드의 상태 쓰레드의 우선순위 Main 쓰레드 동기화 데드락 마감일시 2021년 1월 23일

github.com

 

목표

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

학습할것

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

Process란?

  • 단순히 실행중인 프로그램
  • 프로그램을 실행하면 운영체제에 의해 메모리 공간을 할당 받아 실행 중인 것을 말한다.
  • 이러한 프로세스는 프로그램에 사용되는 데이터메모리(자원) 그리고 쓰레드로 구성이된다.

 

💡 Multi-Process란?

하나 이상의 프로세서가 서로 협력하여 일을 처리하는 것을 말합니다.
프로세스는 메모리 공간을 독립적으로 사용하고 있기 때문에, context switching을 하려고 하면 비용이 비싸고, 프로세스간 커뮤니케이션도 까다롭습니다.

 

💡 Context Switching이란?

멀티 프로세서 환경에서 cpu가 어떤 하나의 프로세스를 실행하고 있는 상태에서 인터럽트 요청에 의해 다음 우선 순위의 프로세스가 실행되어야 할 때 기존 프로세스의 상태 또는 레지스터의 값을 저장하고, cpu가 다음 프로세스를 실행하도록 새로운 프로세스의 상태 또는 레지스터의 값을 교체하는 작업

 

멀티 쓰레드의 오버헤드? (Context Switching)

 

멀티쓰레드 환경은 잘못 자칫 하면 오버헤드를 발생시킬 수도 있다. 결국 컴퓨터도 이전의 작업에 대한 기억을 해야 된다는 것이다.

 

- JPA 공부를 어느정도 끝내고 밥을 먹어야 하는데, 내가 무슨반찬을 먹고 있었지? '아 멸볶 먹고있었지~' 멸치볶음을 마저 먹어야 겠다.

 

사실 이런 과정들이 전부 컴퓨터 한테는 그 전에 하던일을 기억해야 하기 때문에 오버헤드가 발생할 수 있다는 얘기다.

 

※ 병렬 프로그래밍에 관심이 많으면 actor model(Akka)로 짜라(싱크로나이드, STM(Clojure) 방식보다 더 빠름)

www.slideshare.net/whiteship/concurrency-programming

 

Concurrency programming

Concurrent Programming in Java from: NFJS Magazine 2011 March demo by: Keesun Baik(http//whiteship.me) email: whiteship2000@gmail.compresent at: G+…

www.slideshare.net

 

💡 interrupt란?

운영 체제에서 컴퓨터에 예기치 않은 일이 발생하더라도 작동이 중단되지 않고 계속적으로 업무 처리를 할 수 있도록 해 주는 기능.

Thread란?

  • 프로세스 내의 자원을 이용해서 실제로 작업을 수행하는 주체를 의미
  • 모든 프로세스에는 1개 이상의 쓰레드가 존재하여 작업을 수행
  • 두 개 이상의 쓰레드를 가지는 프로세스를 멀티 쓰레드 프로세스라고 한다.
  • 경량 프로세스라고 불리며 가장 작은 실행 단위이다.
💡 Multi-Thread란?

쓰레드는 하나의 프로세스에 메모리 영역을 공유한다. 상대적으로 context switching 비용이 저렴하고 여러 쓰레드 간 통신도 비교적 쉽다.

CPU의 코어가 한 번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수와 일치.
대부분 쓰레드의 수는 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보인다.

따라서, 프로세스의 성능이 단순하게 쓰레드의 개수에 비례하는것은 아니며, 하나의 쓰레드를 가진 프로세스보다 두 개의 쓰레드를 가진 프로세스가 오히려 더 낮은 성능을 보일 수 있다.

 

 

 

💡 동시성(concurrency)과 병렬성(parallelism)

멀티 쓰레드가 실행될 때 이 두가지 중 하나로 실행된다. 이것은 cpu의 코어의 수와도 연관이 있는데,
하나의 코어에서 여러 쓰레드가 실행되는 것을 동시성,
멀티 코어를 사용할 때 각 코어별로 개별 쓰레드가 실행 되는 것을 병렬성이라고 한다.

만약 코어의 수가 쓰레드의 수보다 많다면, 병렬성으로 쓰레드를 실행하면 되는데
코어의 수보다 쓰레드의 수가 더 많을 경우 동시성을 고려하지 않을 수 없다.

동시성을 고려 한다는 것은, 하나의 코어에서 여러 쓰레드를 실행할 때 병렬로 실행하는 것처럼 보이지만
사실 병렬로 처리하지 못하고 한 순간에는 하나의 쓰레드만 처리할 수 있어서 번갈아 가면서 처리하게 되는데
그 번갈아 가면서 수행하느게 워낙 빠르기 때문에 각자 병렬로 실행 되는 것처럼 보일 뿐이다.

https://catch-me-java.tistory.com/47

 

멀티 쓰레드

https://wisdom-and-record.tistory.com/48

그림만 보면, 멀티쓰레드로 코딩을 해도 시간이 줄어들지 않음

하지만 시스템 리소스가 조금 있고, A와 B가 코어가 다른경우 시간이 줄어듬

 

※ 시스템 리소스를 극한으로 끌어내는 코딩 방법

/**
 * 스레드풀에 스레드를 많이 만들어 봤자
 * 하드웨어가 지원하는 CPU의 개수보다는 더 많이 돌릴 수 없음
 * 그래서 스레드풀의 스레드에는 CPU 개수만큼만 만들기
*/
ExecutorService service = Executors.newFixedThreadPool(8);

// 15개의 스레드를 관리하는 latch
CountDownLatch latch = new CountDownLatch(15);

...

for(int index = 1; index <= 15; index++){
	service.execute(new Runnable(){
    	@Override
        public void run(){ 
        	//비즈니스 로직
            latch.countDown(); //latch 하나씩 내려주기, 이걸 하지 않으면 작업이 안끝남
        }
    });
}

...

latch.await(); //모든 latch가 다끝날때까지 기다리기
service.shutdown(); // 스레드 풀 종료
  • 기선님 깃허브 대시보드 코드 일부
  • 요청이 실패하면 해당 스레드만 실패하는 것, 다 실패하는게 아님
  • 50초 -> 9초

 

Java에서의 쓰레드

일부 쓰레드에서 실행중인 코드가 새로운 Thread 객체를 생성할 때 새로운 쓰레드는 자신을 생성한 쓰레드의 우선 순위와 동일하게 설정된 우선 순위를 가지며 생성 쓰레드가 데몬쓰레드인 경우 데몬 쓰레드가 된다.

JVM이 시작할 때 일반적으로 main이라는 쓰레드가 있다.

JVM은 다음 중 하나가 발생할때 까지 쓰레드를 유지한다.

  • Runtime 클래스의 종료 메소드가 호출되었으며, 보안관리자가 종료 조작이 발생하도록 허용할 때
  • 데몬 쓰레드가 아닌 모든 쓰레드는 실행된 후 run() 메소드의 작업이 끝나거나 run 메소드 이외에서 예외를 throw 했을 때 종료

모든 쓰레드에는 식별 목적으로 이름이 있다.

둘 이상의 쓰레드가 동일한 이름을 가질 수 있다.

쓰레드가 생성될 때 이름이 지정되지 않은 경우 새 이름이 생성된다.

("Thread-숫자" 형식으로 생성된다. 숫자는 0부터 시작해서 생성할 때 마다 1씩 증가한다.)

 

Thread 클래스와 Runnable 인터페이스

 

Thread

public class ThreadEx1 extends Thread {

    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        ThreadEx1 th = new ThreadEx1();
        th.start();
    }

}

 

Runnable

    public class ThreadEx1 {
    	public static void main(String[] args) {
        new Thread(() -> System.out.println("gd")).start();
        }
    }

 

둘 중 어느걸 사용해야 할까?

Thread를 상속받아서 구현할 수 있는 메소드

  • run() 메소드 말고도 다른 메소드를 오버라이딩 해야 할 필요가 있을 때 Thread를 상속받는다.
  • 아니라면, Runnable 인터페이스를 사용한다. 왜냐하면 run() 메소드밖에 존재하지 않기 때문이다.
  • 혹은 다른 클래스를 상속해야될 때 Runnable인터페이스를 사용한다.

run() 메소드와 start()메소드

구현과 실행에 관련된 run() 메소드와 start() 메소드

public void run() : 쓰레드가 실행되면 run() 메소드를 호출하여 작업을 한다.

public synchronized void start() : 쓰레드를 실행시키는 메소드. start 메소드가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행 대기 상태에 있다가 자신의 차례가 되어야 실행.

 

run() 메소드와 start()메소드의 차이점

쓰레드를 시작할 때 run() 메소드를 호출하면 되는 것 같은데, 실제로는 start() 메소드를 호출해서 쓰레드를 실행한다. main메소드에서 run() 메소드를 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 메소드를 호출하는 것이다. 반면에 start() 메소드를 호출하면 새로운 쓰레드가 작업을 실행하는데 필요한 새로운 호출 스택(call stack)을 생성한 다음에 run()을 호출한다. 즉, 새로 생성된 호출 스택에 run()이 첫 번째로 올라가게 한다. run() 메소드의 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서 생성된 호출 스택도 소멸된다.

 

아래는 각각 main메소드에서 run메소드를 호출했을 때와 main 메소드에서 start 메소드를 호출했을 때를 그림으로 나타낸 예제다.

 

한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 뜻이다. 하나의 쓰레드 객체에 대해 start() 메소드를 두 번 이상 호출하면 실행시에 IllegalThreadStateException이 발생한다.

package com.soap;

public class RunThreads {
    public static void main(String[] args) {
        runBasic();
    }

    public static void runBasic() {
        ThreadSample thread = new ThreadSample();
        thread.start();
        thread.start();
    }
}

다음과 같이 start 메소드를 두번 호출하는 경우는 정상적으로 실행이된다.
첫번째 쓰레드를 실행 한 뒤 또 다른 새로운 쓰레드를 생성해서 실행하기 때문이다.

MyThread_1 th1 = new MyThread_1();
th1.start()
th1 = new MyThread_1();
th1.start()

 

※ 한 쓰레드에서 예외가 발생해서 종료하더라도 다른 쓰레드의 실행에는 영향을 미치지 않는다.

 

 

Runnable 인터페이스

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 인터페이스는 함수형 인터페이스로 run() 추상메소드 하나만이 존재한다.

구현하는 클래스에서 run() 메소드를 구현하는 걸로 쓰레드에게 작업할 내용을 설정할 수 있다.

 

 

https://sujl95.tistory.com/63

ThreadSample.java

public class ThreadSample extends Thread{
    @Override
    public void run(){
        System.out.println("This is ThreadSample's run() method.");
    }
}

RunnableSample.java

public class RunnableSample implements Runnable{
    @Override
    public void run() {
        System.out.println("This is RunnableSample's run() method.");
    }
}

RunThreads.java

public class RunThreads {
    public static void main(String[] args) {
        runBasic();
    }

    public static void runBasic() {
        RunnableSample runnable = new RunnableSample();
        new Thread(runnable).start();
        ThreadSample thread = new ThreadSample();
        thread.start(); //이것이 끝날때까지 runBasic()은 기다리지 않는다.
        System.out.println("RunThreads.runBasic() method is ended.");
    }
}

결과

This is RunnableSample's run() method.
RunThreads.runBasic() method is ended.
This is ThreadSample's run() method.

thread.start() 메소드가 끝날 때까지 기다리지 않고, 그 다음 줄에 있는 thread라는 객체의 start() 메소드를 실행한다. 새로운 쓰레드를 시작하므로 run() 메소드가 종료될 때까지 기다리지 않고 다음 줄로 넘어가게 된다.

 

 

Thread는 순서대로 동작할까?

public class RunMultiThreads {
    public static void main(String[] args) {
        runMultiThread();
    }
    
    public static void runMultiThread(){
        RunnableSample[] runnable = new RunnableSample[5];
        ThreadSample[] thread = new ThreadSample[5];
        for(int loop = 0; loop < 5; loop++){
            runnable[loop] = new RunnableSample();
            thread[loop] = new ThreadSample();
            
            new Thread(runnable[loop]).start();
            thread[loop].start();
        }
        System.out.println("RunMultiThreads.runMultiThread() method is ended");
    }
}

 

  • 순서대로 실행하지 않는다. 실행결과는 컴퓨터의 성능에 따라 매번 달라질 수도 있다.
  • run() 메소드가 끝나지 않으면 애플리케이션은 종료되지 않는다.

Thread sleep메소드

  • sleep 메소드는 주어진 시간 만큼 대기를 하게 된다.

public class EndlessThread extends Thread {
    public void run() {
        while (true) {
            try {
                System.out.println(System.currentTimeMillis());
                Thread.sleep(1_000L); //L을 쓰는게 좋은 습관
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

이 예제는 무한으로 실행되니 직접 실행을 중지해줘야 한다. Thread.sleep() 메소드를 사용할 때는 항상 try-catch로 묶어줘야 한다. 적어도 InterruptedException으로 예외 처리를 해줘야 한다. 왜냐하면 sleep() 메소드는 InterruptedException 예외를 던질 수도 있다고 선언되어 있기 때문이다.

run() 메소드 사용시 반드시 try-catch를 해줘야 하는데, 개발자가 catch()문 안에 실행할 로직이 따로 없다. 그냥 RuntimeException(e)로 던져주는것 말고는... 또한, try-catch문을 쓰기 시작하면 코드가 굉장히 더러워지므로 lombok의 @SneakyThrows를 이용하면 코드를 깨끗하게 작성할 수 있다.

 

 

 

 

쓰레드의 상태

Thread의 실행시점은 .start() 메소드로 실행을 하는 것처럼 보이지만 실제로는 그렇지 않다. start() 메소드를 실행하게 되면, 스레드의 실행 대기 상태가 되고, 스케줄러가 스레드를 실행시키는 방식으로 되어있다.

1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 합니다. (실행 대기열은 큐와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행됩니다.)

2. 자기 차례가 되면 실행상태가 됩니다.

3. 할당된 실행시간이 다되거나 yield()메소드를 만나면 다시 실행 대기상태가 되고 다음 쓰레드가 실행됩니다.

4. 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있습니다.

※ I/O Block은 입출력 작업에서 발생하는 지연상태를 말합니다. 사용자의 입력을 받는 경우를 예로 들 수 있습니다.)

5. 지정된 일시정지시간이 다되거나, notify(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행 대기열에 저장되어 자신의 차례를 기다리게 됩니다.

6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드를 소멸됩ㄴ디ㅏ.

 

 

쓰레드의 상태와 스케줄링에 관련된 메소드들

sleep()

  • public static void sleep(long millis) : 지정된 시간동안 쓰레드를 멈추게 한다. (millis, 1/1000초 단위)
  • public static void sleep(long millis, int nanos) : (millis, 1/1000초 단위, nanos 1/1000000000초 단위 == 10억분의 1초)

  - 밀리세컨드와 나노세컨드의 시간단위로 세밀하게 값을 지정할 수 있지만 어느 정도의 오차가 발생할 수 있다.

  -  sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 디거나 interrupt()가 호출되면(InterruptedException 발생), 잠에서 깨어나 실행대기 상태가 된다.

  - sleep()을 호출 할 때는 항상 try-catch문으로 InterruptedException을 예외처리 해줘야 한다.

  - sleep()은 항상 현재 실행 중인 쓰레드에 대해 작동한다. 때문에 static으로 선언되어 있으며 참조변수를 이용해서 호출하기 보다는 Thread.sleep(2000)과 같이 호출해야 한다.

 

위 예제는 th1과 th2 두 개의 쓰레드를 생성해 th1객체에 sleep()메소드를 호출하여 th1 쓰레드의 작업을 4초간 멈추려고 한 코드입니다. th1을 일시정지 상태로 뒀기 때문에 main보다 th1 쓰레드가 가장 먼저 종료될 것으로 예상했지만 실제로는 main 메소드가 가장 늦게 종료됩니다. th1.sleep(4000)과 같이 호출해도 실제로 영향을 받는 것은 main 메소드를 실행하는 main 쓰레드 입니다.

 

 

interrupt()

  • public void interrupt() : 쓰레드의 interrupted상태를 false에서 true로 변경, 쓰레드에게 작업을 멈추라고 요청합니다. 
  • public boolean isInterrupted() : 쓰레드의 interrupted상태를 반환, interrupted()가 호출되었는지 확인하는데 사용할 수 있지만, interrupted()와 달리 interrupted상태를 false로 초기화하지 않습니다.
  • public static boolean interrupted() : 현재 쓰레드의 interrupted상태를 반환 후, false로 변경. 쓰레드가 sleep(), wait(), join()에 의해 '일시정지 상턔'에 있을 때, 해당 쓰레드에 대해 interrupte()를 호출하면, sleep(), wait(), join()에서 interruptedException이 발생하고 쓰레드는 '실행 대기 상태(RUNNABLE)'로 바뀝니다.

 

 

위 예제는 interrupt()와 isInterrupted()를 사용해서 카운트 다운 도중에 사용자의 입력이 들어오면 카운트 다운을 종료하는 코드입니다. 사용자의 입력이 끝나면 interrupt()에 의해 카운트 다운이 중간에 멈추게 됩니다.

 

 

위 예제는 이전의 코드와 시간 지연을 위한 for문 대신 Thread.sleep(1000)으로 1초 동안 지연하도록 변경한 코드입니다. 사용자가 입력을 완료해도 카운트는 종료 되지 않는데 이것은 Thread.sleep(1000)에서 InterruptedException이 발생했기 때문입니다. sleep()에 의해 멈춰 있을 때 interrupt()를호출하면 InterruptedException이 발생하고 쓰레드의 Interrupted 상태는 false로 자동 초기화 됩니다.

 

 

이런 상황에서 사용자의 입력을 받을 때 카운트다운을 종료하려면 catch 블럭에 interrupt()를 추가로 넣어줘서 쓰레드의 interrupted상태를 true로 다시 바꿔줘야 합니다.

try{
	Thread.sleep(1000);
}catch(InterruptedException e){
	interrupt();
}

catch 블럭에 interrupt()를 추가한 뒤 실행 결과

 

suspend(), resume(), stop()

suspend() : sleep()처럼 쓰레드를 일시정지 합니다.

resume() : suspend()에 의해 일시정지 상태에 있는 쓰레드를 실행대기 상태로 만듭니다.

stop() ; 호출되는 즉시 쓰레드가 종료됩니다.

 

suspend(), resume(), stop()은 쓰레드의 실행을 제어하는 가장 손쉬운 방법이지만, suspend()와 stop()이 교착상태(deadlock)를 일으키기 쉽게 작성되어 있으므로 이 메소드들은 모두 '@Deprecated' (사용 권장 X)이 됬습니다. 

stop()은 사실상 없다고 생각하면 좋다. 즉, 자바가 스레드를 종료시키는 방법은 없다고 생각해라. stop()를 사용한다는 것은 JVM이 직접 스레드를 종료시키는 건데, 이렇게 시스템적인 레벨로 처리를 한다면 스레드가 들고있던 lock이 풀리면서 알수없는 데이터 구조가 생성되는 등 복잡한 문제들이 발생한다.
따라서, 개발자가 스레드가 사용하고 있는 데이터를 확인 후 코딩으로 직접 종료를 해야한다. (상태값을 true -> false)   

 

yield()

쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)합니다.

 

예를 들어 스케쥴러에 의해 1초의 실행시간을 할당받은 스레드가 0.5초의 시간동안 작업한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기 상태가 됩니다.

 

yield()와 interrupt()를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있습니다.

 

아래의 코드는 yield()와 interrupt()를 이용해서 효율성과 응답성을 높인 코드입니다.

만약 suspend()를 호출해서 suspended가 true라면 run() 메소드의 while문을 의미없이 계속 돌게 됩니다.(바쁜 대기상태 / busy-waiting). 이 때 yield 메소드를 else 문에 추가함으로써 남은 시간을 낭비하지 않고 다른 쓰레드에게 차례를 양보합니다.

 

또한 while문 안의 Thread.sleep(1000)에 의해 쓰레드가 일시정지 상태에 머물러 있는 상황이라면 stopped의 값이 true로 바뀌었어도 쓰레드가 정지될 때까지 최대 1초의 시간지연이 생길 수 있습니다. stop()에 th.interrupt()를 추가함으로써 sleep()에서 InterruptedException이 발생하여 즉시 일시정지 상태에서 벗어나게 되므로 응답성이 좋아집니다.

 

package com.soap;


import javax.swing.*;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        MyThread_1 th1 = new MyThread_1("쓰레드1");
        MyThread_1 th2 = new MyThread_1("쓰레드2");
        MyThread_1 th3 = new MyThread_1("쓰레드3");
        th1.start();
        th2.start();
        th3.start();

        try{
            Thread.sleep(2000);
            th1.suspend();
            Thread.sleep(2000);
            th2.suspend();
            Thread.sleep(2000);
            th1.resume();
            Thread.sleep(2000);
            th1.stop();
            th2.stop();
            Thread.sleep(2000);
            th3.stop();
        }catch(InterruptedException e){ }

    }
}

class MyThread_1 implements Runnable{

    boolean suspended = false;
    boolean stopped = false;

    Thread th;

    MyThread_1(String name) {
        th = new Thread(this, name);
    }

    @Override
    public void run() {
        String name = th.getName();

        while(!stopped){
            if(!suspended){
                System.out.println(name);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    System.out.println(name + " - interrupted");
                }
            }else{
                Thread.yield(); //suspended가 true일 때 쓰레드의 남은시간을 양보해줌
            } //만약 Thread.yield()가 없다면 쓰레드는 남은 시간을 아무런 일도 하지 않는 while문을 돌며 낭비하게됨
        }
        System.out.println(name + " - stopped");
    }

    public void suspend(){
        suspended = true;
        th.interrupt();
        System.out.println(th.getName() + " - interrupt() by suspend()");
    }

    public void stop(){
        stopped = true;
        th.interrupt();
        // stop()에 th.interrupt() 를 추가함으로써 sleep()에서
        // InterruptedException이 발생하여 즉시 일시정지 상태에서 벗어나게 되므로
        // 응답성이 좋아집니다.
        System.out.println(th.getName() + " - interrupt() by stop()");
    }

    public void resume() { suspended = false; }
    public void start() { th.start(); }
}

 

join()

쓰레드는 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용합니다.

public void join()
public void join(long millis)
public void join(long millis, long nanos)

 

시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다립니다.

작업중에 다른 쓰레드의 작업이 먼저 수행되어야 할 필요가 있을 때 join()을 사용합니다.

 

join 메소드도 sleep 메소드처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출 되는 부분을 try-catch문으로 감싸서 interruptedException을 catch 해야합니다.

 

sleep()과 다른점
join()은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static메소드가 아니다.

아래의 코드는 JVM의 가비지 컬렉터를 흉내 내어 간단히 구현한 것입니다. 최대 메모리가 1000인 상태에서 사용된 메모리가 60%를 초과한 경우 gc 쓰레드가 깨어나서 메모리를 비우는 작업을 합니다. 이 때 만약 join이 없는 상태로 한다면 main쓰레드는 계속해서 메모리를 늘려가고 최악의 경우 1000이 넘었는데도 계속 해서 메모리를 쌓을 수 있습니다. 이때 gc.join()을 사용해서 gc가 작업할 시간을 주고 main 쓰레드는 지정된 시간동안 대기하는 것이 필요합니다.

 

package com.soap;


import jdk.jfr.Frequency;

import javax.swing.*;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        MyThread_1 gc = new MyThread_1();
        gc.setDaemon(true);
        gc.start();

        int requiredMemory = 0;

        for(int i = 0; i < 20; i++){
            requiredMemory = (int)(Math.random() * 10) * 20;

            //필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리의 60%이상을
            //사용했을 경우 gc를 깨운다.
            if(gc.freeMemory() < requiredMemory || gc.freeMemory() < gc.totalMemory() * 0.4) {
                gc.interrupt(); //잠자고 있는 gc를 깨운다.
                try{
                    gc.join(100); //join()을 호출해서 gc가 작업할 시간을 주고 main쓰레드는 기다리낟.
                }catch(InterruptedException e) {}
            }
            gc.usedMemory += requiredMemory;
            System.out.println("usedMemory:" + gc.usedMemory);
        }


    }
}

class MyThread_1 extends Thread{

    final static int MAX_MEMORY = 1000;
    int usedMemory = 0;

    @Override
    public void run() {
        while(true){
            try{
                Thread.sleep(1000 * 10);
            }catch(InterruptedException e) {
                System.out.println("Awaken by interrupt().");
            }

            gc(); //garbage collection을 수행
            System.out.println("Garbage Collected. Free Memory : " + freeMemory());
        }
    }

    public void gc(){
        usedMemory -= 300;
        if( usedMemory < 0) usedMemory = 0;
    }

    public int totalMemory() { return MAX_MEMORY; }
    public int freeMemory() { return MAX_MEMORY - usedMemory; }
}

 

Object 클래스에 선언된 쓰레드와 관련된 메소드들

https://sujl95.tistory.com/63

public class StateThread extends Thread{
    private Object monitor;

    public StateThread(Object monitor){
        this.monitor = monitor;
    }

    public void run(){
        try{
            for(int loop = 0; loop < 1000; loop++){
                String a = "A";
            }
            synchronized (monitor){
                monitor.wait();
            }
            System.out.println(getName() + "is notified.");
            Thread.sleep(1000);
        }catch(InterruptedException ie){
            ie.printStackTrace();
        }
    }

}

 

위의 코드와 같이 직접 멀티 스레드를 코딩하면 개발이 매우 힘들었을 것이다. 
이러한 멀티 스레드 코딩은 다 서블릿 컨테이너에 들어가 있다. (톰캣, 제티, 네티, 언더토우 등)

스레드는 서버의 리소스를 극한으로 활용할 때 사용한다. 하지만 이것두 서블릿 컨테이너들이 다 해준다.
@RestController
public class HelloController{
	
    @GetMapping("/hello")
    public String hello(){
    	return "Hello Spring";
    }
}

우리는 위와 같이 스프링 MVC를 사용한다.

".../hello" 라는 요청이 들어올 때 마다 hello() 메소드가 실행이 된다. 만약 여러 요청이 들어온다면 서버가 가지고 있는 모든 리소스와 스레드를  활용해서 실행을 해준다. 다시 말해서, 개발자가 멀티스레딩 코딩을 하지 않았지만, 서블릿 컨테이너에 의해 멀티스레드 코딩으로 돌아간다.

 

그런데, 우리가 만든 코드가 서블릿 컨테이너에서 동작하지 않는다면? 단적인 예로 백기선님의 Github 대시보드 코드는 단일 스레드로 구현이 되었기 때문에 속도가 매우 느리다.

이를 멀티스레드 코딩으로 한다면 서버의 리소스를 효율적으로 사용 및 동시에 여러 CPU 까지 돌리게 되므로, 속도가 빨라진다.

 

관련 경험을 쌓아서 면접에서 말하면 좋을듯!!

 

쓰레드의 우선순위

 

쓰레드 스케쥴링은 우선순위 방식과 순환할당 방식이 있다.

 

💡 순환 할당이란?

각자 시간 할당량을 정해서 하나의 쓰레드를 정해진 시간만큼만 실행하고 다시 다른 쓰레드를 실행하는 것을 말하는데, 이런 순환 할당 방식은 JVM이 정하기 때문에 코드로 제어할 수는 없다.

Java에서 각 쓰레드는 우선순위(Priority)에 관한 자신만의 필드를 가지고 있다. 이러한 우선순위에 따라 특정 쓰레드가 더 많은 시간동안 작업을 할 수 있도록 설정한다.

getPriority()와 setPriority() 메소드를 통해 쓰레드의 우선순위를 반환하거나 변경할 수 있다. 쓰레드의 우선순위가 가질 수 있는 범위는 1부터 10까지이며, 숫자가 높을 수록 우선순위 또한 높아 진다.

 

하지만 쓰레드의 우선순위는 비례적인 절댓값이 아닌 어디까지나 상대적인 값일 뿐이다. 우선순위가 10인 쓰레드가 우선순위가 1인 쓰레드보다 더 빨리 수행되는 것이 아니다. 단지 우선순위가 10인 쓰레드는 우선순위가 1인 쓰레드보다 좀 더 많이 실행 큐에 포함되어, 좀 더 많은 작업 시간을 할당받을 뿐이다.

 

※main 메소드를 수행하는 쓰레드는 우선순위가 5이므로 main메소드내에서 생성하는 쓰레드의 우선순위는 기본적으로 5가 된다.

 

public static void test2() {
    PrimeThread1 p1 = new PrimeThread1(143);
    PrimeThread2 p2 = new PrimeThread2(143);
    p1.start();
    p2.start();

}

static class PrimeThread1 extends Thread {
    long minPrime;

    PrimeThread1(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.print("1");
        }
    }
}

static class PrimeThread2 extends Thread {
    long minPrime;

    PrimeThread2(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.print("2");
        }
    }
}

같은 우선순위를 가지는 쓰레드를 실행시키는 경우 번갈아가면서 실행함을 확인할 수 있다. 원래 소스코드 실행은 순차적으로 실행되지만 쓰레드의 경우 순차적으로 실행하는 것이 아닌 번갈아 가면서 실행되는 것을 볼 수 있다.

public static void test3() {
    PrimeThread t1 = new PrimeThread("1st", 1); 
    PrimeThread t2 = new PrimeThread("2nd", 1); 
    PrimeThread t3 = new PrimeThread("3rd", 1); 
    t1.setPriority(Thread.MAX_PRIORITY);//max - 10
    t2.setPriority(Thread.NORM_PRIORITY);//norm - 5
    t3.setPriority(Thread.MIN_PRIORITY);// min - 1
    t1.start();
    t2.start();
    t3.start();
}

하지만 우선순위를 부여하는 경우 상대적으로 우선순위가 높은 쓰레드가 좀더 빠르게 실행된다.

출력 결과를 보면 순서가 정확히 일치하지는 않는것을 볼 수 있는데, 우선순위가 높다고 해서 무조건 먼저 실행되는 것이 아니라 실행 기회를 더 많이 가지는 것이기 때문에 정확히 일치하지는 않는다.

 

또한, 쿼드코어에선 4개의 쓰레드가 병렬성으로 실행될 수 있기 때문에 4개 이하의 쓰레드에서는 크게 영향이 없고 5개 이상일 때 의미가 생긴다.

public static void test4() {
    PrimeThread[] threads = new PrimeThread[20];
    for (int i = 0; i < 20; i++) {
        threads[i] = new PrimeThread(i+1+"", 1);
        if(i <= 10) threads[i].setPriority(1);
        else threads[i].setPriority(10);
    }
    for (int i = 0; i < 20; i++) {
        threads[i].start();
    }
}

static class PrimeThread extends Thread {
    long minPrime;
    String text;

    PrimeThread(String text, long minPrime) {
        this.minPrime = minPrime;
        this.text = text;
    }

    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        for (long i = 0; i < 2000000000; i++) {
        }
        System.out.print(text + " ");
    }
}

이 실행결과는 10코어의 컴퓨터에서 1-10쓰레드까지 우선순위 1을 주고 11-20번 쓰레드까지 10의 우선순위를 부여해 실행한 결과이다. 결과를 보면 11-20번 쓰레드가 먼저 실행된 것을 확인할 수 있다.

 

Main 쓰레드

 

Java는 실행 환경인 JVM에서 돌아가게 된다. 이것이 하나의 프로세스이고 Java를 실행하기 위해 우리는 main() 메소드가 메인 쓰레드이다. 

따로 쓰레드를 실행하지 않고 main() 메소드만 실행하는 것을 싱글쓰레드 애플리케이션이라고 한다.

 

오른쪽 그림과 같이 메인 쓰레드에서 쓰레드를 생성하여 실행하는 것을 멀티 쓰레드 애플리케이션이라고 한다. 

싱글 쓰레드 애플리케이션에서는 메인쓰레드가 종료되면 프로세스도 종료되지만 멀티 쓰레드 애플리케이션은 실행중인 쓰레드가 하나라도 있다면 종료되지 않는다.

 

※ Main 쓰레드의 참조를 얻을 수 있습니다.

public static void main(String[] args) {
	Thread mainThread = Thread.currentThread(); 	        
    System.out.println(mainThread.getName());
}

 

※ Main 쓰레드에서 join()을 하면 데드락에 걸린다.

package com.soap;

public class Main {
    public static void main(String[] args) {

        try {
            Thread.currentThread().join();
            // the following statement will never execute because main thread join itself
            System.out.println("This statement will never execute");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

Daemon Thread

  • Main 쓰레드의 작업을 돕는 보조적인 역할을 하는 쓰레드이다.
  • Main 쓰레드가 종료되면 데몬 쓰레드는 강제적으로 자동 종료가 된다.(어디까지나 Main 쓰레드의 보족 역할을 수행하기 때문에, Main 쓰레드가 없어지면 의미가 없어지기 때문이다.)
  • Java의 Garbage Collector가 데몬 쓰레드입니다. (워드프로세스의 자동저장, 화면자동갱신 등이 있습니다.)

크롬이라는 메인 메소드가 실행이 되면서, 유튜브의 백그라운드 재생, google docs의 문서 자동저장 등을 데몬 쓰레드라고 볼 수 있다.

 

Daemon Thread Example

package com.soap;

public class Main {
    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 100; i++){
                    System.out.println("유튜브 영상 실행중");
                    try{
                        Thread.sleep(500);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.setDaemon(true); //데몬 쓰레드 설정
        thread.start();
        System.out.println("메인 메소드 종료");
    }
}

메인 메소드가 종료되면서 유튜브 영상 실행중이라는 코드 하나를 출력하고 종료한다.

 

만약 thread.setDaemon(false)로 하면 지속적으로 유튜브 영상 실행중이 100개가 출력된다.

 

 

동기화

 

싱글쓰레드 프로세스의 경우 프로세스 내에서 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는데 별 문제는 없습니다.

 

멀티 쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 됩니다. 아래는 두개의 쓰레드에서 공유 자원인 예금을 사용해서 입금하는 작업을 할 때 발생하는 문제입니다.

예금이 10만원인 통장에서 두 쓰레드가 각각 접근해서 10만원, 5만원을 입금해서 원래라면 35만원이 됐어야 하는데 예금이 최종적으로 저장된 예금은 20만원이 되는 큰일이 생겼습니다.

 

이렇게 공유 자원 접근 순서에 따라 실행 결과가 달라지는 프로그램의 영역을 임계구역(Critical section)이라고 합니다.

 

임계구역 해결 조건

  • 상호 배제(mutual exclusion) : 한 쓰레드가 임계구역에 들어가면 다른 쓰레드는 임계구역에 들어갈 수 없습니다. 이것이 지켜지지 않으면 임계구역을 설정한 의미가 없습니다.
  • 한정 대기(bounded waiting) : 한 쓰레드가 계속 자원을 사용하고 있어 다른 쓰레드가 사용하지 못한 채 계속 기다리면 안됩니다. 어떤 쓰레드도 무한 대기하지 않아야 합니다. 즉 특정 쓰레드가 임계구역에 진입하지 못하면 안됩니다.
  • 진행의 융통성(progress flexibility) : 한 쓰레드가 다른 쓰레드의 작업을 방해해서는 안됩니다.
우리는 임계 구역(critical section)과 잠금(lock)의 개념을 활용해서 한 쓰레드가 특정 작업을 마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 수행할 수 있게 됩니다.

 

공유 데이터를 사용하는 코드 영역을 임계구역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 합니다. 그리고 해당 쓰레드가 임계 구역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계구역의 코드를 수행할 수 있게 됩니다.

 

-> 마치 공공 장소의 화장실을 사용할 때 문을 잠그고 들어가서 일을 본 뒤 화장실 문을 열고 다음사람에게 차례를 넘겨주는 것을 떠올리면 lock에 대한 이해가 쉽습니다.

 

이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화(synchronization)라고 합니다. 쓰레드에 안전하다고 표현합니다(thread-safe)

자바에서는 synchronized블럭을 이용해서 쓰레드의 동기화를 지원했지만, JDK1.5부터는 'java.util.concurrent.locks'와 'java.util.concurrent.atomic'패키지를 통해서 다양한 방식으로 동기화를 구현할 수 있도록 지원하고 있습니다.
💡 기아(starvation) 상태란?

어떤 쓰레드가 다른 쓰레드들이 CPU시간을 모두 잡고 있어 CPU시간을 사용할 수 없게 되는 현상을 "기아 상태"라고 한다. 
+ lock을 얻지못하고 오랫동안 기다리게 되는 현상

 

💡 Race Condition이란?

여러 쓰레드가 lock을 얻기 위해 경쟁하는 것을 '경쟁 상태(race confition)' 라고 합니다.
Ex) 공유하는 자원에 어떤 순서로 접근하냐에 따라 결과가 달라질 수도 있다. 이러한 현상의 원인을
"race condition 때문에 발생했다" 라고 말하기도 한다.

 

💡 Critical Path란?

동시에 실행하는 작업들 중 가장 긴 작업시간, 전체 수행시간을 줄이기 위해 우선적으로 개선할 부분

흰색으로 표시한게 Critical Path

  • 왼쪽의 Critical Path를 변경하면 오른쪽처럼 되는데 그럼 Critical Path가 변경이됨

 

 

자바에서 동기화 하는 방법은 3가지로 분류된다

  • Synchronized 키워드
  • Atomic 클래스
  • Volatile 키워드

synchronized를 이용한 동기화

가장 간단한 동기화 방법으로 synchronized키워드를 이용해서 임계구역을 설정하는 방법입니다.

  • 메소드 전체를 임계구역으로 설정
    • 메소드가 호출된 시점부터 lock을 얻어서 작업하고 메소드가 종료되면 lock을 반환합니다.
public synchronized void calcSum(){
   ...
}
  • 특정한 영역을 임계구역으로 지정
    • 참조변수는 lock을 걸고자 하는 객체를 지정해주며 이 블럭의 영역 안으로 들어가면서 부터 쓰레드는 지정된 객체의 lock을 얻게 되고, 이 블럭을 벗어나면 lock을 반납합니다.
synchronized ( 객체의 참조변수) {
   ...
}

두 가지 방법 모두 lock의 획득과 반납이 자동적으로 이루어지므로 우리가 해야 할 일은 그저 임계구역만 지정해주는 것입니다.

 

임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메소드 전체에 락을 거는 것보다 synchronized 블럭으로 임계구역을 최소화해서 보다 효율적인 프로그램이 되도록 노력해야 한다.

 

아래의 예제는 동기화 하지 않은 코드입니다. 실행 결과를 보면 통장의 잔고가 음수가 되는것을 확인할 수 있습니다. 이런 결과의 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문입니다.

package com.soap;

public class ThreadExample {
    public static void main(String[] args) {
        Runnable r = new MyThread_1();
        new Thread(r).start();
        new Thread(r).start();
    }
}


class Account{
    private int balance = 1000;

    public int getBalance(){
        return balance;
    }

    public void withdraw(int money){
        if(balance >= money){
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){

            }
            balance -= money;
        }
    }
}

class MyThread_1 implements Runnable {
    Account acc = new Account();

    @Override
    public void run() {
        while(acc.getBalance() > 0){
            //100, 200, 300 중의 한 값을 임의로 선택해서 출금(withdraw)
            int money =(int)(Math.random() * 3 + 1) * 100;
            acc.withdraw(money);
            System.out.println("balance:" + acc.getBalance());
        }
    }
}

위의 코드에서 문제를 해결하는것은 간단합니다. withdraw() 메소드에 synchronized 키워드를 사용해서 임계구역으로 지정해주면 더이상 음수값이 나타나지 않습니다.

 

synchronized 블록으로 만들어도 같은 결과를 얻을 수 있습니다. 아래는 withdraw 안의 작업을 synchronized 블록으로 감싼 코드입니다.

public void withdraw(int money) {
    synchronized(this) {
        if (balance >= money) {
            try { Thread.sleep(1000); } catch (Exception e) { }
            balace -= money;
        }
    }
}

 

데드락(교착상태)

 

2개 이상의 프로세스가 다른 프로세스의 작업이 끝나기만 기다리며 작업을 더 이상 진행하지 못하는 상태를 교착 상태(dead lock)라고 합니다. 교착 상태의 가장 좋은 예로 사용되는 식사하는 철학자 문제로 설명하겠습니다.

 

이 문제는 철학자들이 둥근 테이블에 앉아 식사를 하는데 각자의 자리의 왼쪽에 있는 젓가락을 잡은 뒤 오른쪽의 젓가락도 잡아야만 식사가 가능하다는 조건이 있습니다. 모든 철학자들은 왼쪽의 젓가락을 잡고 오른쪽을 쳐다보면 왼손에 젓가락을 들고 오른쪽을 쳐다보고 있는 다른 철학자가 보일것입니다. 결국 오른쪽 젓가락을 잡지 못해 모두 굶어 죽게 될것입니다.

 

오른쪽 그림은 식사하는 철학자 문제를 자원 할당 그래프로 나타낸 것입니다. P1~P4는 프로세스(또는 쓰레드)를 나타내고 R1~R4는 공유 자원을 의미합니다.

 

교착 상태가 발생하는 원인

교착 상태가 발생하기 위해서는 아래의 4가지 조건을 만족해야 합니다.

이 4가지 조건을 교착 상태의 필요조건 이라고 합니다.

 

1.상호배제 : 철학자들은 서로 포크를 공유할 수 없습니다.

-> 자원을 공유하지 못하면 교착 상태가 발생합니다. 여기서 자원은 배타적인 자원이어야 합니다. 배타적인 자원은 임계구역에서 보호되기 때문에 다른 프로세스(쓰레드)가 동시에 사용할 수 없습니다.

 

2.비선점 : 각 철학자는 다른 철학자의 포크를 빼앗을 수 없습니다.

-> 자원을 빼앗을 수 없으면 자원을 놓을 때 까지 기다려야 하므로 교착상태가 발생합니다.

 

3.점유와 대기 : 각 철학자는 왼쪽 포크를 잡은 채 오른쪽 포크를 기다립니다.

-> 자원 하나를 잡은 상태에서 다른 자원을 기다리면 교착 상태가 발생합니다.

 

4.원형 대기 : 자원 할당 그래프가 원형입니다.

-> 자원을 요구하는 방향이 원을 이루면 양보를 하지 않기 때문에 교착상태가 발생합니다.

이 조건 중에서 한 가지라도 만족하지 않으면 교착 상태는 발생하지 않는다. 이중 순환대기 조건은 점유대기 조건과 비선점 조건을 만족해야 성립하는 조건이므로, 위 4가지 조건은 서로 완전히 독립적인 것은 아니다.

 

교착상태를 관리 하기 위해 예방, 회피, 무시할 수 있다고 한다.

 

교착상태의 예방

  • 상호배제 조건의 제거
    • 교착 상태는 두 개 이상의 프로세스가 공유가능한 자원을 사용할 때 발생하는 것이므로 공유 불가능한, 즉 상호 배제 조건을 제거하면 교착 상태를 해결할 수 있다.
  • 점유와 대기 조건의 제거
    • 한 프로세스에 수행되기 전에 모든 자원을 할당시키고 나서 점유하지 않을 때에는 다른 프로세스가 자원을 요구하도록 하는 방법이다. 자원 과다 사용으로 인한 효율성, 프로세스가 요구하는 자원을 파악하는 데에 대한 비용, 자원에 대한 내용을 저장 및 복원하기 위한 비용, 기아 상태, 무한대기 등의 문제점이 있다.
  • 비선점 조건의 제거
    • 비선점 프로세스에 대해 선점 가능한 프로토콜을 만들어 준다.
  • 환형 대기 조건의 제거
    • 자원 유형에 따라 순서를 매긴다.
이 교착 상태의 해결 방법들은 자원 사용의 효율성이 떨어지고 비용이 많이 드는 문제점이 있다.

 

교착상태 회피

자원이 어떻게 요청될지에 대한 추가정보를 제공하도록 요구하는 것으로 시스템에 circular wait가 발생하지 않도록 자원 할당 상태를 검사한다.

교착 상태 회피하기 위한 알고리즘으로 크게 두가지가 있다.

1. 자원할당 그래프 알고리즘(Resource Allocation Graph Algorithm)

2. 은행원 알고리즘(Banker's algorithm)

 

교착상태 무시

예방과 회피방법을 활용하게 되면 자연적으로 성능상 이슈가 발생될텐데, 

데드락 발생에 대한 상황을 고려하는 것의 코스트가 낮다면 별다른 조치를 하지 않을 수도 있다고 한다.

 

 

VisualVM 프로그램, , 

  • JVM을 실시간으로 모니터링 할 수 있는 오픈소스 기반 GUI 툴
  • 데드락 확인 가능
  • heap덤프 및 쓰레드덤프 가능

 

Thread Pool(스레드 풀)

스레드를 생성하는데 드는 비용이 많다. 그럼 스레드를 미리 생성해서 가져다 쓰면 되지 않을까? 라는 생각에서 나온게 스레드 풀이다.

https://catch-me-java.tistory.com/47

  • 할일이 여러개라도 스레드는 더 생성되지 않는다. 
  • 스레드가 일이 끝난다고 종료하는게 아니라, queue에 들어간 다른 작업을 할당 받는다.
    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    // 스레드 총 개수 및 작업 스레드 이름 출력
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
                    int poolSize = threadPoolExecutor.getPoolSize(); //poolSize 총 스레드 개수

                    String threadName = Thread.currentThread().getName();
                    System.out.println("[총 스레드 개수 : " + poolSize + "] 작업 스레드 이름 : " + threadName);
                }
            };
            executorService.submit(runnable);
            Thread.sleep(10);
        }
        executorService.shutdown();
    }
    
    
output : 
[총 스레드 개수 : 2] 작업 스레드 이름 : pool-1-thread-2
[총 스레드 개수 : 1] 작업 스레드 이름 : pool-1-thread-1
[총 스레드 개수 : 3] 작업 스레드 이름 : pool-1-thread-3
[총 스레드 개수 : 4] 작업 스레드 이름 : pool-1-thread-4
[총 스레드 개수 : 4] 작업 스레드 이름 : pool-1-thread-1
[총 스레드 개수 : 4] 작업 스레드 이름 : pool-1-thread-2
[총 스레드 개수 : 4] 작업 스레드 이름 : pool-1-thread-3
[총 스레드 개수 : 4] 작업 스레드 이름 : pool-1-thread-4
[총 스레드 개수 : 4] 작업 스레드 이름 : pool-1-thread-1
[총 스레드 개수 : 4] 작업 스레드 이름 : pool-1-thread-2

 

※ ExecutorService 참조글

gompangs.tistory.com/entry/JAVA-ExecutorService-%EA%B4%80%EB%A0%A8-%EA%B3%B5%EB%B6%80

 

[JAVA] ExecutorService 관련 공부

ExecutorService 관련 공부 1. ExecutorService에 관하여 ExecutorService는 병렬작업 시 여러개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA의 라이브러리이다. 통상적으로 작업을 분리하고 수행하는 작

gompangs.tistory.com

 

참고

sujl95.tistory.com/63

catch-me-java.tistory.com/47

www.notion.so/ac23f351403741959ec248b00ea6870e

 

fork & join & pool 참조

parkadd.tistory.com/48