[Java] Thread in Java, Spring

About Thread in java, spring

Posted by JongMin-Lee on March 16, 2020

1. Thread

Java와 Spring에서의 Thread활용을 보기 전 쓰레드에 대해 알아보겠습니다.

프로세스(Process) & 쓰레드(Thread)

프로세스(Process)

프로세스는 실행 중인 프로그램으로 디스크로부터 메모리에 적재되어 CPU의 할당을 받을 수 있는 것을 말합니다. 간단히 말해서 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램을 의미 합니다.

쓰레드(Thread)

쓰레드는 프로세스의 실행 단위 입니다. 한 프로세스 내에서 동작되는 여러 실행의 흐름으로 프로세스 내의 주소 공간이나 자원을 공유할 수 있습니다. 쓰레드는 쓰레드 ID, PC(Program Counter), 레지스터, 스택으로 구성됩니다. 같은 프로세스에 속한 쓰레드는 코드, 데이터, 파일과 같은 운영체제 자원들을 공유합니다.

[참고] blogspot

왼쪽의 그림이 single-thread 오른쪽이 multi-thread 입니다. thread를 감싸고 있는 것이 process입니다. 각각의 thread에는 register와 stack이 따로 할당되어 있습니다.
스택(stack)은 함수 호출시 전달되는 인자, 되돌아갈 주소값 및 함수 내에서 선언하는 변수 등을 저장하기 위해 사용되는 메모리 공간 이므로 스택 메모리 공간이 독립적이라는 것은 독립적인 함수 호출이 가능하다는 것이고, 이는 독립적인 실행 흐름이라는 것 입니다. 따라서 쓰레드의 정의에 따라 독립적인 실행 흐름을 추가하기 위한 최소 조건으로 독립된 스택을 할당합니다.
PC(Program Counter)는 쓰레드가 명령어의 어디까지 수행하였는지를 나타냅니다. 쓰레드는 CPU를 할당 받았다가 스케줄러에 의해 다시 선점당합니다. 이 때문에 명령어가 연속적으로 수행되지 못하고 어느 부분까지 수행했는지 기억할 필요가 있습니다. 따라서 PC register 또한 독립적으로 할당합니다.

Java Thread

쓰레드에는 몇가지 상태가 존재합니다.

[참고] DZone

  • New : 쓰레드 클래스를 생성했을 때의 상태 입니다.
  • Running : 쓰레드가 동작중인 상태 입니다.
  • Suspended : 동작중인 쓰레드는 일시중지 될 수 있습니다. 중지됬던 쓰레드는 다시 재개될 수 있습니다.
  • Blocked : 쓰레드가 리소스를 대기(wait)할 때의 상태 입니다.
  • Terminated : 쓰레드는 종료될 수 있으며 실행중에도 즉시 종료될 수 있습니다. 한번 종료되면 재개할 수 없습니다.
Main Java Thread

Java의 Main 쓰레드는 상당히 중요합니다. 왜냐하면 Main 쓰레드로 부터 자식(child) 쓰레드가 생길 수 있고, 프로그램을 실행시킬 때 자동적으로 생성되는 쓰레드 이기 때문입니다.
Java에서 쓰레드를 생성하는 2가지 방법이 있습니다.

  • Runnable interface를 구현(implements) 하는 방법
  • Thread를 상속(extends) 하는 방법
Runnable Interface

가장 쉬운 방법은 Runnable interface를 구현하는 방법입니다. Runnable interface는 run 메소드만 구현해주면 됩니다. 한번 보겠습니다.

1
2
3
4
5
6
7
8
9
public class ExamClass implements Runnable{

    @Override
    public void run() {
        System.out.println("ExamClass running");
    }

}

1
2
3
4
5
6
7
8
public class MainClass {

    public static void main(String[] args) {
        Thread t1 = new Thread(new ExamClass());
        t1.start();
    }

}

예제 클래스인 ExamClassRunnable interface를 implements하여 run메소드를 구현했습니다. Main 클래스에서 Tread 타입으로 받아 start() 메소드를 이용하여 실행하였습니다.(Thread가 start하면 run()메소드가 실행됩니다.) console에서 ‘ExamClass running’이라는 문자열을 확인할 수 있습니다.

Extending Thread

두 번째로는 Thread를 상속받는 방법이 있습니다. Thread를 상속받은 뒤 마찬가지로 run메소드를 구현해주면 됩니다.

1
2
3
4
5
6
7
8
public class ExamClass extends Thread{

    @Override
    public void run() {
        System.out.println("Extending Thread");
    }
    
}
1
2
3
4
5
6
7
8
public class MainClass {

    public static void main(String[] args) {
        ExamClass examClass = new ExamClass();
        examClass.start();
    }

}

Runnable interface를 구현한 것과 다른점은 Thread를 상속 받았기 때문에 바로 start() 메소드로 실행 가능하다는 것 입니다.

Sync & Async

