[ 7주차 - 0925 ] - ②
금일 커리큘럼
├ 09:00 ~ 12:00 FrontEnd (...other post)
└ 13:00 ~ 18:00 자바 프로그래밍 (프로세스와 스레드, 스레드 생성, 동기화, 데몬스레드, 스레드 제어 메서드)
1. 프로세스와 스레드 구조
프로세스 (Process)
- 프로세스는 실행 중인 프로그램의 인스턴스
- 각 프로세스는 독립적인 메모리 공간을 가짐
- 프로세스끼리는 메모리를 공유하지 않음
- 각 프로세스 안에 여러 스레드가 존재할 수 있음
스레드 (Thread)
- 스레드는 프로세스 내에서 실제 작업을 수행하는 최소 단위
- 같은 프로세스 내의 스레드들은 메모리를 공유
- 각 스레드는 자신만의 스택(Stack)을 가짐
- 코드, 데이터, 힙 영역은 공유
# 프로세스와 스레드 구조
# Th : thread (스레드)
┌──────────────────────┐ ┌──────────────────────┐
│ Process A │ │ Process B │
│ │ │ │
│ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │
│ │Th-1 │ │Th-2 │ │ │ │Th-1 │ │Th-2 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │스택 │ │스택 │ │ │ │스택 │ │스택 │ │
│ └─────┘ └─────┘ │ │ └─────┘ └─────┘ │
│ │ │ │
│ 공유 메모리: │ │ 공유 메모리: │
│ - Code (코드) │ │ - Code (코드) │
│ - Data (전역변수) │ │ - Data (전역변수) │
│ - Heap (동적메모리) │ │ - Heap (동적메모리) │
└──────────────────────┘ └──────────────────────┘
독립적 독립적
(공유불가) (공유불가)
# 메모리 공유 관계
Process A: Thread 1,2가 Code/Data/Heap 공유 (Stack만 개별)
Process B: Thread 1,2가 Code/Data/Heap 공유 (Stack만 개별)
Process A ↔ B: 완전 독립 (메모리 공유 없음)
멀티스레드
멀티스레드란 ? 여러 스레드가 동시에 실행되는 프로그래밍 기법
- 장점
- 성능향상 : 여러 작업을 동시에 처리하여 전체 처리 시간 단축
- 자원효율성 : 프로세스보다 적은 리소스 사용
- 응답성 향상 : UI가 더 빠르게 반응
- 단점 및 주의사항
- 동시성 문제 : 공유 자원 접근 시 충돌 가능
- 복잡성 증가 : 설계 및 디버깅 어려움
- 데드락 위험 : 잘못된 동기화로 인한 교착 상태 발생 가능
프로세스와 멀티스레드 비교
| 구분 | 멀티 프로세스 | 멀티 스레드 |
|---|---|---|
| 메모리 공유 | 독립적인 메모리 공간 | 메모리 공간 공유 |
| 통신 방식 | IPC | 공유 메모리를 통한 직접 통신 |
| 생성 비용 | 높음 (프로세스 생성 오버헤드) | 낮음 (스레드 생성 가벼움) |
| 컨텍스트 스위칭 | 비용 높음 | 비용 낮음 |
| 안정성 | 한 프로세스 죽어도 다른 프로세스 영향 없음 | 한 스레드 오류 시 전체 프로세스 영향 |
| 디버깅 | 상대적으로 쉬움 | 어려움 (동기화 문제) |
| 메모리 사용량 | 많음 (각각 독립적 메모리) | 적음 (메모리 공유) |
| 확장성 | 제한적 | 좋음 |
스레드는 어떨때 사용 할까?
- I/O 작업이 많은 경우 (예: 파일 읽기/쓰기, 네트워크 통신)
- 사용자 인터페이스가 있는 애플리케이션 (UI 스레드와 작업 스레드 분리)
- 병렬 처리 작업 (예: 대규모 데이터 처리, 이미지 처리)
- 서버 애플리케이션 (예: 웹 서버에서 각 요청을 별도의 스레드로 처리)
2. 스레드 생성과 실행
스레드 Class 상속하여 별도 스레드 정의 가능
- Thread 클래스 상속
Thread클래스를 상속받아run()메서드를 오버라이딩start()메서드를 호출하여 스레드 실행
public class MyThread extends Thread {
private String name;
MyThread(String name) {
this.name = name;
}
@Override
public void run() {
// 스레드가 실행할 코드
for (int i = 1; i <= 5; i++) {
System.out.println(this.name + " 실행 중: " + i);
try {
Thread.sleep(1000); // 1초 슬립
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}
public static void main(String[] args) {
System.out.println("메인 스레드 시작");
MyThread thread1 = new MyThread("Thread-1");
MyThread thread2 = new MyThread("Thread-2");
thread1.start(); // 스레드 시작
thread2.start();
for (int i = 1; i <= 5; i++) {
System.out.println("main" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
System.out.println("메인 스레드 종료");
}
}
# 실행 결과
메인 스레드 시작
main1
Thread-2 실행 중: 1
Thread-1 실행 중: 1
main2
Thread-1 실행 중: 2
Thread-2 실행 중: 2
main3
Thread-2 실행 중: 3
Thread-1 실행 중: 3
Thread-2 실행 중: 4
Thread-1 실행 중: 4
main4
main5
Thread-2 실행 중: 5
Thread-1 실행 중: 5
메인 스레드 종료
3. Runnable 인터페이스
Runnable 이란 ? 스레드로 실행할 작업을 정의하는 인터페이스
- Runnable 인터페이스 구현
Runnable인터페이스를 구현하여run()메서드를 정의Thread객체에Runnable객체를 전달하여 스레드 실행
- Runnable = 스레드 ? 아님
Runnable인터페이스는 스레드로 실행할 작업을 정의하는 역할Thread클래스는 실제 스레드를 생성하고 관리하는 역할Runnable은 작업 단위,Thread는 실행 단위임
Runnable 상속 예시
class MyRunnable implements Runnable {
private String taskName;
MyRunnable(String name) {
this.taskName = name;
}
@Override
public void run() {
// 스레드가 실행할 코드
for (int i = 1; i <= 3; i++) {
System.out.println(this.taskName + " 실행 중: " + i);
try {
Thread.sleep(1000); // 1초 슬립
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}
}
class RunnableTest {
public static void main(String[] args) {
MyRunnable task1 = new MyRunnable("task1");
MyRunnable task2 = new MyRunnable("task2");
Thread thread1 = new Thread(task1); // Runnable을 Thread에 전달
Thread thread2 = new Thread(task2);
thread1.start(); // 스레드 시작
thread2.start();
}
}
# 실행 결과
task1 실행 중: 1
task2 실행 중: 1
task1 실행 중: 2
task2 실행 중: 2
task2 실행 중: 3
task1 실행 중: 3
Runnable 람다 예시
public class LambdaThreadTest {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("람다스레드확인: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
});
thread1.start();
}
}
# 실행 결과
람다스레드확인: 1
람다스레드확인: 2
람다스레드확인: 3
스레드 상속 아닌 러너블 구현할때 장점
- 1. 상속의 제약 : 스레드는 단일상속, 러너블은 다중상속 가능
// ❌ Thread 상속 - 다른 클래스 상속 불가
class MyThread extends Thread {
// 이미 Thread를 상속받아서 다른 클래스 상속 불가능
}
// ✅ Runnable 구현 - 다른 클래스 상속 가능
class MyTask extends ParentClass implements Runnable {
public void run() { /* 작업 내용 */ }
}
- 2. 작업과 스레드 분리 : 같은 작업을 여러 스레드에서 재사용 가능
// Thread 상속 - 작업과 스레드가 결합
class PrintThread extends Thread {
public void run() {
System.out.println("Hello");
}
}
// Runnable 구현 - 작업과 스레드 분리
Runnable task = () -> System.out.println("Hello");
Thread t1 = new Thread(task); // 같은 작업을
Thread t2 = new Thread(task); // 여러 스레드에서 재사용
- 3. 람다식 활용 : Runnable은 함수형 인터페이스로 람다 표현식 사용 가능
// Thread 상속 - 람다 불가
class MyThread extends Thread {
public void run() { System.out.println("실행"); }
}
// Runnable - 람다 가능 (함수형 인터페이스)
Thread t = new Thread(() -> System.out.println("실행"));
- 4. 스레드 풀 호환성 : ExecutorService 등 스레드 풀에서 Runnable 사용
ExecutorService executor = Executors.newFixedThreadPool(3);
// Runnable은 스레드 풀에서 바로 사용 가능
executor.execute(() -> System.out.println("스레드 풀 실행"));
executor.submit(() -> System.out.println("Future 반환"));
결론 = 스레드상속 대신 러너블 사용 권장
| 구분 | Thread 상속 | Runnable 구현 |
|---|---|---|
| 다중 상속 | 불가능 | 가능 |
| 유연성 | 낮음 | 높음 |
| 코드 재사용성 | 낮음 | 높음 |
| 람다식 사용 | 불가능 | 가능 |
| 스레드 풀 호환 | 어려움 | 쉬움 |
| 권장 사항 | 특별한 경우만 | 일반적으로 권장 ✅ |
- ✅ 더 나은 객체지향 설계 (IS-A vs HAS-A)
- ✅ 코드의 유연성과 재사용성 향상
- ✅ 최신 자바의 함수형 프로그래밍 패러다임과 일치
- ✅ 스레드 풀, CompletableFuture 등과의 호환성
4. 스레드 동기화
동기화란 ? 여러 스레드가 공유 자원에 동시에 접근하는 것을 제어하는 기법
동기화 필요한 이유
- 여러 스레드가 공유 자원에 동시에 접근하면 데이터 불일치 문제가 발생
- 예: 은행 계좌 잔액 업데이트, 카운터 증가 등
- 동기화를 통해 한 번에 하나의 스레드만 공유 자원에 접근하도록 제어
동기화가 없는 경우
class CounterA {
private int count = 0;
// 동기화가 없는 메서드
public void increment() {
count++; // 비원자적 연산 (읽기 + 쓰기 + 저장 -> 3번 연산)
}
public int getCount() {
return count;
}
}
public class NoSyncTest {
public static void main(String[] args) {
CounterA counter = new CounterA();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
// join 쓰는 이유 ? 자식스레드 끝날 때까지 메인스레드가 대기함
thread1.join(); // thread1이 끝날 때까지 대기
thread2.join(); // thread2가 끝날 때까지 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("최종 카운트: " + counter.getCount());
}
}
# 실행 결과 (동기화 없을 때)
# 스레드A 1000 + 스레드B 1000 = 2000이 되어야 하지만
# 동기화가 없어서 중간에 count++의 읽기/쓰기/저장 중첩충돌 인해 값이 달라짐
최종 카운트: 1873
synchronized 키워드 (동기화)
- 메서드나 블록에
synchronized키워드를 사용하여 동기화 가능
class CounterB {
private int count = 0;
// 동기화된 메서드
public synchronized void increment() {
count++; // 이제 이 메서드는 한 번에 하나의 스레드만 접근 가능
}
public int getCount() {
return count;
}
}
public class SyncTest {
public static void main(String[] args) {
CounterB counter = new CounterB();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
// join 쓰는 이유 ? 자식스레드 끝날 때까지 메인스레드가 대기함
thread1.join(); // thread1이 끝날 때까지 대기
thread2.join(); // thread2가 끝날 때까지 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("최종 카운트: " + counter.getCount());
}
}
# 실행 결과 (동기화 있을 때)
# 동기화되서 2000 정상 출력
최종 카운트: 2000
synchronized 블록
- 특정 코드 부분을
synchronized()블록으로 동기화 가능 - 특정 객체 잠금 ?
lock객체를 사용하여 동기화 범위 지정lock: 특정 객체 잠금 (추천)this: 현재 객체 잠금
class CounterC {
private final Object lock = new Object();
private int count = 0;
public void increment() {
// 동기화 블록
synchronized (lock) { // lock 객체를 잠금
count++;
}
// or
// synchronized (this) { // 현재 객체를 잠금
// count++;
// }
}
public int getCount() {
return count;
}
}
5. 데몬스레드
데몬스레드란 ? 백그라운드에서 실행되는 스레드로, 주 스레드가 종료되면 자동으로 종료됨
- 데몬스레드 특징
- 주 스레드가 종료되면 데몬스레드도 자동 종료
- 주로 백그라운드 작업에 사용 (예: 가비지 컬렉터, 로그 기록 등)
setDaemon(true)메서드로 데몬스레드 설정 가능
class TaskThread extends Thread {
public void run() {
System.out.println("작업시작");
try{
sleep(2000); // 2초 작업
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("TaskThread 작업 완료"); // 추가하면 더 명확
}
}
class DaemonThread extends Thread {
public void run() {
while(true){
System.out.println("일하는중...");
try{
sleep(1000); // 1초마다 출력
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class DaemonExam {
public static void main(String[] args) {
TaskThread task1 = new TaskThread();
task1.start();
DaemonThread deTh = new DaemonThread();
deTh.setDaemon(true); // 데몬스레드로 설정
deTh.start();
try{
task1.join(); // TaskThread가 끝날 때까지 대기
}catch(InterruptedException e){
System.out.println(e.getMessage());
}
System.out.println("작업완료");
// 여기서 main 스레드 종료 후 데몬스레드도 자동 종료
}
}
# 실행 결과
작업시작
일하는중...
일하는중...
TaskThread 작업 완료 # TaskThread 2초 작업 완료
일하는중... # 데몬스레드가 한 번 더 실행
작업완료 # main 스레드 & 데몬스레드 종료
- 데몬스레드 쓰는 이유
- 백그라운드에서 지속적으로 실행되어야 하는 작업에 적합
- 주 스레드가 종료되면 자동으로 종료되어 자원 해제 용이
- 예: 로그 기록, 모니터링, 가비지 컬렉션 등
- 데몬스레드 주의사항
- 데몬스레드는 중요한 작업에 사용하지 말 것 (예: 파일 저장, 네트워크 통신)
- 주 스레드가 종료되면 데몬스레드도 즉시 종료되어 작업이 중단될 수 있음
- 데몬스레드 내에서 자원을 해제하거나 정리하는 코드를 작성하지 말 것
etc.
스레드 제어 메서드 - sleep(), join(), interrupt()
sleep() : 지정된 시간동안 스레드 일시 정지
Thread.sleep(밀리초)형태로 사용InterruptedException익셉션 처리 필요
// 앞선 예제에서 사용된 sleep()
try {
Thread.sleep(1000); // 1초간 일시 정지
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
- sleep() 사용 이유 ?
- 스레드 간 실행 시간 간격 조절
- CPU 사용률 조절 (무한루프 방지)
- 시뮬레이션에서 시간 지연 효과 구현
join() - 다른 스레드 스레드 완료 대기
- 순차적 실행이 필요할 때 사용
InterruptedException익셉션 처리 필요
// 동기화 예제에서 사용된 join()
try {
thread1.join(); // thread1이 끝날 때까지 대기
thread2.join(); // thread2가 끝날 때까지 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("최종 카운트: " + counter.getCount());
- join() 사용 이유 ?
- 자식 스레드 완료 후 결과 확인
- 모든 작업 완료 후 다음 단계 진행
- 메인 스레드가 너무 빨리 종료되는 것 방지
interrupt() - 실행 중인 스레드에게 중단 요청
- sleep(), wait(), join() 상태의 스레드를 깨움
InterruptedException익셉션 발생 시킴
public class InterruptExample {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("작업 중...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("작업이 중단됨");
}
});
worker.start();
// 3초 후 중단 신호
try {
Thread.sleep(3000);
worker.interrupt(); // 중단 요청
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}
스레드 제어 메서드 정리
| 메서드 | 기능 | 사용된 예제 |
|---|---|---|
sleep(ms) |
지정 시간 일시 정지 | Thread 상속, Runnable, 람다, 동기화 예제 |
join() |
다른 스레드 완료 대기 | 동기화 예제, 데몬스레드 예제 |
interrupt() |
스레드 중단 신호 | 별도 예제 |
InterruptedException 이란?
- 스레드가 대기 상태에 있을 때
interrupt()메서드로 중단 신호를 받으면 발생 sleep(),wait(),join()메서드에서 발생 가능- 예외 처리를 통해 스레드를 정상적으로 종료하거나 다른 작업 수행
멀티 스레드 에러 영향
- 스레드 A에서 예외 발생 시 해당 스레드만 종료되고, 다른 스레드는 계속 실행됨
- 단,
OutOfMemoryError와 같은 치명적인 오류는 JVM 전체에 영향을 미쳐 모든 스레드가 종료될 수 있음
스레드 A Error & 스레드 B 러닝 예시
public class ThreadErrorTest {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println("Thread A 시작");
int result = 10 / 0; // 오류 발생
System.out.println("Thread A 끝"); // 실행 안됨
});
Thread threadB = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread B 실행: " + i);
try { Thread.sleep(1000); }
catch (InterruptedException e) {}
}
});
threadA.start();
threadB.start(); // Thread A 오류와 상관없이 정상 실행
}
}
스레드 A OutOfMemoryError 예시
public class ThreadErrorTest {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println("Thread A 시작");
throw new OutOfMemoryError(); // 치명적인 오류 발생
});
Thread threadB = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread B 실행: " + i);
try { Thread.sleep(1000); }
catch (InterruptedException e) {}
}
});
threadA.start();
threadB.start(); // 스레드A에서 JVM 전체 영향 줘서 다른 스레드 종료
}
}'멋사 - 부트캠프 19기 : Java > Java' 카테고리의 다른 글
| (09.30) Java 프로그래밍 - Stream API, 스트림 생성, 스트림 연산, 옵셔널 (0) | 2025.09.30 |
|---|---|
| (09.29) Java 프로그래밍 - 스레드 통신, 제어, 데드락 해결방식, 함수형 프로그래밍 (0) | 2025.09.29 |
| (09.16) JDBC 프로그래밍 - DAO/DTO 복습, JDBC 트랜잭션, HikariCP (0) | 2025.09.18 |
| (09.15) JDBC 프로그래밍 - JDBC, Gradle/Maven, executeUpdate/executeQuery, DAO/DTO (0) | 2025.09.15 |
| (09.05) java 객체지향 - OOP 개념, SOLID 원칙 : SRP OCP LSP ISP DIP (0) | 2025.09.05 |