
자바에서 스레드는
어려워서 머리를 싸매기도 하지만
더 넓은 세계를 마주하는 것 같아 설레이기도 하는 챕터다.
자바의 스레드에 대해 알아보자.

스레드를 이해하려면 먼저 스레드가 등장하기 까지의 포함관계를 이해해야 한다.
프로그램은 단순 파일이다.
게임을 시작 전 아이콘을 더블클릭 하는 것이
바로 프로그램을 실행하는 것이다.
게임을 하기 위해 아이콘을 더블클릭하면 게임이 실행되며, 실행중인 상태가 된다.
이 때 실행중인 프로그램을 프로세스라고 한다.
프로세스는 스레드 실행 환경을 제공하는 컨테이너 역할을 한다.
게임 내에서 친구들과 채팅도 하면서 동시에 몬스터를 사냥하기도 한다.
실행 중인 프로그램인 프로세스 내에서도 여러 작업이 동시에 일어난다.
이처럼 프로세스 내 실행 작업 단위를 바로 스레드라고 하는 것이다!

프로세스 내에서 스레드가 한개만 실행되면 단일 스레드,
동시에 여러개가 실행되면 멀티 스레드라고 한다.
멀티 스레드를 이해하기 전에 먼저 여기서 이야기하는 멀티의 의미를 이해해야 한다.

멀티 태스킹이란 하나의 CPU가
여러 프로세스를 순차적으로 조금씩 실행하면서
마치 동시에 실행하는 것처럼 보이게 하는 것을 의미한다.

멀티 프로세싱은 멀티 태스킹처럼
CPU가 여러 프로세스를 순차적으로 실행하면서 동시에 실행하는 것처럼 보이게 하는데,
멀티 태스킹과 달리 CPU가 한개가 아닌 여러개일 때의 상황을 말한다.
멀티 태스킹에서 일하는 사람이 늘었다고 생각하면 된다.
CPU가 여러 프로세스를 '조금씩' 실행하는데,
조금씩을 설정하는 것이 바로 스케줄링이다.
예를 들어 프로세스1은 10% 실행하고, 프로세스2는 15%를 실행하는 것.
다양한 우선순위와 최적화 기법을 사용하여 운영체제가 결정한다.
CPU가 여러 프로세스를 번갈아서 실행한다고 했다.
프로세스1을 조금 처리하고, 프로세스2를 또 조금 처리한다.
그 이후 다시 프로세스1을 조금 처리해야 하는데,
얼만큼 처리해서 어디서부터 다시 처리를 시작하면 좋을지
체크포인트를 기록해야 다시 빠르게 작업에 돌입할 수 있다.
이처럼 작업해뒀던 상태를 메모리에 잠시 저장하고 불러오는 작업을 컨텍스트 스위칭이라고 한다.

요즘은 하드웨어의 성능이 좋아 대부분 다수의 CPU를 보유하고 있다.
CPU가 2개라고 가정하고, 자바에서 스레드 개수에 따라 어떤 상황이 벌어지는지 살펴보자.
언뜻 보면 CPU의 개수와 Thread의 개수가 동일한 것이
CPU를 100% 활용하면서 컨텍스트 스위칭 비용이 발생하지 않아 가장 좋은 것처럼 보인다.
하지만 CPU가 연산하느라 바쁜 것이 아니라, 연결이나 통신 문제로 대기하는 상태라면 어떨까?
바쁘지도 않으면서, 바쁜 척 하는 CPU가 있다면 오히려 다른 일을 맡겨버리는게 좋을 것이다.
따라서 실제로 연산이 빈번하게 발생하는 경우,
스레드의 개수를 CPU + 1개로 설정하는 것이 이상적이다.
하지만 백엔드에서 연결이나 통신 문제가 더 많이 일어나는 상황이라면,
테스트를 통해서 적절한 스레드 수를 설정해야 한다.

