예전에 작성했던 스레드에 추가적인 내용을 담았다.
프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원, 스레드로 구성되어 있다.
따라서, 모든 프로세스는 하나 이상의 스레드를 갖는다.
일반적인 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드를 말한다. 일반 스레드는 작업이 모두 끝나면 프로세스가 종료되지만, 데몬 스레드는 일반 스레드가 종료되면 작업 중이라 하더라고 강제적으로 종료된다.
이는 데몬 스레드가 일반 스레드의 보조 역할이기 때문에 일반 스레드가 없으면 존재의 의미가 사라지기 때문이다.
데몬 스레드의 예로는 가비지 컬렉터, 워드 프로세서의 자동저장 등이 있다.
작업 관리자를 통해 현재 실행 중인 프로그램들의 쓰레드 수를 살펴보면 대다수가 하나 이상의 스레드를 가지고 있는 것을 확인할 수 있다.
이렇게 하나의 스레드를 갖는 프로세스를 싱글 스레드 프로세스, 하나 이상의 스레드를 갖는 프로세스를 멀티 스레드 프로세스라고 한다.
프로세스를 하나의 공장, 스레드를 한명의 일꾼으로 생각해보았을 때, 한 공장에 한명의 일꾼을 두는 두개의 공장보다 한 공장에 여러명의 일꾼을 두는 것이 비용적으로 이득일 것이다.
스레드는 하나의 요청이 마무리될 때까지 다른 요청을 대기 상태에 두어야 한다. 이는 사용자가 파일을 다운로드 받는 상황에서 다른 행위를 할 수 없다는 의미다.
이러한 싱글 스레드의 단점은 멀티 스레드를 사용하며 보완할 수 있다.
같은 자원을 가지고 여러개의 요청을 처리할 수 있기 때문에 싱글 스레드에 비해 자원을 효율적으로 사용할 수 있다.
Thread1 t1 = new Thread1(); // 객체를 생성했다고 해서 스레드가 실행되는 것은 아니다.
Thread1 t2 = new Thread1();
t1.start(); // 생성된 객체를 start() 메서드를 통해 실행시킬 수 있다.
t2.start();
코드만 보면 스레드가 start()
를 호출해 바로 작업을 실행하는 것처럼 보이지만 사실이 아니다.
start()
를 호출하면서 작업을 실행할 수 있는 실행대기 상태로 전환되는 것이다. 따라서 t1
스레드가 먼저 호출되었다 하더라도 t2
보다 먼저 실행되는 것이 아니다.
싱글 스레드의 경우에는 코드의 실행 흐름처럼 위->아래로 작업을 실행하여 작업 순서를 파악할 수 있고, 순서를 지정 할 수 있지만 멀티 스레드는 OS 스케줄러에 의해 순서가 결정되며 개발자는 그 순서를 절대 알 수 없다.
💡 스레드는 일회용이다. 한번 실행이 종료된 스레드는 다시 실행할 수 없다.
멀티 스레드는 작업을 번갈아가며 수행한다. 싱글 스레드에서의 A 작업 시간인 t1과 멀티 스레드의 A의 작업 시간인 t1은 거의 동일하다.
🚨 멀티 스레드의 각 분할된 작업 시간은 OS 스케줄러에 의해 정해지므로 제어할 수 없다.
스레드는 우선순위를 지정할 수 있는 방법이 있긴 하다. 하지만 이 방법도 해당 스레드에 더 많은 작업 시간을 주어질 뿐 순서가 정해지는 것이 아니다.
여러 작업을 번갈아가며 수행하는 것은 동일하나 우선순위가 더 높은 A 스레드에 한번 수행하는 작업 시간을 더 길게 잡을 수 있다.
🚨 하지만 이 방법도 그저 희망사항일 뿐 만족할 수 있는 결과물을 가지기란 어렵다.
하지만 실제로 싱글 스레드와 멀티 스레드의 실행 소요 시간을 살펴 보면 크게 차이가 나지 않는다.
이는 두가지 이유를 가진다.
이렇게 실행 소요 시간이 별로 차이가 나지 않음에도 멀티 스레드를 사용하는 이유는 시간이 더 걸리더라도 여러 작업을 한번에 할 수 있다는 강한 장점을 가지기 때문이다.
입출력 시 작업 중단을 의미한다.
사용자에게 입력을 받아야 하는 작업이 있다고 한다면, 싱글 스레드의 경우 사용자가 입력을 하기 전까지는 스레드가 그 어떠한 일도 수행할 수 없다.
멀티 스레드의 경우 A의 작업에 대한 사용자의 응답이 있을 때까지 B 작업을 대기 시켜둘 필요가 없다. 따라서 싱글 스레드에 보다 효율적으로 CPU를 사용할 수 있으며 멀티 스레드의 작업이 더 빨리 마치는 것을 확인할 수 있다.
멀티 스레드는 장점만 가지고 있을까?
그렇지 않다.
멀티 스레드 프로세스는 여러 스레드가 같은 자원을 공유하면서 작업하기 때문에 동기화, 교착 상태와 같은 문제가 발생할 수 있기 때문에 설계에 고려해야 할 사항들이 늘어나 신중한 프로그래밍을 진행해야 한다.
멀티 스레드는 서로 같은 자원을 공유하며 작업을 하기 때문에 서로의 작업에 영향을 줄 수 있다.
하나의 작업이 끝난 후, 변경된 데이터로 작업을 진행하면 좋겠지만 멀티 스레드는 작업의 순서를 정하여 진행할 수 없다는 것을 기억하자.
이러한 단점을 보완하기 위해 한 스레드가 진행중인 작업을 다른 스레드가 간섭하지 못하도록 스레드의 동기화를 한다.
다른 스레드가 간섭할 수 없도록 영역을 지정해주는 것이다. 임계 영역은 락(lock, 일종의 열쇠)를 얻은 단 하나의 스레드만 해당 영역에 접근이 가능하다. 락은 객체 하나당 하나의 락만 존재하며, 임계 영역에서 작업을 끝낸 스레드는 락을 반환한다.
다른 스레드는 반환된 락을 가지고 임계 영역에 접근할 수 있다.
임계영역을 설정하기 위해 synchronized
키워드를 사용하며, 2가지 방법이 존재한다.
1. 메서드 전체를 임계 영역으로 지정
public synchronized void method() {
...
}
2. 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) {
...
}
🚨 주의해야 할 점, 임계 영역에서 사용되는 인스턴스 변수는 반드시
private
접근 제어자를 가지고 있어야 한다.private
이 아닐 경우 외부에서 직접 접근할 수 있기 때문에 아무리 동기화를 진행한다 하더라고 값을 변경을 막을 수 없다.