요즘 OS는 모두 멀티태스킹을 지원하는데 멀티태스킹을 가능하게 하는 요소 중 하나가 바로 '멀티스레딩'이다. 멀티스레딩은 하나의 프로세스 안에서 여러 개의 스레드가 동시에 작업을 수행하는 것을 의미한ㄷ.
스레드 구현
자바에서 스레드를 구현하는 방법은 크게 두 가지이다. 하나는 'Runnable' 인터페이스를 구현하는 방법이고, 다른 하나는 'Thread' 클래스를 상속하는 방법이다.
1. Runnable 인터페이스 구현
public class MyRunnable implements Runnable {
@Override
public void run() {
// 스레드가 수행할 작업 내용
}
}
2. Thread 클래스 상속
public class MyThread extends Thread {
@Override
public void run() {
// 스레드가 수행할 작업 내용
}
}
스레드 생성과 실행
스레드를 생성하기 위해서는 위에서 언급한 방법으로 구현한 클래스를 인스턴스화하여 'start()' 메서드를 호출해야 한다. 'start()' 메서드를 호출하면 JVM은 해당 스레드에게 별도의 콜 스택을 할당하고 작업을 수행하게 된다. 작업을 수행하기 위해 'run()' 메서드가 아닌 'start()' 메서드를 호출해야 한다는 것에 유의해야한다.
public class ThreadExample {
public static void main(String[] args) {
// Runnable 인터페이스를 구현한 클래스의 인스턴스 생성
MyRunnable myRunnable = new MyRunnable();
// Thread 클래스의 생성자에 Runnable 인스턴스를 전달하여 스레드 생성
Thread thread1 = new Thread(myRunnable);
// Thread 클래스를 상속한 클래스의 인스턴스 생성
MyThread myThread = new MyThread();
// start() 메서드를 호출하여 스레드 실행
thread1.start();
myThread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable 스레드: " + i);
try {
Thread.sleep(500); // 0.5초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 상속 스레드: " + i);
try {
Thread.sleep(500); // 0.5초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
스레드의 상태와 실행 제어
스레드는 여러 상태를 가질 수 있으며, NEW, RUNNABLE, BLOCKED, WAITING, TIME_WAITING, TERMINATED 등이 있다. 스레드의 상태는 해당 스레드의 생명 주기와 관련이 있다.
- NEW : 스레드가 생성되고 아직 start()가 호출되지 않은 상태
- RUNNABLE : 실행 중 또는 실행 가능 상태
- BLOCKED : 동기화 블럭에 의해 일시정지된 상태
- WAITING, TIME_WAITING : 실행가능하지 않은 일시정지 상태
- TERMINATED : 스레드 작업이 종료된 상태
스레드 간 협력 작업을 위해 'wait()'와 'notify()' 메서드를 활용할 수 있다. 'wait()' 메서드는 스레드를 Not Runnable 상태로 전환하며, 'notify()' 메서드는 대기 중인 스레드에게 다시 Runnable 상태로 전환할 수 있는 기회를 준다.
스레드 동기화
멀티스레딩 환경에서는 여러 스레드가 공유 자원에 접근할 때 동기화 문제가 발생할 수 있다. 이를 해결하기 위해 'synchronized' 키워드를 사용하여 임계영역과 lock을 활용하고 'wait()'와 'notify()'를 활용할 수도 있다.
동기화 방법
- 임계 영역(Critical Section) : 공유 자원에 대한 접근을 하나의 스레드만 가능하도록 제한하여 동시에 여러 스레드가 해당 자원에 접근하지 못하도록 보호하는 메커니즘
- 뮤텍스(Mutex) : 공유 자원에 대한 접근을 한 번에 하나의 스레드만 허용하도록 제어하는 동기화 방법이다. 뮤텍스는 보통 하나의 프로세스 내에서 사용되며, 여러 스레드가 경쟁적으로 자원에 접근하는 것을 방지한다.
- 이벤트(Event) : 특정한 사건이 발생하거나 조건이 충족되면 다른 스레드에게 알림을 보내는 동기화 방법
- 세마포어(Semaphore) : 한정된 개수의 자원을 여러 스레드가 동시에 사용하려 할 때, 접근 가능한 스레드의 개수를 제어하는 동기화 기법이다. 세마포어는 카운터 변수를 통해 스데르의 허용 가능 개수를 관리하며, 스레드가 자원을 사용하면 카운터가 감소하고 반납하면 증가한다.
- 대기 가능 타이머(Waitable Timer) : 특정 시간이 지나면 대기 중인 스레드를 깨워주는 동기화 방법이다. 스레드가 일시적으로 대기 상태로 들어가면서 특정 시간 후에 다시 실행될 수 있게 해준다.
synchronized 키워드 사용
public class SynchronizationExample {
private int sharedCounter = 0; // 여러 스레드가 공유하는 변수
public synchronized void incrementCounter() {
sharedCounter++; // 공유 변수를 증가시키는 메서드
System.out.println("Counter value: " + sharedCounter);
}
public static void main(String[] args) {
SynchronizationExample example = new SynchronizationExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.incrementCounter(); // 스레드 1이 공유 변수를 증가시킴
try {
Thread.sleep(500); // 0.5초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.incrementCounter(); // 스레드 2가 공유 변수를 증가시킴
try {
Thread.sleep(500); // 0.5초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start(); // 스레드 1 시작
thread2.start(); // 스레드 2 시작
}
}
wait()과 notify() 활용
스레드 간의 협력 작업을 강화하기 위해 try-catch문 내에 적절히 사용하면 좋다.
'Languages > Java' 카테고리의 다른 글
[Java] JVM (Java Virtual Machine) (0) | 2023.09.06 |
---|---|
[Java] 고유 락 (Intrinsic Lock) (0) | 2023.08.31 |
[Java] Casting (0) | 2023.08.10 |
[Java] 문자열 클래스 & 오브젝트 클래스 (0) | 2023.08.10 |
[Java] 직렬화 (Serialization) (0) | 2023.07.31 |