스레드를 생성하는 방법에는 Thread를 상속하는 것과
Runnable을 구현하는 방법으로 총 2가지가 있다.
결론적으로 말하면 Runnable을 구현하는 방식으로 스레드를 생성해야 한다.
자바에서는 다중 상속이 불가능하기 때문이다.
이 외에도 여러모로 Runnable을 구현하여 생성하는 것이 더 이점이 많다.
class Task extends Thread {
@Override
public void run() {
// 코드 작성
}
}
public void createThread() {
Task task = new Task();
task.start();
Thread를 상속하여 스레드를 생성하는 경우,
run 메서드를 오버라이딩하면 된다.
class Task implements Runnable {
@Override
public void run() {
// 코드 작성
}
}
public void createThread() {
Thread thread = new Thread(new Task());
thread.start();
Runnable을 구현하여 스레드를 생성하는 경우,
run 메서드를 오버라이딩하고 Thread를 생성하면서 Runnable 객체를 전달하면 된다.

Thread thread = new Thread(new Task());
thread.start();
스레드를 생성한 후, 실행하기 위해서는 start() 메서드를 사용해야 한다.
run() 메서드를 오버라이딩 했는데, 왜 start() 메서드를 호출하는 것일까?
start() 메서드가 아닌 run() 메서드를 호출하는 경우,
main 메서드의 스택 프레임에서 run() 메서드를 수행해버린다.
스택은 LIFO 순서로 진행되어 run() 메서드를 수행하는 동안 다른 메서드를 동시에 수행할 수 없다.
start() 메서드를 호출해야 해당 스레드의 스택 프레임이 또 하나 생성되어
그 곳에서 run() 메서드를 수행해 동시에 메서드를 수행하는 멀티 스레드 환경을 구축할 수 있다.
프로세스는 각각의 메서드, 스택, 힙 영역을 가진다.
따라서 문제가 발생했을 때, 다른 프로세스에 영향을 미치지 않아 안정성이 높다.
하지만 각각의 영역을 가지게 되므로 효율성은 떨어진다.
하지만 스레드는 스택 영역을 제외하고 메서드, 힙 영역을 공유한다.
스레드 간의 통신 비용이 적어 효율적으로 사용이 가능해 성능이 좋아지지만,
동기화 문제 등 자원을 공유함으로써 발생하는 문제에 대한 위험이 있다.

스레드는 다음과 같은 정보를 가진다.
스레드의 정보 중, 스레드의 상태는 실행 흐름을 파악하기 위해
반드시 이해하고 시작해야 하는 부분이다.
스레드 생성부터 스레드 시작 전까지의 상태를 의미한다.
스레드가 만들어졌지만, start() 메서드가 호출되기 전의 상태이다.
스레가 실행 중이거나, 실행될 준비가 된 상태를 의미한다.
start() 호출 이후 스레드가 종료되기 전 까지의 상태이다.
동기화 락을 대기하는 상태이다.
다른 스레드가 락을 보유하고 있어 락을 획득하기 위해 대기하는 상태이다.
다른 스레드의 작업을 완료할 때까지 기다리는 상태이다.
다른 스레드의 작업을 기다리는 상태이지만,
무한정 기다리는 것이 아닌 일정 시간만 대기하는 상태이다.
스레드가 종료된 상태이다.
스레드는 종료 후 다시 시작할 수 없다.

스레드를 사용하는 다양한 메서드에 대해 알아보자.
thread.start(); // ** run() 메서드가 아닌 start() 메서드 호출 **
스레드를 실행하는 메서드이다.
스레드 생성 시 오버라이딩한 run() 메서드를 호출하는 것이 아닌,
반드시 start() 메서드를 호출해서 스레드를 실행해야 한다.
thread.join();
thread.join(1000); // ** 단위는 ms **
부모 스레드가 생성한 스레드의 작업이 완료할 때까지 기다리는 메서드이다.
스레드의 작업이 종료될 때까지 부모 스레드는 다음 코드로 넘어가지 않는다.
파라미터로 대기할 시간을 지정할 수 있다.
thread.interrupt();
thread에게 작업 중단을 지시하는 메서드이다.
interrupt() 메서드를 호출하면, thread의 인터럽트 상태가 true로 변경된다.
인터럽트 상태가 true인 thread는 interruptException을 던지는 메서드 호출 시
곧바로 예외를 던지고 작업을 중단하며, 인터럽트 상태를 다시 false로 변경한다.
thread.isInterrupdted();
스레드의 인터럽트 상태를 알 수 있는 메서드이다.
인터럽트 상태만 확인할 뿐, 인터럽트 상태를 변경하지 않는다.
thread.interrupted();
스레드의 인터럽트 상태가 true인 경우, false로 변경하고 true를 반환한다.
스레드의 인터럽트 상태가 false인 경우, 인터럽트 상태를 유지하고 false를 반환한다.
인터럽트 상태가 true로 계속 유지될 경우 예상치 못한 상황에서 종료될 수 있으므로
인터럽트를 발생시킨 후에는 반드시 스레드의 인터럽트 상태를 false로 변경해야 한다.
thread.yield();
Thread가 특별하게 수행하고 있는 작업이 없는 경우,
CPU 스케줄링에 다시 들어가면서 동시에 다른 스레드에게 CPU 작업 우선 순위를 양보하는 메서드이다.

스레드는 크게 2가지로 분류할 수 있다.
사용자 스레드의 경우 모든 작업을 완료할 때까지 실행되며,
모든 사용자 스레드가 종료될 경우 JVM도 종료된다.
보조적인 작업을 수행하는 스레드로,
모든 사용자 스레드가 종료될 경우 작업이 완료되지 않은 상태여도 함께 종료된다.
thread.setDaemon(true);
데몬 스레드는 setDaemon() 메서드를 통하여 데몬 스레드로 지정할 수 있다.
자바에서 스레드의 등장은 새로운 세계를 마주하는 느낌이다.
스레드에 돌입하면서 복잡한 프로그램의 세계를 더욱 자세히 이미지화해보자.