10주차 과제: 멀티쓰레드 프로그래밍
목표
자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.
학습할 것
- Thread 클래스와 Runnable 인터페이스
- 쓰레드의 상태
- 쓰레드의 우선순위
- Main 쓰레드
- 동기화
- 데드락
Thread 클래스와 Runnable 인터페이스
자바에서 쓰레드를 사용하기 위해서는 두 가지 방법이 있습니다.
- Thread 클래스를 상속받아 구현
- Runnable 인터페이스를 상속받아 구현
그 전에 먼저 쓰레드가 무엇인지 한번 알아보겠습니다.
쓰레드를 알기 위해서는 먼저 프로세스의 개념부터 알고 가는게 좋습니다.
Thread
프로세스란 동작하고 있는 프로그램을 말합니다. 프로그램이란 파일 시스템에 있는 실행 파일(.exe ...)을 말합니다. 보통 한 개의 프로세스는 한 가지 일을 하지만 쓰레드를 이용하게 되면 한 프로세스 내에서 두 가지 또는 그 이상의 일을 동시해 할 수 있습니다.
Thread Class
package com.azurealstn.sociallogin.Thread;
public class Test extends Thread {
int seq; //어떤 쓰레드인지 확인하기 위해 쓰레드의 순번
public Test(int seq) {
this.seq = seq;
}
@Override
public void run() {
System.out.println(this.seq + "thread start");
try {
Thread.sleep(1000); //1초
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(this.seq + "thread end");
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Test(i);
thread.start();
}
System.out.println("main end");
}
}
Thread 클래스를 상속받으면 run()
메소드를 구현할 수 있습니다. 그리고 구현한 run()
메소드를 호출하려면 start()
메소드를 호출해주면 됩니다.
실행 결과 순서는 0부터 순서대로 출력되지 않고 랜덤으로 출력되는 것을 확인할 수 있습니다. 즉, 쓰레드는 순서에 상관없이 동시에 실행된다는 점을 알 수 있습니다.
위 코드는 쓰레드가 모두 종료되기도 전에 main()
메소드가 종료된 것을 확인할 수 있습니다.join()
메소드를 사용하면 쓰레드가 종료될 때까지 기다리게 하는 메소드입니다. 즉, join()
메소드를 사용하면 모든 쓰레드가 종료된 후에 main()
메소드를 종료시킬 수 있습니다.
Runnable Interface
Thread를 사용할 때 Thread 클래스보다는 Runnable 인터페이스를 더 많이 사용합니다.
package com.azurealstn.sociallogin.Thread;
import java.util.ArrayList;
public class Test implements Runnable {
int seq; //어떤 쓰레드인지 확인하기 위한 쓰레드의 순번
public Test(int seq) {
this.seq = seq;
}
@Override
public void run() {
System.out.println(this.seq + "thread start");
try {
Thread.sleep(1000); //1초
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(this.seq + "thread end");
}
public static void main(String[] args) {
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Test(i));
thread.start();
threads.add(thread);
}
for (int i = 0; i < threads.size(); i++) {
Thread thread = threads.get(i);
try {
thread.join();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("main end");
}
}
Runnable 인터페이스의 장점은 Thread 클래스는 상속받으면 다른 클래스들은 상속받을 수 없습니다. 하지만 Runnable 인터페이스는 인터페이스이기 때문에 다른 클래스를 상속받아서 확장성과 재사용성이 용이해집니다. 또한 Thread 를 한번 사용하면 재사용이 불가능하지만, Runnable 을 이용하여 Thread 를 구현하면 재사용할 수 있습니다.
쓰레드의 상태
쓰레드를 다루다 보면 쓰레드의 행동을 직접 제어해야할 경우가 있습니다.
예를 들어, 1000장의 문서를 복사해야 하는데 공유된 프린터는 1개 밖에 없도 가정하십니다.
분명 다른 사람들도 써야하기 때문에 한꺼번에 1000장을 복사할 순 없습니다.
그래서 100장씩 나누어서 복사하고 그 때마다 다른 사람들이 사용할 수 있게 통제할 수 있습니다.
이와 마찬가지로 쓰레드도 행동을 통제할 필요가 있는데 그 전에 쓰레드의 상태를 먼저 알아야 합니다.
스레드가 start()
하게 되면 위 그림과 같은 상태로 진행됩니다.
- 경우에 따라서 쓰레드는 실행 상태에서 실행 대기 상태로 가지 않을 수도 있습니다.
- 실행 상태에서 일시 정지 상태로 가기도 하는데, 일시 정지 상태는 쓰레드가 실행할 수 없는 상태입니다.
- 쓰레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야합니다.
이러한 쓰레드의 상태를 알 수 있는 방법이 getState()
메소드가 있습니다.getState()
의 쓰레드 상태에 따른 Thread.State
열거 상수가 있습니다.
Test Class
package com.azurealstn.sociallogin.Thread;
public class Test extends Thread {
private TargetThread targetThread;
public Test(TargetThread targetThread) {
this.targetThread = targetThread;
}
@Override
public void run() {
while (true) {
Thread.State state = targetThread.getState(); //쓰레드 상태
System.out.println("타겟 쓰레드 상태: " + state);
//객체 생성 상태일 경우 실행 대기 상태로 만듬
if (state == Thread.State.NEW) {
targetThread.start();
}
//종료 상태일 경우 while문 종료
if (state == Thread.State.TERMINATED) {
break;
}
try {
Thread.sleep(1000);
} catch (Exception e) {}
}
}
public static void main(String[] args) {
Test test = new Test(new TargetThread());
test.start();
}
}
TargetThread
package com.azurealstn.sociallogin.Thread;
public class TargetThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100000000; i++) {} //RUNNABLE 상태 유지
try {
Thread.sleep(1500); //TIMED_WAITING 생태 유지
} catch (Exception e) {}
for (int i = 0; i < 100000000; i++) {} //RUNNABLE 상태 유지
}
}
쓰레드 상태 순서 : NEW -> RUNNABLE -> TIMED_WAITING -> RUNNABLE -> TERMINATED
자세한 부분은 https://widevery.tistory.com/27 이 블로그를 참고해주세요.
쓰레드의 우선순위
멀티쓰레드는 동시성(Concurrency) 또는 병렬성(Parallelism)으로 실행됩니다.
- 동시성 : 멀티 작업을 위해 하나의 코어에서 멀티쓰레드가 번갈아가며 실행하는 성질
- 병렬성 : 멀티 작업을 위해 멀티 코어에서 개별 쓰레드를 동시에 실행하는 성질
싱글 코어 CPU를 이용한 멀티쓰레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업입니다. 즉, 실행하는 것이 워낙에 빨라서 병렬성으로 보일 뿐입니다.
쓰레드 스케줄링
쓰레드의 개수가 코어의 수보다 많을 경우 쓰레드를 어떤 순서에 의해 동시성으로 실행할 것인지를 결정해야 하는데 이것을 쓰레드 스케줄링이라고 합니다. 쓰레드 스케줄링에 의해 쓰레드들은 아주 짧은 시간에 번갈아가면서 run()
메소드를 조금씩 실행합니다.
우선순위는 1 ~ 10까지 부여가 되고 10이 가장 높습니다. 만약 우선순위를 변경하고 싶으면 setPriority()
메소드를 이용하면 됩니다.
Main 쓰레드
모든 자바 어플리케이션은 Main 쓰레드가 main()
메소드를 실행하면서 시작됩니다. 즉, Main 쓰레드는 main()
메소드의 코드 흐름입니다. Main 쓰레드가 없다면 멀티쓰레드가 나올 수 없습니다.
싱글 쓰레드의 경우 Main 쓰레드가 종료되면 프로세스도 종료되지만, 멀티쓰레드는 Main 쓰레드가 종료되더라도 실행 중인 쓰레드가 하나라도 있다면 프로세스는 종료되지 않습니다.
동기화
쓰레드 동기화란 여러 개의 쓰레드가 한 개의 자원을 사용하고자 할 때, 해당 쓰레드만 제외하고 나머지는 접근하지 못하도록 막는 것입니다.
멀티쓰레드로 인해 동기화를 제어해야하는 경우가 있습니다.synchronized
키워드를 사용하면 멀티쓰레드 상태에서 동일한 자원을 동시에 접근하게 되었을 때 동시 접근을 막게 됩니다.synchronized
를 사용하는 방법은 두 가지가 있습니다.
- 메소드에
synchronized
- 메소드에
synchronized
를 붙이면 메소드 전체가 임계 영역으로 설정됩니다. - 쓰레드는
synchronized
메소드가 호출되는 시점부터 해당 메소드가 포함된 객체의 Lock을 얻어 작업을 수행하다가 메소드가 종료되면 Lock을 반환합니다.
- 메소드에
- 블록에
synchronized
- 블록 앞에
synchronized
를 붙입니다. - 참조변수는 Lock을 걸고자 하는 객체를 참조하는 것이어야 합니다.
- 이 블록 안으로 들어가면 쓰레드는 지정된 객체의 Lock을 얻게 되고 블록을 벗어나면 Lock을 반납합니다.
- 블록 앞에
synchronized
: 단 하나의 쓰레드만 실행할 수 있는 메소드 또는 블록을 말합니다.
- 다른 쓰레드는 메소드나 블록이 실행이 끝날 때까지 대기해야 합니다.
wait()
메소드를 호출하면 쓰레드는 일시 정지 상태가 됩니다.notify()
나notifyAll()
메소드를 호출하면 다른 쓰레드가 실행 대기 상태가 됩니다.- 임계 영역은 멀티쓰레드 프로그램의 성능에 밀접하기 때문에 메소드 전체에 Lock을 거는 것보다는 블록에 Lock을 걸어서 임계 영역을 최소화하도록 해야 합니다.
데드락
쓰레드 데드락(Thread DeadLock)이란 멀티쓰레드 프로그래밍에서 동기화를 통해 Lock을 걸어 자원을 해당 쓰레드를 제외하고 나머지는 접근을 못하도록 막았습니다.
하지만 두 개의 쓰레드에서 서로가 가지고 있는 Lock이 해제되기를 기다리는 상태가 생길 수 있으며 이러한 상태를 교착상태(deadlock)이라고 합니다. 즉, 교착상태가 되면 어떠한 작업도 실행되지 못하고 그저 서로가 상대방의 작업이 끝나기만을 바라는 무한 대기 상태입니다.
References
'Java' 카테고리의 다른 글
[자바 라이브 스터디] 12. 어노테이션 (0) | 2021.10.19 |
---|---|
[자바 라이브 스터디] 11. 열거형 (0) | 2021.10.18 |
[자바 라이브 스터디] 09. 예외 처리 (0) | 2021.10.12 |
[자바 라이브 스터디] 08. 인터페이스 (0) | 2021.10.12 |
[자바 라이브 스터디] 07. 패키지 (0) | 2021.10.12 |
댓글