Sync는 Synchronized 즉 동기라는 뜻 입니다. Async는 Asynchronized로 동기와 반대인 비동기 입니다. 동기와 비동기는 상당히 많이 쓰입니다.
대표적으로 많이 드는 예가 은행업무 입니다. 만약 내가 10만원이 있는 통장에서 10만원을 인출하려고 합니다. 이 때 다른누군가가 10만원을 동시에 인출하려고 시도 합니다. 이럴 경우 문제가 발생합니다. 내가 10만원을 인출하여 0원이 되고 있는 과정에서 다른 누군가가 먼저 10만원을 인출할 수가 있습니다. 이 일이 일어나는 이유는 내가 10만원을 인출하는 프로세스가 아직 안끝나서 통장잔액은 10만원이기 때문입니다. 여기에 동기화를 적용해야 합니다. 동기화를 적용하게 되면 내가 10만원을 인출하면 다른 누군가는 내가 인출완료할 때까지 접근할 수 없습니다. 따라서 내가 인출을 완료하여 0원이 되면 그제서야 인출을 시작하게 됩니다. 이렇듯 동기화는 A1라는 업무중일 때 A2가 같은 업무를 하기 위해서는 A1이 끝날 때까지 기다리거나 중지시키고 해야 합니다.
비동기는 이와 반대입니다. 기다릴 필요없이 별개의 실행을 합니다. Ajax를 사용해 보셨다면 이해가 쉬울 것 입니다.

Synchronized method/block

동기화를 구현하는데는 2가지 방식이 있습니다. method구현과 block형식의 구현 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ExamA {

    public void ExamA1(String thread){
        System.out.println("ExamA1 시작");
        synchronized (this){
            for(int i=0; i < 3; i++){
                System.out.println("thread : " + thread + ", ExamA1 : " + i);
            }
        }
        System.out.println("ExamA1 끝");
    }

    public synchronized void ExamA2(String thread){
        System.out.println("ExamA2 시작");

        for(int i=0; i < 3; i++){
            System.out.println("thread : " + thread + ", ExamA2 : " + i);
        }

        System.out.println("ExamA2 끝");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
        ExamA examA = new ExamA();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                examA.ExamA1("thread1");
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                examA.ExamA2("thread2");
            }
        });

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

위의 ExamA 클래스에는 메서드 방식과 block방식이 구현되어 있습니다. 그러면 결과를 확인해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
ExamA1 시작
ExamA2 시작
thread : thread2, ExamA2 : 0
thread : thread2, ExamA2 : 1
thread : thread2, ExamA2 : 2
ExamA2 끝
thread : thread1, ExamA1 : 0
thread : thread1, ExamA1 : 1
thread : thread1, ExamA1 : 2
ExamA1 끝

한가지 이상한 점이 있습니다. 동기화 방식은 한가지 쓰레드가 끝나고 동작해야 하는데 시작이라는 문자열이 연속으로 출력된 걸 보실 수 있습니다. 그 이유는 block 방식 때문입니다. 문자열을 출력하는 부분을 보시면 synchronized의 메서드 밖에 작성된걸 볼 수 있습니다. block방식에서는 synchronized 메서드 안에만 동기화 방식으로 동작하기 떄문에 연속으로 출력 된 것 입니다. 이번에는 main 메서드를 좀 더 변경해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
    ExamA examA = new ExamA();
    ExamA examA1 = new ExamA();

    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            examA.ExamA1("thread1");
        }
    });

    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            examA1.ExamA2("thread2");
        }
    });

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

ExamA의 객체를 하나 더 생성한 뒤 실행해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
ExamA1 시작
ExamA2 시작
thread : thread2, ExamA2 : 0
thread : thread1, ExamA1 : 0
thread : thread1, ExamA1 : 1
thread : thread1, ExamA1 : 2
ExamA1 끝
thread : thread2, ExamA2 : 1
thread : thread2, ExamA2 : 2
ExamA2 끝

아까와는 다르게 값이 뒤섞여 출력되는 것을 볼 수 있습니다. 객체가 다르면 두 쓰레드가 동시에 동작 된다는 것을 확인할 수 있습니다.

Spring Thread

Spring에서는 쓰레드 관리를 위해서 TaskExecutor를 제공해 줍니다. TaskExecutor는 java.util.concurrent.Executor를 상속받은 인터페이스 입니다. TaskExecutor를 사용하려면 먼저 Bean 설정을 해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ThreadConfig {
    @Bean
    public TaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(4);
        executor.setThreadNamePrefix("default_task_executor_thread");
        executor.initialize();
        return executor;
    }
}

위 처럼 TaskExecutor에 대해 빈 설정을 해줍니다. 이제는 간단 합니다. 주입만 해주면 끝이 납니다. 이제 쓰레드 빈을 만들어 준 후 주입시켜주겠습니다.
실행될 Task를 가지는 Runnable클래스를 만들어 줍니다.

1
2
3
4
5
6
7
8
9
@Component
@Scope("prototype")
public class MyThread implements Runnable {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyThread.class);
    @Override
    public void run() {
        LOGGER.info("Called from thread");
    }
}

실행 프로그램을 서비스에 주입하여 인스턴스가 실행할 준비를 해줍니다.

1
2
3
4
5
6
7
8
9
10
11
@Service
public class AsynchronousService {
    @Autowired
    private TaskExecutor taskExecutor;
    @Autowired
    private ApplicationContext applicationContext;
    public void executeAsynchronously() {
        MyThread myThread = applicationContext.getBean(MyThread.class);
        taskExecutor.execute(myThread);
    }
}

지금까지 Thread의 대략적인 내용과 코드를 살펴봤습니다. 이외에도 다른 메서드들이 있고, 사용처가 있습니다. 저는 기본적인 부분만 다루었기 때문에 다른 사항들은 레퍼런스나 다른글을 참고해 주시면 감사하겠습니다. 감사합니다!

참고
  • DZone(https://dzone.com/articles/java-thread-tutorial-creating-threads-and-multithr)
  • tutorialspoint(https://www.tutorialspoint.com/spring/spring_bean_scopes.htm)
  • egkatzioura(https://egkatzioura.com/2017/10/25/spring-and-async/)