하나의 cpu가 두가지이상의 일을 동시에 하는것
학습 목표
- 스레드가 무엇인지 설명할 수 있다.
프로세스 (Process)와 스레드(Thread)
프로세스는 데이터, 컴퓨터자원, 그리고 스레드로 구성되는데, 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스코드를 실행한다 즉 스레드는 하나의 코드 실행 흐름 이라고 볼수있다.
- 싱글 스레드와 멀티 스레드의 차이를 설명할 수 있다.
메인스레드(Main thread)
자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드는 main메서드이며, 메인 스레드가 main메서드를 실행시켜준다. 메인 스레드는 main메스드의 코드를 처음부터 끝까지 순차적으로 실행시키며 코드의 끝을 만나거나 return문을 만나면 실행을 종료한다.
만약 자바 애플리케이션의 소스 코드가 싱글 스레드로 작성되었다면, 그 애플리케이션이 실행되어 프로세스가 될 때 ㄱ오로지 메인 스레드만 가지는 싱글 스레드 프로세스가 될꺼다. 반면 메인스레드에서 또다른 스레드를 생성하여 실행시킨다면 해당 애플리케이션은 멀티 스레드로 동작하게 된다.
멀티스레드(Multi Thread)
하나의 프로세스는 여러개의 스레드를 가질 수 있으며, 멀티스레드 프로세스라 한다 . 여러개의 스레드를 가진다는 것은 여러 스레드가 동시에 작업을 수행할 수 있다는것을 의미한다.
예를들어 메신저 프로그램을 사용할 때 상대방에게 보낼 사진을 업로드하면서 동시에 메세지를 주고받을수있다. 이처럼 메신저프로그램이 여러가지 작업을 동시에 수행하려면, 작업을 동시에 실행해줄 스레드가 추가적으로 필요하다.
- 스레드를 생성하는 두 가지 방법을 활용할 수 있다.
작업 스레드 생성과 실행
메인 스레드 외에 별도의 작업 스레드를 활용한다는 것은, 다시말해 작업 스레드가 수행할 코드를 작성하고, 작업스레드를 생성하여 실행시키는것을 의미한다.
자바는 객체지향 언어이므로 모든 자바 코드는 클래스 안에 작성된다. 그러므로 스레드가 수행할 코드도 클래스 내부에 작성해주어야하며, run()이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어져 있다.
run() 메서드는 Runnable인터페이스와 Thread클래스에 정의되어져있다. 따라서 작업스레드를 생성하고 실행하는 방법은 두가지가 있다.
첫번째 방법 - Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
두번째 방법 - Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
- 스레드를 실행시킬 수 있다.
익명 객체를 사용하여 스레드 생성하고 실행하기
쓰레드가 수행할 동작은 run() 메서드의 바디에 작성해야하며, 자바는 객체지향 언어이므로 클래스 안에 코드를 작성해야한다. 그러나 꼭 클래스를 따로 정의하지 않고도 익명 객체를 활용하여 스레드를 생성하고 실행시킬 수 있다.
- 스레드를 동기화할 수 있다.
프로세스는 자원, 데이터, 그리고 스레드로 구성된다, 프로세스는 스레드가 운영 체제로부터 자원을 할당 받아 소스 코드를 실행하여 데이터를 처리한다.
이때, 싱글 스레드 프로세스는 데이터에 단 하나의 스레드만 접근하므로 문제되지 않지만, 멀티 스레드 프로세스의 경우 두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생 할 수있다.
임계영역(Critical section)과 락(Lock)
임계 영역은 오로지 하나으 ㅣ스레드만 코드를 실행할 수 있는 코드 영역을 의미하며, 락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다.
즉 임계 영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지고 있지 않을 때, 임의의 스레드A는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다.
이때 스레드A가 임계 영역 내의 코드를 모두 실행하면 락을 반납한다. 이 때부터는 다른 스레들 중 하나가 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다.
두 스레드가 동시에 실행하면 안되는 영역을 설정하는것이다. 즉 withraw()메서드를 두 스레드가 동시에 실행하지 못하게 해야한다.
이를 임꼐 영역과 락이라는 용어를 사용하여 표현하면 다음과 같다.
withdraw() 메서드를 임계 영역으로 설정해야한다
특정 코드 구간을 임계 영역으로 설정할땐 synchronized라는 키워드를 사용한다 이 키워드는 두가지 방법으로 사용할 수 있다.
1. 메서드 전체를 임계 영역으로 지정하기
메서드의 반환타입 좌측에 sychronized 키워드를 작성하면 메서드 전체를 임계 영역으로 설정할 수 있다 이렇게 메서드 전체를 임계 영역으로 지정하면 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락을 얻는다.
즉, withdraw()가 호출되면, withdraw()를 실행하는 스레드는 withdraw()가 포함된 객체의 락을 얻으며, 해당 스레드가 락을 반납하기 이전에 다른 스레드는 해당 메서드의 코드를 실행하지 못하게 된다.
2.특정한 영역을 임계 영역으로 지정하기.
특정 영역을 임계 영역으로 지정하려면 아래와 같이 synchronized 키워드와 함께 소괄호(()) 안에 해당 영역이 포함된 객체의 참조를 넣고, 중괄호({})로 블럭을 열어, 블럭 내에 코드를 작성하면 됩니다.
이 경우에도 마찬가지로, 임계 영역으로 설정한 블럭의 코드로 코드 실행 흐름이 진입 할 때, 해당 코드를 실행하고 있는 스레드가 this에 해당하는 객체의 락을 얻고, 배타적으로 임계 영역 내의 코드를 실행합니다.
- 스레드의 상태를 이해하고, 제어할 수 있다.
start()는 스레드를 실행시키는 메서드는 아니다.
start()는 스레드의 상태를 실행 대기 상태로 만들어주는 메서드이며, 어떤 스레드가 start()에 의해 실행 대기 상태가 되면 운영체제가 적절한 때에 스레드를 실행시켜준다.
여기에서 알 수 있는 사실은 다음과 같습니다.
- start()는 스레드를 실행 대기 상태로 만들어준다.
- 즉, 스레드에는 상태라는 것이 존재한다.
- 또한, 스레드의 상태를 바꿀 수 있는 메서드가 존재한다.
스레드 실행 제어 메서드
sleep(long milliSecond): milliSecond동안 스레드를 잠시 멈춥니다.항상 지정한 시간 만큼 정확히 스레드가 중지되는 것은 아니며 오차를 가진다.
sleep()은 Thread의 클래스 메서드이다. 따라서 sleep()을 호출할 때에는 Thred.sleep(1000);과 같이 클래스를 통해서 호출하는것을 권장한다.
sleep()을 호출하면 호출하는 코드를 실행한 스레드의 상태가 실행상태에서 일시정지(TIME_WAITING)상태로 전환된다.
sleep()에 의해 일시정지된 스레드는 다음의 경우에 실행 대기 상태로 복귀한다.
-인자로 전달한 시간 만큼의 시간이 경과한 경우
-interrupt()를 호출한 경우
위의 두 가지 경우 중 interrupt()를 호출하여 스레드를 실행 대기 상태로 복귀시키고자 한다면 반드시 try … catch 문을 사용해서 예외 처리를 해주어야 합니다. 아래에서 설명할 예정이지만 간단하게 미리 설명하자면, interrupt()가 호출되면 기본적으로 예외가 발생하기 때문입니다.
try { Thread.sleep(1000); } catch (Exception error) {}
interrupt() : 일시 중지 상태인 스레드를 실행 대기 상태로 복귀시킵니다.
void interrupt()
interrupt()는 sleep(), wait(), join()에 의해 일시 정지 상태에 있는 스레드들을 실행 대기 상태로 복귀시킵니다.
sleep(), wait(), join()에 의해 일시 정지된 스레드들의 코드 흐름은 각각 sleep(), wait(), join()에 멈춰있습니다.
멈춰있는 스레드가 아닌 다른 스레드에서 멈춰 있는 스레드.interrupt()를 호출하면, 기존에 호출되어 스레드를 멈추게 했던 sleep(), wait(), join() 메서드에서 예외가 발생되며, 그에 따라 일시 정지가 풀리게 됩니다.
yield() : 다른 스레드에게 실행을 양보합니다.
static void yield()
yield()는 다른 스레드에게 자신의 실행 시간을 양보합니다. 예를 들어, 운영 체제의 스케줄러에 의해 3초를 할당 받은 스레드 A가 1초 동안 작업을 수행하다가 yield()를 호출하면 남은 실행 시간 2초는 다음 스레드에게 양보됩니다.
스레드를 활용할 때, 스레드에게 반복적인 작업을 실행시키는 경우가 많습니다. 그런데 특정 경우에 반복문의 순회가 불필요할 때가 있습니다.
예를 들어 다음과 같이 코드가 작성되어져 있을 때, example의 값이 false라면 스레드는 while문의 반복이 불필요함에도 계속해서 반복시킵니다.
public void run() {
while (true) {
if (example) {
...
}
}
}
이러한 경우에 yield()를 유용하게 활용할 수 있습니다. 아래처럼 어떤 스레드가 yield()를 호출하면 example의 값이 false일 때에 무의미한 while문의 반복을 멈추고 실행 대기 상태로 바뀌며, 자신에게 남은 실행 시간을 실행 대기열 상 우선순위가 높은 다른 스레드에게 양보합니다.
public void run() {
while (true) {
if (example) {
...
}
else Thread.yield();
}
}
join() : 다른 스레드의 작업이 끝날 때까지 기다립니다.
void join()
void join(long milliSecond)
join()은 특정 스레드가 작업하는 동안에 자신을 일시 중지 상태로 만드는 상태 제어 메서드입니다.
인자로 시간을 밀리초 단위로 전달할 수 있으며, 전달한 인자만큼의 시간이 경과하거나, interrupt()가 호출되거나, join() 호출 시 지정했던 다른 스레드가 모든 작업을 마치면 다시 실행 대기 상태로 복귀합니다.
join()은 다음과 같은 측면에서 sleep()과 유사합니다.
- join()을 호출한 스레드는 일시 중지 상태가 됩니다.
- try … catch문으로 감싸서 사용해야 합니다.
- interrupt()에 의해 실행 대기 상태로 복귀할 수 있습니다.
그러나 sleep()과 join()은 다음과 같은 차이점을 가집니다.
- sleep()은 Thread 클래스의 static 메서드입니다. 반면, join()은 특정 스레드에 대해 동작하는 인스턴스 메서드입니다.
- 예 : Thread.sleep(1000);
- 예 : thread1.join();
wait(), notify() : 스레드 간 협업에 사용됩니다.
스레드를 활용하다보면, 두 스레드가 교대로 작업을 처리해야할 때가 있습니다. 이 때 사용할 수 있는 상태 제어 메서드가 바로 wait()과 notify()입니다.
스레드A와 스레드B가 공유 객체를 두고 협업하는 상황을 가정해봅시다. 스레드간 협업은 아래의 플로우로 진행됩니다.
먼저, 스레드A가 공유 객체에 자신의 작업을 완료합니다. 이 때, 스레드B와 교대하기 위해 notify()를 호출합니다. notify()가 호출되면 스레드B가 실행 대기 상태가 되며, 곧 실행됩니다. 이어서 스레드A는 wait()을 호출하며 자기 자신을 일시 정지 상태로 만듭니다.
이후 스레드B가 작업을 완료하면 notify()를 호출하여 작업을 중단하고 있던 스레드A를 다시 실행 대기 상태로 복귀시킨 후, wait()을 호출하여 자기 자신의 상태를 일시 정지 상태로 전환합니다.
'TIL' 카테고리의 다른 글
좋은 객체 지향 설계의 5가지 원칙(SOLID) (0) | 2022.09.17 |
---|---|
[Java]자바 가상 머신(Java Virtual Machine) (0) | 2022.09.16 |
[Java]추상화(Abstraction) (0) | 2022.09.16 |
[Java]스트림(Stream) (0) | 2022.09.15 |
[Java]람다식(Lambda Expression) (0) | 2022.09.15 |