자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.
운영체제에 의해 메모리에 공간을 할당받아 실행중인 프로그램을 프로세스라 한다.
프로세스는 프로그램에 사용되는 데이터, 메모리 공간, 쓰레드로 구성이 된다.
프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다.
모든 프로세스는 최소 1개 이상의 쓰레드가 존재한다.
두개 이상의 쓰레드를 사용하는 프로세스를 멀티 쓰레드 프로세스라고 부른다.
경량 프로세스라고도 불리며 가장 작은 실행 단위이다.
Thread클래스와 Runnable인터페이스를 이용하여 구현할 수 있다.
Thread클래스는 Runnable 인터페이스를 구현한 클래스이다.
둘 중 어떤걸 사용하면 좋을까?
Thread 클래스를 확장할 필요가 있으면 Runnable 인터페이스를 구현하고 필요 없을 경우 Thread 클래스를 사용하면 된다.
public class Main {
static class ThreadSample extends Thread{
@Override
public void run() {
System.out.println("ThreadSample Run!!");
}
}
static class RunnableSample implements Runnable{
@Override
public void run() {
System.out.println("RunnableSample Run!!");
}
}
public static void main(String[] args) {
RunnableSample runnableSample = new RunnableSample();
new Thread(runnableSample).start();
ThreadSample threadSample = new ThreadSample();
threadSample.start();
}
}
위의 코드에서 메인메서드를 보면 runnable을 구현한 runnableSample은 바로 start()를 사용하지 못하고 new Thread의 매개변수로 이용해서 새로운 Thread객체를 이용해서 사용하는 것을 볼 수 있다. 이는 람다로 해결가능하긴 하지만 Theard클래스를 상속받아 사용하는게 더 간단해보인다.
쓰레드를 실행시키는 순서대로 작동하는지 확인해보려한다.
아래 코드는 runnble과 Thread가 돌아가면서 생성되고 실행되는 코드이다.
public static void main(String[] args) {
RunnableSample[] runnableArr = new RunnableSample[5];
ThreadSample[] threadArr = new ThreadSample[5];
for (int i = 0; i < runnableArr.length; i++) {
runnableArr[i] = new RunnableSample();
new Thread(runnableArr[i]).start();
threadArr[i] = new ThreadSample();
threadArr[i].start();
}
}
위 코드의 실행결과는 아래와 같다.
확인해보면 2번째 루프까지는 순서대로 실행되다가 3번째 루프부터 반복되지 않는걸 확인할 수 있다.
이 결과로 알 수있는건 쓰레드를 시작하더라도 바로 시작되는게 아니라 컴퓨터의 메모리 성능에 따라 실행된다는 것이다.
해당 메서드들은 쓰레드를 실행한다는 점은 같지만 start 메서드는 쓰레드가 작업하는 스택을 따로 만들어서 실행해준다.
run메서드는 인스턴스의 메서드 호출과 같기에 여러번 호출이 가능하지만 start 메서드는 한번 실행만 가능하다.
쓰레드 프로그래밍에서 여러 쓰레드를 시간과 자원에 낭비없이 사용하기 위해서는 쓰레드와 상태를 제어함으로써 효율성을 높일 수 있다.
상태 | 설명 |
---|---|
NEW | 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화블럭에 의해서 일시정지된 상태이며 락이 풀리기를 기다리는 상태 |
WATING | 쓰레드가 대기중인 상태 |
TIMED_WAITING | 특정 시간만큼 대기중인 상태 |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
gerState() 메서드를 통해 쓰레드의 상태를 확인할 수 있다.
아래는 쓰레드의 상태를 제어하는 메서드이다.
쓰레드는 priority(우선순위) 멤버 변수를 갖고 있다. 각 쓰레드별로 우선순위를 1~10까지 지정할 수 있으며 1이 가장 높은 순위이다.
기본값은 5이다.
setPriority()를 통해 지정할 수 있다.
Java를 실행하기 사용하는 main() 메서드가 메인 쓰레드이다.
Java로 만든 애플리케이션이 main 쓰레드 외의 다른 쓰레드를 실행하지 않으면 싱글 쓰레드 애플리케이션이다.
main쓰레드 외의 다른 쓰레드를 생성해서 실행하면 멀티 쓰레드 애플리케이션이 되는것이다.
여기서 생성되는 쓰레드는 일반 쓰레드와 데몬 쓰레드이 있고 이런 쓰레드들은 그룹을 가지게 된다.
우선 쓰레드 그룹에 대해 알아보겠다.
쓰레드 그룹은 서로 연관된 쓰레드들을 그룹으로 묶어서 관리하는 것을 말한다.
쓰레드 그룹은 쓰레드 생성시 쓰레드그룹을 매개변수로 넘겨줌으로써 속하게 할 수있다. 지정하지 않으면 메인 스레드 그룹이 된다.
데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하는 쓰레드로써 일반 쓰레드가 종료되면 데몬 쓰레드도 강제로 종료되는 스레드이다.
이러한 데몬 쓰레드의 대표적인 예로 JVM에서 Heap영역에 생성된 객체들을 정리하는 가비지컬렉션을 수행하는 스레드가 있다.
데몬 스레드는 쓰레드를 똑같이 생성해주고 setDaemon(true)를 호출함으로써 설정할 수 있다.
그렇게 하면 현재 쓰레드의 데몬 쓰레드가 된다.
멀티 쓰레드 프로세스에서는 여러 쓰레드가 메모리를 공유하기 때문에 한 쓰레드가 작업하던 부분을 다른 쓰레드가 접근하는 문제가 생길 수 있다.
이러한 문제를 해결하기위해 다른 쓰레드가 접근하지 못하도록 하는 작업이 동기화이다.
접근하지 못하는 영역을 설정해주면 되는데 이러한 영역을 임계영역이라고 한다. 임계영역은 synchronized 키워드를 이용하면 된다.
// 메서드 전체를 임계영역으로 설정
public synchronized void method1 () {
......
}
// 특정한 영역을 임계영역으로 설정
synchronized(객체의 참조변수) {
......
}
임계영역을 설정하면 해당 매서드에 들어가는 순간 lock을 얻어 작업을 수행하다 해당 메서드가 종료되면 lock을 반납한다.
임계영역은 메서드 전체를 설정하기보다 두번째 방법인 지역을 선택하고 해당 지역내에서 락이 걸릴 객체의 참조변수를 넘겨주는 방식을 이용하는게 낫다. 아무래도 다른 스레드가 전체 메소드일 경우 대기시간이 길어지기에 효율이 떨어진다.
lock
lock은 일종의 자물쇠 개념이다. 모든 객체는 lock을 하나씩 가지고 있으며 해당 객체의 lock을 가진 쓰레드만이 임계영역의 코드를 수행할 수 있게되고 다른 스레드는 대기상태가 된다.
데드락은 자원을 사용하는 쓰레드들이 서로 원하는 자원에 락을 가지고 있으면서 서로가 가지고 있는 다른 락을 가질려고 할 때 발생한다.
쓰레드 | 가지고 있는 락 | 원하는 락 |
---|---|---|
1 | A | B |
2 | B | A |
위의 표와 같이 서로 다른 쓰레드가 자원을 소유하고 있으면서 서로의 자원을 접근하려할 때 무한대기 상태에 빠지게 되는데 이러한 상황을 데드락
이라고 한다.