목표
자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.
목차
1. Thread 클래스와 Runnable 인터페이스
2. 쓰레드의 상태
3. 쓰레드의 우선순위
4. Main 쓰레드
5. 동기화
6. 데드락
멀티태스킹과 쓰레드의 개념
멀티태스킹: 여러 작업을 동시에 처리
미디어 플레이어 같은 경우 오디오, 비디오, 입력처리 태스크가 동시에 이루어 진다.
멀티태스킹은 응용프로그램의 여러 태스크가 동시에 진행되게 하는 기법으로, 응용프로그램의 목적을 효율적으로 달성하게 해준다.
쓰레드: 운영체제나 JVM에서 태스크를 실행하는 단위로서, 운영체제나 JVM에 의해 관리되는 단위.
자바 쓰레드를 보면 JVM은 하나의 태스크를 실행하기 위해 하나의 쓰레드를 만들고, 쓰래드가 태스크 코드를 실행하게끔 한다.
멀티프로세싱: 하나의 응용프로그램을 여러 개의 프로세스로 구성하여 각 프로세스가 하나의 작업을 처리하는 기법
- 각 프로세스는 고유한 메모리 영역을 가지고 있어 독립적으로 실행한다.
- 프로세스들은 변수를 공유할 수 없다.
- 프로세스 사이의 통신(IPC)이 어려워 오버헤드가 크다.
- 프로세스 사이의 문맥 교환에 따른 과도한 작업량와 시간 소모 문제
이런 문제를 개선한 방법이 멀티쓰레딩이다.
멀티쓰레딩: 하나의 응용프로그램을 동시처리가 가능한 여러 작업으로 분할하여 작업의 개수만큼 쓰레드를 생성하여 각 쓰레드로 하여금 하나의 작업을 처리하도록하는 기법
한 쓰레드가 대기하는 동안 다른 쓰레드를 실행하여 시간 지연을 줄이고, 자원의 비효율적 사용을 개선한다.
- 쓰레드는 자원과 메모리를 공유
- 통신에 따른 오버헤드가 작다.
- 문맥 교환이 빠르다.
자바에는 프로세스가 존재하지 않고 쓰레드 개념만 있다.
자바 쓰레드는 JVM에 의해 스케줄되는 실행 단위 코드 블록이다.
1. Thread 클래스와 Runnable 인터페이스
(1) Thread 클래스
java.lang.Thread
Thread 클래스의 메소드 | 내용 |
Thread() Thread(Runnable target) Thread(String name) Thread(Runnable target, String name) |
- 쓰레드 객체 생성 - Runnable 객체인 target을 이용하여 쓰레드 객체 생성 - 이름이 name인 쓰레드 객체 생성 - Runnable 객체를 이용하며, 이름이 name인 쓰레드 객체 생성 |
void run() | 쓰레드 코드로서 JVM에 의해 호출됨 이 메소드를 오버라이딩하여 쓰레드 코드를 작성해야 함 이 메소드가 종료하면 쓰레드도 종료 |
void start() | JVM에게 쓰레드 실행 요청 |
void interrupt() | 쓰레드 강제 종료 |
static void yield() | 다른 쓰레드에게 실행을 양보 이 때 JVM은 쓰레드 스케줄링을 시행하며 다른 쓰레드를 선택하여 실행 |
void join() | 쓰레드가 종료할 때까지 기다림 |
long getId() | 쓰레드 ID 값 리턴 |
String getName() | 쓰레드 이름 리턴 |
int getPriority() | 쓰레드 우선순위 값 리턴, 1~10 |
void setPriority(int n) | 쓰레드 우선순위 값을 n으로 변경 |
Thread.State getState() | 쓰레드 상태 값 리턴 |
static void sleep(long mills) | 쓰레드는 mills 시간 동안 잔다 |
static Thread currentThread() | 현재 실행중인 쓰레드 객체의 레퍼런스 리턴 |
(2) Thread 클래스를 이용하여 만들기
Thread 클래스를 상속받아 만든다. Thread를 상속받으면 다른 클래스를 상속 받을 수 없다.
class TimeThread extends Thread{
}
class TimeThread extends Thread{
int n=0;
@Override
public void run(){
while(true){
System.out.println(n);
n++;
try{
sleep(1000); // 1초동안 잠 잔다
}
catch(InterruptedException e){
return;
}
}
}
}
public class TestThread{
public static void main (String[] args){
TimeThread th=new TimeThread();
th.start();
}
}
실행결과
0
1
2
...
(3) RUNNABLE 인터페이스를 이용하여 만들기
class runnableThread implements Runnable{
}
class TimeThread implements Runnable{
int n=0;
@Override
public void run(){
while(true){
System.out.println(n);
n++;
try{
Thread.sleep(1000); // 1초동안 잠 잔다
}
catch(InterruptedException e){
return;
}
}
}
}
public class ThreadTest{
public static void main (String[] args){
Runnable r = new TimeThread();
Thread th = new Thread(r);
th.start();
}
}
2. 쓰레드의 상태
쓰레드는 생명 주기를 가지며 여러 상태의 변이를 거친다.
- NEW
쓰레드 생성은 됐지만 실행 준비가 되지 않은 상태. start() 메소드가 호출되면 RUNNABLE 상태가 된다.
- RUNNABLE
쓰레드가 실행되고 있거나, 실행 준비되어 스케줄링을 기다리는 상태
- BLOCK
쓰레드가 I/O 작업을 실행하여 I/O 작업의 완료를 기다리면서 멈춘 상태. 그동안 JVM은 다른 쓰레드를 스케줄링 한다.
- WAITING
쓰레드가 어떤 객체 a애 대해 a.wait()을 호출하여, 다른 쓰레드가 a.notify(), a.nofityAll()을 불러줄 때까지 무한정 기다리는 상태
- TIMED_WAITING
쓰레드가 sleep(long n)을 호출하여 n밀리초 동안 잠을 자는 상태
- TERMINATED
쓰레드 종료 상태
3. 쓰레드의 우선순위
JVM은 우선순위를 기준으로 쓰레드를 스케줄링한다.
main 쓰레드는 보통값(5)의 우선순위를 가지며, 자식 쓰레드는 부모 쓰레드읠 우선순위 값을 물려받는다.
thread.setPriority(1~10);
thread.setPriority(Thread.MIN_PRIORITY); // 최솟값 1
thread.setPriority(Thread.NORM_PRIORITY); // 보통값 5
thread.setPriority(Thread.MAX_PRIORITY); // 최댓값 10
void setPriority(int newPriority) // 우선순위 값 변경
class MutiThread extends Thread {
@Override
public void run(){
long id = Thread.currentThread().getId(); // 쓰레드 아이디
String name = Thread.currentThread().getName(); // 쓰레드 이름
Thread.State s = Thread.currentThread().getState(); // 쓰레드 상태 값
int priority = Thread.currentThread().getPriority(); // 쓰레드 우선순위 값
System.out.println("쓰레드 아이디 = " + id + ", 쓰레드 이름 = " + name + ", 쓰레드 상태값 = " + s + ", 쓰레드 우선순위값 = " + priority);
}
}
public class ThreadPriorityTest {
public static void main(String[] args) {
Thread thread0=new MutiThread();
Thread thread1=new MutiThread();
Thread thread2=new MutiThread();
thread0.setPriority(Thread.MAX_PRIORITY); // 우선순위 최대로 변경
thread1.setPriority(Thread.MIN_PRIORITY); // 우선순위 최소로 변경
thread0.start();
thread1.start();
thread2.start();
}
}
실행결과
쓰레드 아이디 = 16, 쓰레드 이름 = Thread-0, 쓰레드 상태값 = RUNNABLE, 쓰레드 우선순위값 = 10
쓰레드 아이디 = 17, 쓰레드 이름 = Thread-1, 쓰레드 상태값 = RUNNABLE, 쓰레드 우선순위값 = 1
쓰레드 아이디 = 18, 쓰레드 이름 = Thread-2, 쓰레드 상태값 = RUNNABLE, 쓰레드 우선순위값 = 5
우선순위를 보면 thread0 - 2 - 1 순서로 실행되는 것이 맞지만 실제 실행결과는 그렇지 않았다.
우선순위를 지정해도 실제 작업순서는 달라질 수 있다.
실제로 여러번 실행해본 결과 실행할 때 마다 결과가 달라졌다.
4. Main 쓰레드
JVM은 자바 응용프로그램 실행 전, 사용자 쓰레드를 하나 만들어서 main() 메소드를 실행하는데 이 쓰레드를 메인 쓰레드라 한다. 실행 시작 주소는 main() 메소드의 첫 코드가 된다.
자바 응용프로그램의 main() 메소드가 실행되면 데몬쓰레드인 가비지 컬렉션 쓰레드와 사용자 쓰레드인 메인 쓰레드 2가지 쓰레드가 생긴다.
- 데몬쓰레드(daemon thread): 응용프로그램이 실행되는 동안 관리를 위해 존재하는 쓰레드
- 사용자쓰레드(user thread): 응용프로그램에서 생성한 쓰레드
5. 동기화
멀티쓰레드에서 쓰레드가 공유 데이터에 동시에 접근하는 경우 이에 대한 해결책이 쓰레드 동기화(Thread Synchronization)이다.
쓰레드 동기화란 공유 데이터에 접근하고자 하는 다수의 쓰레드가 서로 순서대로 충돌 없이 고유데이터를 배타적으로 접근하기 위해 상호 협력하는 것을 말한다.
자바에서 쓰레드 동기화 방법
(1) synchronized로 동기화 블록 지정
쓰레드가 공유 데이터에 접근할 때, 순차적으로 실행되도록 제어하는 기법으로 쓰레드가 공유 데이터에 접근하면 데이터에 락을 걸어 다른 쓰레드가 사용을 위해 대기하도록 만든다.
synchronized 키워드는 임의의 코드 블록을 동기화가 설정된 임계 영역으로 지정한다.
- 메소드 전체를 임계 영역으로 설정
synchronized void add() { // add()메소드 전체를 임계영역으로 설정
int n = getCurrentSum(); // 현재 합 구하기
n+=1; // 증가
setCurrent(n); // 증가된 결과 기록
}
메소드가 호출되면 자동으로 동기화 된다. add() 메소드가 실행되는 중 다른 쓰레드가 add()를 호출하면 실행되고 있는 쓰레드가 add() 메소드 사용을 마칠 때까지 대기한다.
- 코드 블록을 임계 영역으로 설정
void execute(){
...
synchronized(this){
int n = getCurrentSum();
n+=1;
setCurrentSum(n);
}
...
}
synchronized 블록 내의 코드를 사용하고 있을 때 다른 쓰레드가 이 블록에 접근하면 이미 사용하고 있는 쓰레드가 실행을 마칠 때까지 대기한다.
(2) wait()-notify() 메소드로 쓰레드 실행 순서 제어
공유 메모리를 통해 두 쓰레드가 데이터를 주고 받을 때, 공유 메모리에 대해 두 쓰레드가 동시에 접근하는 producer-consumer 문제를 해결할 수 있다.
- wait()
다른 쓰레드가 이 객체의 notify()를 불러줄 때까지 대기
- notify()
이 객체에 대기 중인 쓰레드를 깨워 RUNNABLE 상태로 만든다.
- notifyAll()
이 객체에 대기 중인 모든 쓰레드를 깨우고 모두 RUNNABLE 상태로 만든다.
6. 데드락(교착상태)
동기화를 이용해 공유 데이터에 동시에 접근하지 못하게 락을 걸었다. 이 때 쓰레드들이 서로 락을 풀리기를 기다리는 상태가 발생할 수 있는데 이를 교착상태(deadlock)라고 한다.
교착상태에서는 어떤 작업도 실행되지 못하며 서로의 작업이 끝나길 기다리는 무한정 대기 상태가 된다.
데드락 발생 조건
- 상호 배제 (Mutual Exclusion) : 한 번에 한개의 프로세스만이 공유 자원을 사용할 수 있어야 한다.
- 점유와 대기 (Hold and Wait) : 최소한 하나의 자원을 점유하고 있으면서 다른 프로세스에 할당되어 사용되고 있는 자원을 추가로 점유하기 위해 대기하는 프로세스가 있어야 한다.
- 비선점 (Non Preemptive) : 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 뺏을 수 없다.
- 환형대기 (Circle Wait) :공유 자원과 공유 자원을 사용하기 위해 대기하는 프로세스들이 원형으로 구성되어 있어 자신에게 할당된 자원을 점유하면서 앞이나 뒤에 있는 프로세스의 자원을 요구해야 한다.
명품 JAVA Programming/황기태,김효수
2021 시나공 정보처리기사 필기
'자바 스터디' 카테고리의 다른 글
12주차 과제: 애노테이션 (0) | 2022.06.13 |
---|---|
11주차 과제: Enum (0) | 2022.06.11 |
9주차 과제: 예외 처리 (0) | 2022.06.08 |
8주자 과제: 인터페이스 (0) | 2022.06.04 |
7주차 과제: 패키지 (0) | 2022.06.04 |