1. Thread
Java와 Spring에서의 Thread활용을 보기 전 쓰레드에 대해 알아보겠습니다.
프로세스(Process) & 쓰레드(Thread)
프로세스(Process)
프로세스는 실행 중인 프로그램으로 디스크로부터 메모리에 적재되어 CPU의 할당을 받을 수 있는 것을 말합니다. 간단히 말해서 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램을 의미 합니다.
쓰레드(Thread)
쓰레드는 프로세스의 실행 단위 입니다. 한 프로세스 내에서 동작되는 여러 실행의 흐름으로 프로세스 내의 주소 공간이나 자원을 공유할 수 있습니다. 쓰레드는 쓰레드 ID, PC(Program Counter), 레지스터, 스택으로 구성됩니다. 같은 프로세스에 속한 쓰레드는 코드, 데이터, 파일과 같은 운영체제 자원들을 공유합니다.
왼쪽의 그림이 single-thread 오른쪽이 multi-thread 입니다. thread를 감싸고 있는 것이 process입니다. 각각의 thread에는 register와 stack이 따로 할당되어 있습니다.
스택(stack)은 함수 호출시 전달되는 인자, 되돌아갈 주소값 및 함수 내에서 선언하는 변수 등을 저장하기 위해 사용되는 메모리 공간 이므로 스택 메모리 공간이 독립적이라는 것은 독립적인 함수 호출이 가능하다는 것이고, 이는 독립적인 실행 흐름이라는 것 입니다. 따라서 쓰레드의 정의에 따라 독립적인 실행 흐름을 추가하기 위한 최소 조건으로 독립된 스택을 할당합니다.
PC(Program Counter)는 쓰레드가 명령어의 어디까지 수행하였는지를 나타냅니다. 쓰레드는 CPU를 할당 받았다가 스케줄러에 의해 다시 선점당합니다. 이 때문에 명령어가 연속적으로 수행되지 못하고 어느 부분까지 수행했는지 기억할 필요가 있습니다. 따라서 PC register 또한 독립적으로 할당합니다.
Java Thread
쓰레드에는 몇가지 상태가 존재합니다.
- New : 쓰레드 클래스를 생성했을 때의 상태 입니다.
- Running : 쓰레드가 동작중인 상태 입니다.
- Suspended : 동작중인 쓰레드는 일시중지 될 수 있습니다. 중지됬던 쓰레드는 다시 재개될 수 있습니다.
- Blocked : 쓰레드가 리소스를 대기(wait)할 때의 상태 입니다.
- Terminated : 쓰레드는 종료될 수 있으며 실행중에도 즉시 종료될 수 있습니다. 한번 종료되면 재개할 수 없습니다.
Main Java Thread
Java의 Main 쓰레드는 상당히 중요합니다. 왜냐하면 Main 쓰레드로 부터 자식(child) 쓰레드가 생길 수 있고, 프로그램을 실행시킬 때 자동적으로 생성되는 쓰레드 이기 때문입니다.
Java에서 쓰레드를 생성하는 2가지 방법이 있습니다.
Runnableinterface를 구현(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();
}
}
예제 클래스인 ExamClass에 Runnable 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/)