목표
자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.
학습할 것 (필수)
- Thread 클래스와 Runnable 인터페이스
- 쓰레드의 상태
- 쓰레드의 우선순위
- Main 쓰레드
- 동기화
- 데드락
Thread 와 Process의 차이점
프로세스와 쓰레드의 차이점은 무엇일까?
프로세스(process) : 실행 중인 프로그램 즉 OS 내에서 돌고 있는 프로그램을 의미한다.
쓰레드(Thread) : 프로세스 내에서 실제 작업을 수행
즉 프로세스는 하나 이상의 쓰레드를 가지고 있다고도 말할 수 있다.
그렇다면 왜 프로세스를 여러 개 즉 멀티 프로세스 프로그래밍이 아니라 멀티 쓰레드 프로그래밍을 할까?
그 이유는 한 프로세스 내의 자원은 다른 프로세스에서 접근할 수 없다. 또한 Context Switching 과정에 있어
많은 비용이 소모되기 때문에 멀티 프로세스 프로그래밍은 비용이 많이 든다고 생각할 수 있다.
따라서 멀티쓰레드 프로그래밍을 할 시 Process 내에 있는 자원을 공유 할 수 있음으로
Context Switching 비용을 조금 더 절감할 수 있다.
하지만 멀티쓰레드 프로그래밍은 아까도 말했지만 Process 내 자원을 공유 함으로
자원의 동기화(Synchronization) 문제가 일어날 수 있음으로 프로그래밍 시 주의를 요한다.
Thread 클래스와 Runnable 인터페이스
쓰레드를 구현하는데 java에서는
- Thread 클래스 를 상속
- Runnable 인터페이스를 구현
위와 같은 2가지 방법이 있다.
Runnable
Runnable 인터페이스를 구현 시에는 Run 메서드 안에 쓰레드가 할 코드를 작성해주면 된다.
public class MyThreadByRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
Thread
Thread 클래스를 상속받을 시에는 Thread 클래스 내 Run 메서드를 재정의(Override) 하면 된다.
public class MyThreadByThread extends Thread {
@Override
public void run() {
for (int i = 0; i <10; i++) {
System.out.println(getName());
}
}
}
이 코드를 실행할 때도 차이점이 존재한다.
Thread를 상속받은 MyThreadByThread 클래스는 객체를 생성 후 start() 메소드를 통해 시작하면 된다.
public class MainSolution {
public static void main(String[] args) {
MyThreadByThread thread =new MyThreadByThread();
thread.start();
}
}
하지만
Runnable을 구현하고 있는 MyThreadByRunnable은 Thread 생성자에 파라미터로 넘기어 Thread 타입으로
생성 후 start() 메소드를 통해 시작하면 된다.
public class MainSolution {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyThreadByRunnable());
thread1.start();
}
}
여기서 start() 메서드는 새로운 call stack을 만드는 메서드이다. 흐름은 다음과 같다.
따라서 두 코드를 같이 실행하면 쓰레드의 시작 순서는 상관없이 뒤죽박죽 섞여서 나올 것이다.
쓰레드의 실행 순서는 OS의 스케쥴러를 통해 결정되기 때문에 그렇다.
Main 쓰레드
자바 애플리케이션의 시작점이라고 말할 수 있다.
즉 Main 스레드는 프로그램이 시작될 때 실행을 시작하는 이고 자식 쓰레드 들은 Main으로부터 생성된다.
또한 Main 스레드가 종료되어도 자식 스레드가 종료되지 않으면 프로그램이 종료되지 않는다.
public static void main(String[] args) {
//code
}
데몬 쓰레드(daemon thread)
데몬 쓰레드는 일반 쓰레드를 보조하는 역할로써 일반 쓰레드와 동작 방식은 동일하지만
일반 쓰레드와의 가장 큰 차이점은
일반쓰레드가 종료된다면 데몬쓰레드의 작업이 남아있어도 강제 종료된다는 것이다.
코드를 보면 다음과 같다.
public class MainSolution {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
MyThreadByThread thread =new MyThreadByThread();
thread.setDaemon(true);
thread.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
쓰레드 우선순위
쓰레드 우선순위 란?
특정 쓰레드를 조금 더 가중치를 주는 것 임으로
절대로 가중치가 높은 쓰레드의 작업이 먼저 실행이 완료된다고 생각하면 안된다.
단지 가중치를 줌으로써 확률상 먼저 작업이 끝날 수 있다고 생각하는것이 좋다.
java 에서 쓰레드 우선순위를 주는 방법은
setPriority(int newPriority) 통해 설정 할 수 있으며
여기서 newPriority 의 최대 값은 10 최소 값은 1 이다.
우선순위를 설정해주지 않으면 중간 값인 5 가 자동으로 셋팅 된다.
쓰레드 상태
docs.oracle.com/javase/8/docs/api/java/lang/Thread.State.html
쓰레드의 상태의는 다음과 같이 정의 할 수 있다.
상태 | 열거 상수 | 설명 |
객체 생성 | NEW | 스레드 객체가 생성. 아직 start() 메소드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시 정지 |
WAITING | 다른 스레드가 통지할 때까지 기다리는 상태 |
TIMED_WAITING | 주어진 시간 동안 기다리는 상태 | |
BLOCKED | 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 | |
종료 | TERMINATED | 실행을 마친 상태 |
위의 표중 일시 정지 의 경우를 살펴보자.
일시 정지 상태란 Thread 가 다음 작업을 기다리거나 어떠한 행위를 기달리고 있는 경우 라고 생각하면된다.
즉 코드로 보자면 다음과 같다.
public class MainSolution {
public static void main(String[] args) {
System.out.println("main 쓰레드 시작");
MyThreadByThread myThreadByThread =new MyThreadByThread();
Thread myThreadByRunnable = new Thread(new MyThreadByRunnable());
myThreadByThread.start();
myThreadByRunnable.start();
String s = new Scanner(System.in).next(); // BLOCKED 상태 입력값을 대기하는중(I/O BLOCKING)
System.out.println("입력값은 "+s+"입니다");
try {
myThreadByThread.join(); //WAITING 상태 Main 쓰레드가 myThreadByThread 작업이 종료될때까지 기달림
myThreadByRunnable.join(); //WAITING 상태 Main 쓰레드가 myThreadByRunnable 작업이 종료될때까지 기달림
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main 쓰레드 :한숨 자고 종료하자..");
try {
Thread.sleep(1000); //TIMED_WAITING 상태 주어진 1초를 기달렸다가 종료됨
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main 쓰레드 종료");
}
}
자 다음과 위 코드 처럼 쓰레드의 상태를 조작할 수 있는 method들이 존재한다.
docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Thread.html
method | 설명 |
static void yield() | 현재 스레드가 프로세서의 현재 사용을 양보 할 의사가 있다는 스케줄러에 대한 힌트입니다. |
static void sleep(long millis) | 현재 실행중인 스레드가 지정된 밀리 초 동안 일시적으로 실행 중지 되도록합니다. |
void join(long millis) | 이 스레드가 작업이 완료 될 때 까지 기다립니다 |
void interrupt() | 이 스레드의 interrupted 상태를 true 로 변경합니다. |
동기화(synchronization)
글 시작 부분에 이런 말을 했었다.
멀티쓰레드 프로그래밍을 할 시 Process 내에 있는 자원을 공유 할 수 있음으로 ...
따라서 한 쓰레드가 진행중인 작업을 다른 쓰레드 쪽에서 간섭하는 일이 발생 할 수 있다.(not thread-safe)
이러한 문제점을 해결 하기위해 동기화(synchroniztion)을 진행하면 된다.
동기화를 한다는 뜻은 단 하나의 쓰레드만 영역 내의 코드를 수행할 수 있게 한다는 뜻이다.
동기화를 하는 방법은 2가지가 존재한다.
1. synchronized 키워드 사용
2. java.lang.Object 클래스의 wait(), notify(), notifyAll() 메소드 이용
1. synchronized 키워드 사용
동기화 블록(임의의 코드블록을 임계영역으로 지정)
synchronized(객체){
//code
}
해당 블록내의(임계영역) 객체에는 한 쓰레드에서만 접근이 가능함.
동기화 메서드(메소드 전체를 임계영역으로 지정)
public static synchronized void method(){
//code
}
해당 메서드에 쓰레드가 배정되면 작업이 다 완료 될때까지 다른 쓰레드에서는 접근이 불가능함
synchronized 사용 예제
Main
public class MainSolution {
public static void main(String[] args) {
SyncCount count = new SyncCount(); // 공유할 데이터 생성
SyncExThread jaeJoon = new SyncExThread("JaeJoon", count); //jaejoon 쓰레드생성
SyncExThread kim = new SyncExThread("KIM", count); //KIM 쓰레드 생성
jaeJoon.start();
kim.start();
}
}
공유 객체
public class SyncCount {
int sum = 0;
void add(){ //add 메서드가 불릴때마다 sum 의 값은 +10 씩 됩니다.
int n = sum;
Thread.yield(); // 충돌을 유발하기위한 코드 없어도됩니다
n +=10;
sum =n;
System.out.println(Thread.currentThread().getName()+":"+sum);
}
int getSum(){
return sum;
}
}
쓰레드
public class SyncExThread extends Thread {
SyncCount syncCount;
public SyncExThread(String name, SyncCount syncCount) {
super(name);
this.syncCount = syncCount;
}
@Override
public void run() {
int i= 0;
while (i<100){
syncCount.add();
i++;
}
}
}
해당 코드를 총 100번 add() 메서드를 각각의 쓰레드에게 실행하라고 했으니
현재 총 2개의 쓰레드 이니 100 * 2 = 200 총 200번의 add() 메서드가 실행되야한다.
기대값 : 2000
결과값 : 1990(실행 할때마다 정상일 수도 있고 값의 오차가 존재함)
이 처럼 한 쓰레드가 계산을 하기전에 다른 쓰레드로 작업 스케쥴이 넘어가서 중복계산이 발생 할 수있다.
즉 thread-safe 하지 않은 코드가 만들어 진것이다.
따라서 이 문제를 해결 하기위해서 위에서 배운 동기화 메서드(메소드 전체를 임계영역으로 지정) 사용하면 문제를 해결 할 수 있다.
공유객체(메소드 synchronized)
public class SyncCount {
int sum = 0;
synchronized void add(){ //add 메서드가 불릴때마다 sum 의 값은 +10 씩 됩니다.
int n = sum;
Thread.yield(); // 충돌을 유발하기위한 코드 없어도됩니다
n +=10;
sum =n;
System.out.println(Thread.currentThread().getName()+":"+sum);
}
int getSum(){
return sum;
}
}
기대값: 2000
결과값: 2000
2. java.lang.Object 클래스의 wait(), notify(), notifyAll() 메소드 이용
-> 임계블록 구역에서만 사용가능하니 주의
메소드명 | 설명 |
wait() | 다른 스레드가 이객체의 notify() 를 불러줄 때까지 대기한다. |
notify() | 이 객체에 대기 중인 스레드를 깨워 RUNNABLE 상태로 변경 , 2개 이상의 스레드가 대기 중이라도 오직 한 개의 스레드만 깨워 RUNNABLE 상태로 변경 |
notifyAll() | 이 객체에 대기 중인 모든 스레드를 깨우고 모두 RUNNABLE 상태로 변경 |
위의 synchronized 키워드 만 사용시 lock 을 한 쓰레드에서 차지 하기 때문에 효율이 매우 떨어진다.
그러한 문제점을 해결 하기 위해서 위 3가지의 메서드를 이용하여 코딩을 진행하면 된다.
wait 은 -> Lock 을 반환하고 waiting 구역에서 대기하게 된다.
notify 은 waiting 구역에 잠들어 있는 쓰레드중 무작위( 개발자가 컨트롤 할 수 없음) 를 깨워 쓰레드를 Runnable 상태로 변경한다.
norifyAll 은 waiting 구역에 있는 모든 쓰레드를 깨우고 RUNNABLE 상태로 변경 하지만 그렇다고 해서 synchronized 임계영역 안에 모든 쓰레드가 들어가는 것은아니고 어떠한 하나의 쓰레드가 Lock 얻게 된다.
데드락(Dead Lock)
두 개 이상의 작업이 서로 다른 자원을 점유 하고 있으며 상대방이 가진 자원을 서로 원하는경우
따라서 Lock 된 객체에 서로 접근 하지 못하여 프로그램이 무한정 대기하는 상태를 말한다.
위의 설명처럼 데드락이 발생하는 이유는 4가지의 조건이 동시에 만족해서 발생하는데 그조건은 다음과같다.
1. 상호배제(Mutual Exclusion)
-> 이미 한 쓰레드가 점유하고 있는 자원에 대해서 다른 쓰레드에서 접근 불가능 해야함.
2. 점유와 대기 (Hold and Wait)
-> 다른프로세스가 자원을 이용하고 있다면 그 프로세스가 끝날때까지 대기해야함
3. 비선점 (Non Preemptive)
-> 다른 프로세스의 자원을 가져 올 수 없음
4. 환형대기(Circle wait)
-> 두 개의 프로세스가 서로의 프로세스의 자원을 원하는 경우
'Java > live-study' 카테고리의 다른 글
[Java] 백기선 라이브스터디 12주차: 애노테이션 (0) | 2021.02.03 |
---|---|
[Java] 백기선 라이브 스터디 11주차 과제: Enum (0) | 2021.01.28 |
[Java] 백기선 라이브스터디 9주차 : 예외 처리 (0) | 2021.01.14 |
[JAVA] 백기선 라이브스터디 8주차 과제: 인터페이스 (0) | 2021.01.06 |
(JAVA)백기선 라이브 스터디 7주차 : 패키지 (0) | 2021.01.01 |