일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- gpt활용팁
- 창의적도구
- java스터디
- java 메서드 오버라이딩
- java 애노테이션
- 티스토리챌린지
- 생성형AI
- 오블완
- javautil패키지
- Java 자료구조
- java 람다식
- java 패키지
- ai활용법
- java final 키워드
- binraytree
- javascript
- 화살표연산자
- java 제네릭
- java objact클래스
- java 추상 클래스
- java반복문
- java super메소드
- JAVA데이터타입
- javatime
- this키워드
- java 이진트리
- java
- asyncawait
- java_this
- import 키워드
- Today
- Total
코딩쿠의 코드 연대기
Java 멀티쓰레드 프로그래밍 본문
학습목표
- Thread 클래스와 Runnable 인터페이스
- 쓰레드의 상태
- 쓰레드의 우선순위
- Main 쓰레드
- 동기화
- 데드락
Thread 클래스와 Runnable 인터페이스
Java에서 스레드를 생성하고 관리하는 방법에는 Thread
클래스와 Runnable
인터페이스, 두 가지 주요 방식이 있습니다. 둘 다 멀티스레딩을 구현하는 데 사용되지만, 몇 가지 중요한 차이점이 있습니다.
1. Thread 클래스
Thread
클래스는 스레드를 나타내는 클래스입니다.- 스레드를 생성하려면
Thread
클래스를 상속하고run()
메서드를 오버라이드하여 스레드가 실행할 작업을 정의합니다. start()
메서드를 호출하여 스레드를 시작합니다.
Java
public class MyThread extends Thread {
@Override
public void run() {
// 스레드가 실행할 작업
}
}
// 사용 예시
MyThread thread = new MyThread();
thread.start();
2. Runnable 인터페이스
Runnable
인터페이스는 스레드가 실행할 작업을 정의하는 데 사용됩니다.Runnable
인터페이스를 구현하는 클래스는run()
메서드를 구현해야 합니다.Thread
클래스의 생성자에Runnable
객체를 전달하여 스레드를 생성합니다.
Java
public class MyRunnable implements Runnable {
@Override
public void run() {
// 스레드가 실행할 작업
}
}
// 사용 예시
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
Thread 클래스와 Runnable 인터페이스의 차이점
기능 | Thread 클래스 | Runnable 인터페이스 |
---|---|---|
스레드 생성 방법 | Thread 클래스 상속 |
Runnable 인터페이스 구현 |
run() 메서드 |
오버라이드 | 구현 |
스레드 시작 | start() 메서드 호출 |
Thread 객체 생성 후 start() 메서드 호출 |
다중 상속 | 불가능 | 가능 |
코드 재사용 | 제한적 | 용이 |
어떤 방식을 선택해야 할까요?
일반적으로 Runnable
인터페이스를 사용하는 것이 더 좋은 방법으로 간주됩니다. 왜냐하면:
- 다중 상속: Java는 다중 상속을 허용하지 않으므로
Thread
클래스를 상속하면 다른 클래스를 상속할 수 없습니다.Runnable
인터페이스를 사용하면 다른 인터페이스를 구현하거나 다른 클래스를 상속할 수 있습니다. - 코드 재사용:
Runnable
객체는 여러 스레드에서 재사용할 수 있습니다. - 유연성:
Runnable
인터페이스는 익명 클래스나 람다 표현식으로 구현하여 코드를 간결하게 만들 수 있습니다.
하지만 Thread
클래스의 기능을 확장해야 하는 경우에는 Thread
클래스를 상속하는 것이 더 적합할 수 있습니다.
결론
Thread
클래스와 Runnable
인터페이스는 모두 Java에서 스레드를 생성하고 관리하는 데 사용되는 중요한 도구입니다. 둘 다 장단점이 있으므로 상황에 맞는 적절한 방식을 선택하는 것이 중요합니다. 일반적으로는 Runnable
인터페이스를 사용하는 것이 더 유연하고 효율적인 방법입니다.
Java 쓰레드의 상태, 쓰레드의 우선순위
Java 스레드의 6가지 상태
- NEW (생성)
- 스레드 객체가 생성되었지만 아직
start()
메서드가 호출되지 않은 상태입니다. - 이 상태에서는 스레드가 실행 준비를 하지 않습니다.
- 스레드 객체가 생성되었지만 아직
- RUNNABLE (실행 가능)
start()
메서드가 호출된 후 실행 대기 상태에 들어갑니다.- 실제로 CPU에서 실행 중이거나, 실행을 기다리는 상태를 포함합니다.
- 여기서 RUNNING이라는 별도 상태는 존재하지 않습니다. RUNNABLE 상태에서 CPU를 점유하면 실행 중이 되는 것으로 간주합니다.
- BLOCKED (블록)
- 특정 모니터 락(예:
synchronized
블록)을 획득하기 위해 대기 중인 상태입니다. - 다른 스레드가 락을 해제하면 RUNNABLE 상태로 전환될 수 있습니다.
- 특정 모니터 락(예:
- WAITING (대기)
- 명시적으로
wait()
메서드나 특정 동작(Thread.join()
)에 의해 무기한 대기 중인 상태입니다. - 다른 스레드가
notify()
또는notifyAll()
메서드를 호출해야 상태를 깨고 실행 대기로 돌아갑니다.
- 명시적으로
- TIMED_WAITING (시간 제한 대기)
sleep()
,join(long timeout)
,wait(long timeout)
과 같은 메서드로 인해 일정 시간 동안 대기하는 상태입니다.- 시간이 만료되거나 명시적으로 깨우는 동작이 수행되면 RUNNABLE 상태로 돌아갑니다.
- TERMINATED (종료)
- 스레드의 실행이 완료되었거나, 종료된 상태입니다. 이 상태에서는 스레드를 다시 실행할 수 없습니다.
쓰레드 우선순위의 특징
- 우선순위 범위
- Java에서 쓰레드 우선순위는 1부터 10까지 설정 가능하며, 숫자가 높을수록 우선순위가 높습니다. 기본값은
NORM_PRIORITY(5)
입니다. Thread
클래스에서 제공하는 상수:Thread.MIN_PRIORITY
(1): 최소 우선순위Thread.NORM_PRIORITY
(5): 기본 우선순위Thread.MAX_PRIORITY
(10): 최대 우선순위
- 쓰레드의 우선순위는
setPriority()
메서드를 사용하여 변경할 수 있습니다.
- Java에서 쓰레드 우선순위는 1부터 10까지 설정 가능하며, 숫자가 높을수록 우선순위가 높습니다. 기본값은
주의: 우선순위는 쓰레드 스케줄링에 영향을 미치지만, 항상 우선순위가 높은 쓰레드가 먼저 실행되는 것은 아닙니다. 운영체제의 스케줄링 정책에 따라 달라질 수 있습니다.
- 우선순위의 역할
- 우선순위는 스레드 스케줄러에게 힌트를 제공하는 역할을 합니다.
- 우선순위가 높은 쓰레드는 CPU 자원을 할당받을 가능성이 더 높습니다.
- 하지만 운영체제의 스케줄링 정책에 따라 결과는 달라질 수 있습니다.
- Java의 스레드는 운영체제 종속적입니다. 즉, 우선순위는 반드시 우선 실행을 보장하지 않습니다.
- 예를 들어, Windows와 Linux에서는 스케줄링 방식이 다를 수 있습니다.
- 주의할 점
- 높은 우선순위를 가진 스레드가 낮은 우선순위의 스레드를 완전히 "기아 상태"로 만들 수는 없습니다. 이는 Java의 공정성(fairness) 보장에 기반합니다.
- 그러나 우선순위를 과도하게 조정하면 프로그램의 동작이 예측하기 어려워질 수 있으므로 사용 시 신중해야 합니다.
예제 분석
Java
public class ThreadPriorityExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 1: " + i);
}
});
thread1.setPriority(Thread.MAX_PRIORITY);
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2: " + i);
}
});
thread2.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
}
}
이 예제에서는 thread1
의 우선순위를 MAX_PRIORITY
로 설정하고 thread2
의 우선순위를 MIN_PRIORITY
로 설정했습니다. 따라서 thread1
이 thread2
보다 먼저 실행될 가능성이 높습니다.
참고:
- 쓰레드의 상태는
getState()
메서드를 사용하여 확인할 수 있습니다. Thread
클래스는 쓰레드를 제어하기 위한 다양한 메서드를 제공합니다. (sleep()
,join()
,interrupt()
등)- 멀티스레딩 프로그래밍은 동기화 문제를 발생시킬 수 있으므로 주의해야 합니다. (
synchronized
키워드,wait()
,notify()
등을 사용하여 동기화 문제를 해결할 수 있습니다.)
- 우선순위 설정
thread1
은MAX_PRIORITY(10)
,thread2
는MIN_PRIORITY(1)
로 설정되어 있습니다.
- 결과
- 우선순위 때문에 thread1이 CPU를 먼저 점유할 가능성이 높습니다. 하지만, 결과는 운영체제와 JVM의 스케줄링 정책에 따라 달라질 수 있습니다.
- 두 스레드가 거의 동시에 실행되거나 교차 출력될 수도 있습니다.
- 실행 순서
- 예를 들어, 출력이 다음과 같이 나타날 가능성이 있습니다:
Thread 1: 0 Thread 1: 1 Thread 1: 2 Thread 2: 0 Thread 2: 1
- 그러나 완전히 다른 순서로 출력될 가능성도 존재합니다.
추가 주의사항
- setPriority() 남용 금지
- 스레드 우선순위를 지나치게 조정하면 프로그램이 특정 환경에서는 정상 동작하더라도 다른 JVM이나 운영체제에서 예상치 못한 동작을 보일 수 있습니다.
- 스레드 우선순위는 비결정적
- Java 스레드의 실행 순서는 완전히 우선순위에 의존하지 않으며, 이 점을 항상 고려해야 합니다.
- 스케줄링 정책에 따라 달라짐
- 운영체제 스케줄러에 따라 우선순위가 무시될 가능성도 있으므로, 우선순위를 중요한 동작 논리에 포함시키는 것은 바람직하지 않습니다.
결론
설명은 대체로 정확하지만, "thread1이 thread2보다 먼저 실행될 가능성이 높다"는 것은 운영체제와 JVM에 따라 달라질 수 있다는 점을 기억해야 합니다. Java 쓰레드의 우선순위는 스케줄러에게 힌트를 줄 뿐이며, 결정적인 동작은 보장하지 않습니다.
Java Main 쓰레드
Java에서 Main 쓰레드는 프로그램 실행 시 JVM에 의해 자동으로 생성되는 첫 번째 쓰레드입니다. 모든 Java 애플리케이션은 Main 쓰레드에서 시작하며, main()
메서드가 이 쓰레드에서 실행됩니다.
Main 쓰레드의 역할
- 프로그램 실행 시작점:
main()
메서드를 실행하여 프로그램의 실행 흐름을 시작합니다. - 다른 쓰레드 생성: 필요에 따라 다른 쓰레드를 생성하고 관리합니다.
- 프로그램 종료:
main()
메서드가 종료되면 Main 쓰레드도 종료되고, 일반적으로 프로그램도 함께 종료됩니다. (단, 데몬 쓰레드가 아직 실행 중인 경우는 예외)
Main 쓰레드의 특징
- 유일성: Java 프로그램은 항상 하나의 Main 쓰레드를 가집니다.
- 비(非) 데몬 쓰레드: Main 쓰레드는 데몬 쓰레드가 아니므로, Main 쓰레드가 종료될 때까지 JVM은 실행 상태를 유지합니다.
- 제어 가능:
Thread.currentThread()
메서드를 사용하여 Main 쓰레드의 참조를 얻고, 이름 변경이나 우선순위 설정 등의 제어가 가능합니다.
예제
Java
public class MainThreadExample {
public static void main(String[] args) {
// Main 쓰레드의 참조를 얻습니다.
Thread mainThread = Thread.currentThread();
// Main 쓰레드의 이름을 출력합니다.
System.out.println("현재 쓰레드 이름: " + mainThread.getName());
// Main 쓰레드의 우선순위를 출력합니다.
System.out.println("현재 쓰레드 우선순위: " + mainThread.getPriority());
}
}
Main 쓰레드와 다른 쓰레드의 관계
Main 쓰레드는 다른 쓰레드를 생성하고 실행할 수 있습니다. Main 쓰레드가 종료되더라도, 생성된 다른 쓰레드들이 데몬 쓰레드가 아니라면 계속 실행될 수 있습니다.
주의 사항
- 무한 루프: Main 쓰레드가 무한 루프에 빠지면 프로그램이 종료되지 않을 수 있습니다.
- 예외 처리: Main 쓰레드에서 발생한 예외를 처리하지 않으면 프로그램이 비정상적으로 종료될 수 있습니다.
Main 쓰레드는 Java 프로그램의 시작과 끝을 담당하는 중요한 역할을 합니다. Main 쓰레드의 동작 원리와 특징을 이해하는 것은 Java 멀티스레딩 프로그래밍의 기초입니다.
Java 쓰레드 동기화
Java에서 멀티스레딩은 프로그램의 성능을 향상시키는 강력한 도구이지만, 여러 스레드가 동시에 같은 자원에 접근하면 데이터 불일치 문제가 발생할 수 있습니다. 이를 경쟁 조건(Race Condition)이라고 합니다.
쓰레드 동기화는 여러 스레드가 공유 자원에 안전하게 접근하고, 데이터의 일관성을 유지하기 위한 기술입니다. Java는 쓰레드 동기화를 위한 다양한 메커니즘을 제공합니다.
1. synchronized
키워드
- Java에서 가장 기본적인 동기화 방법입니다.
synchronized
키워드를 사용하여 메서드 또는 코드 블록을 임계 영역(Critical Section)으로 지정할 수 있습니다.- 임계 영역은 한 번에 하나의 쓰레드만 접근할 수 있습니다.
synchronized
키워드는 객체의 내재 락(Intrinsic Lock)을 사용하여 동기화를 구현합니다.
Java
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2. volatile
키워드
- 변수에
volatile
키워드를 사용하면 해당 변수에 대한 읽기와 쓰기가 항상 주 메모리(Main Memory)에서 이루어지도록 합니다. - 여러 쓰레드가 공유하는 변수의 가시성(Visibility)을 보장합니다.
volatile
키워드는 경쟁 조건을 완전히 해결할 수는 없지만, 간단한 경우에 유용하게 사용할 수 있습니다.
Java
public class SharedData {
public volatile boolean isRunning = true;
}
3. Lock
인터페이스
java.util.concurrent.locks
패키지에 포함된Lock
인터페이스는synchronized
키워드보다 더 많은 기능을 제공합니다.ReentrantLock
,ReadWriteLock
등 다양한 구현체가 있습니다.lock()
메서드로 락을 획득하고,unlock()
메서드로 락을 해제합니다.try-finally
블록을 사용하여 락을 안전하게 해제하는 것이 중요합니다.
Java
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
4. Atomic
클래스
java.util.concurrent.atomic
패키지에 포함된Atomic
클래스는 원자적 연산(Atomic Operation)을 제공합니다.AtomicInteger
,AtomicLong
,AtomicBoolean
등 다양한 클래스가 있습니다.- 원자적 연산은 경쟁 조건 없이 변수를 업데이트할 수 있는 방법입니다.
Java
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
5. Concurrent
컬렉션
java.util.concurrent
패키지에는 동기화 기능을 내장한 컬렉션 클래스들이 있습니다.ConcurrentHashMap
,ConcurrentLinkedQueue
등이 있습니다.- 이러한 컬렉션들은 멀티스레드 환경에서 안전하게 사용할 수 있도록 설계되었습니다.
주의 사항
- 과도한 동기화는 성능 저하를 야기할 수 있습니다.
- 데드락(Deadlock)과 같은 문제가 발생하지 않도록 주의해야 합니다.
쓰레드 동기화는 멀티스레드 프로그래밍에서 매우 중요한 개념입니다. 적절한 동기화 기술을 사용하여 데이터의 일관성을 유지하고 안전한 멀티스레드 프로그램을 작성해야 합니다.
Java 쓰레드 데드락
Java에서 데드락(Deadlock)은 두 개 이상의 쓰레드가 서로가 가진 자원을 무한정 기다리는 상태를 말합니다.
데드락 발생 조건
데드락은 다음 네 가지 조건이 동시에 만족될 때 발생합니다.
- 상호 배제(Mutual Exclusion): 한 번에 하나의 쓰레드만 자원을 사용할 수 있습니다.
- 점유 대기(Hold and Wait): 자원을 가진 쓰레드가 다른 자원을 기다립니다.
- 비선점(No Preemption): 다른 쓰레드가 가진 자원을 강제로 빼앗을 수 없습니다.
- 순환 대기(Circular Wait): 두 개 이상의 쓰레드가 순환적으로 자원을 기다립니다. (A는 B를 기다리고, B는 C를 기다리고, C는 A를 기다리는 식)
데드락 예시
Java
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: resource1 획득");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (resource2) {
System.out.println("Thread 1: resource2 획득");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: resource2 획득");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (resource1) {
System.out.println("Thread 2: resource1 획득");
}
}
});
thread1.start();
thread2.start();
}
}
위 예제에서 thread1
은 resource1
을 획득하고 resource2
를 기다리고, thread2
는 resource2
를 획득하고 resource1
을 기다리는 상황이 발생하여 데드락에 빠지게 됩니다.
데드락 해결 방법
데드락을 해결하는 방법은 데드락 발생 조건 중 하나 이상을 제거하는 것입니다.
- 상호 배제 제거: 공유 자원을 사용하지 않도록 설계합니다.
- 점유 대기 제거: 모든 자원을 한 번에 요청하거나, 자원을 요청하기 전에 가진 자원을 모두 반납합니다.
- 비선점 제거: 자원을 선점할 수 있도록 설계합니다.
- 순환 대기 제거: 자원에 순서를 부여하고, 순서대로 자원을 요청하도록 합니다.
데드락 예방 전략
- 자원 할당 그래프: 자원 할당 상태를 그래프로 표현하여 데드락 발생 가능성을 분석합니다.
- 뱅커스 알고리즘: 자원 할당 요청을 미리 검사하여 데드락 발생 가능성을 예측합니다.
데드락은 멀티스레드 프로그래밍에서 발생할 수 있는 심각한 문제입니다. 데드락 발생 조건과 해결 방법을 이해하고, 데드락을 예방하기 위한 전략을 적용하는 것이 중요합니다.
'코딩스터디 > JAVA스터디' 카테고리의 다른 글
Java 애노테이션 (3) | 2024.11.18 |
---|---|
Java) Enum (3) | 2024.11.17 |
Java 예외 처리 (4) | 2024.11.15 |
Java) 인터페이스 (interface) (6) | 2024.11.14 |
Java 패키지 (6) | 2024.11.13 |