- 프로세스: 운영체제로부터 자원을 할당받는 작업의 단위. "실행 중인 프로그램"
- OS가 프로세스를 할당할 때 프로그램 Code와 Data, 메모리 영역(Stack, Heap)을 함께 할당
- 프로세스 구조
- Data: 프로그램이 실행 중 초기화된 데이터를 저장하는 공간(전역 변수, static 변수, 상수)
- Stack: 지역 변수, 매개변수 리턴 변수를 저장하는 공간
- Heap: 동적으로 필요한 변수를 저장하는 공간
- 스레드: 프로세스가 할당받은 자원을 이용하는 실행의 단위
- 프로세스가 작업 중인 프로그램에서 실행 요청이 들어오면 스레드를 만들어 명령 처리
- 스레드는 프로세스 내 메모리공간(Heap)을 공유받음
- 스레드는 자신만의 메모리공간(Stack)도 할당받음
- Java 스레드: JVM 프로세스 안에서 실행되는 스레드
- Java Main 스레드부터 실행되며 JVM에 의해 실행됨
싱글 스레드와 멀티 스레드
- 싱글 스레드: 프로세스 안에서 하나의 스레드만 실행되는 것
- Java 프로그램의 경우 main() 스레드만 실행시켰을 때
- main() 메서드의 스레드를 '메인 스레드'라고 부름
- 메인 스레드가 종료되면 JVM도 같이 종료됨
- 멀티 스레드: 프로세스 안에서 여러개의 스레드가 실행되는 것
- 장점
- 여러 개의 작업을 동시에 함으로써 성능이 좋아짐
- 자원을 효율적으로 사용
- 응답 스레드와 작업 스레드를 분리하여 빠르게 응답 가능(비동기)
- 단점
- 동기화 문제 발생: 자원 충돌
- 교착 상태 발생: 작업 진행 불가능
- 장점
스레드 구현방법
1. Java에서 제공하는 Thread 클래스를 상속받아 구현
public class TestThread extends Thread {
@Override
public void run() {
// 쓰레드 수행작업
}
}
...
TestThread thread = new TestThread(); // 쓰레드 생성
thread.start() // 쓰레드 실행
2. Java에서 제공하는 Runnable 인터페이스를 사용하여 구현
public class TestRunnable implements Runnable {
@Override
public void run() {
// 쓰레드 수행작업
}
}
...
Runnable run = new TestRunnable();
Thread thread = new Thread(run); // 쓰레드 생성
thread.start(); // 쓰레드 실행
인터페이스는 다중 상속을 지원 -> 다른 필요한 클래스를 상속받을 수 있으므로 확장성에 유리
3. Runnable 인터페이스 + 람다식
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
int sum = 0;
for (int i = 0; i < 50; i++) {
sum += i;
System.out.println(sum);
}
System.out.println(Thread.currentThread().getName() + " 최종 합 : " + sum);
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
Thread thread2 = new Thread(task);
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
데몬 스레드와 사용자 스레드
- 데몬 스레드: 보이지 않는 곳(background)에서 실행되는 낮은 우선순위를 가진 스레드
- 대표적으로 메모리 영역을 정리해주는 가비지 컬렉터가 있음
- 우선순위가 낮고 다른 스레드가 종료되면 강제 종료 당함
public class Main {
public static void main(String[] args) {
Runnable demon = () -> {
for (int i = 0; i < 1000000; i++) {
System.out.println("demon");
}
};
Thread thread = new Thread(demon);
thread.setDaemon(true); // true로 설정시 데몬스레드로 실행됨
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("task");
}
}
}
- 사용자 스레드: 보이는 곳(foreground)에서 실행되는 높은 우선순위를 가진 스레드
- 대표적으로 메인 스레드가 있음
- 프로그램 기능을 담당
- 스레드 우선순위
- 스레드가 생성될 때 우선순위가 정해짐: 우리가 직접 지정하거나 JVM에 의해 지정됨
- 3가지 (최대/최소/보통) 우선순위로 나뉨
- 최대 우선순위 (MAX_PRIORITY) = 10
- 최소 우선순위 (MIN_PRIORITY) = 1
- 보통 우선순위 (NROM_PRIOTIY) = 5 -> 기본값
- 우선순위의 범위는 OS가 아닌 JVM에서 설정함
- setPriority() 메서드로 설정
- getPriority() 메서드로 확인
- 스레드 그룹
- 서로 관련이 있는 스레드들을 그룹으로 묶어서 관리
- JVM이 시작되면 system 그룹이 생성되고 기본적으로 스레드들은 system 그룹에 포함됨
- 메인 스레드는 system 그룹 하위의 main 그룹에 포함됨
- 모든 스레드들은 반드시 하나의 그룹에 포함되어 있어야 함
- 그룹을 지정받지 못한 스레드는 자신을 생성한 부모 스레드의 그룹과 우선순위를 상속받음
- 그룹 생성
// ThreadGroup 클래스로 객체를 만듭니다.
ThreadGroup group1 = new ThreadGroup("Group1");
// Thread 객체 생성시 첫번째 매개변수로 넣어줍니다.
// Thread(ThreadGroup group, Runnable target, String name)
Thread thread1 = new Thread(group1, task, "Thread 1");
// Thread에 ThreadGroup 이 할당된것을 확인할 수 있습니다.
System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName());
- 그룹 관리
// ThreadGroup 클래스로 객체를 만듭니다.
ThreadGroup group1 = new ThreadGroup("Group1");
// Thread 객체 생성시 첫번째 매개변수로 넣어줍니다.
// Thread(ThreadGroup group, Runnable target, String name)
Thread thread1 = new Thread(group1, task, "Thread 1");
Thread thread2 = new Thread(group1, task, "Thread 2");
// interrupt()는 일시정지 상태인 쓰레드를 실행대기 상태로 만듭니다.
group1.interrupt();
- 스레드 상태
스레드는 실행과 대기를 반복하며 run() 메서드를 수행
일시정지 상태에서는 스레드가 실행을 할 수 없는 상태가 됨
상태 | Enum | 설명 |
객체생성 | NEW | 쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태 |
실행대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시정지 | WAITING | 다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태 |
일시정지 | TIMED_WAITING | 주어진 시간 동안 기다리는 상태 |
일시정지 | BLOCKED | 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태 |
종료 | TERMINATED | 쓰레드의 작업이 종료된 상태 |
- 스레드 제어
- sleep(): 현재 스레드를 지정된 시간동안 멈추게 함
ms(밀리초) 단위로 설정됨
예외처리를 해야함 -> 메서드가 throws 하고 있음
특정 스레드를 지목하는 것은 불가능
try {
Thread.sleep(2000); // 2초
} catch (InterruptedException e) {
e.printStackTrace();
}
- interrupt(): 일시정지 상태인 스레드를 실행대기 상태로 만듦
스레드가 start()된 후 동작하다 interrupt()를 만나면 Thread 클래스 내부의 interrupted 변수가 true가 됨
sleep() 실행 중 interrupt()가 실행되면 예외가 발생
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
break;
}
}
System.out.println("task : " + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "Thread");
thread.start();
thread.interrupt();
System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
}
}
- join(): 정해진 시간동안 지정한 스레드가 작업하는 것을 기다림
시간을 지정하지 않으면 지정한 스레드의 작업이 끝날 때까지 기다림
//...
Thread thread = new Thread(task, "thread");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread가 작업을 끝날 때까지 main 스레드가 기다림
- yield(): 남은 시간을 다음 스레드에게 양보하고 자신은 실행대기 상태가 됨
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
}
} catch (InterruptedException e) {
Thread.yield();
}
};
Thread thread1 = new Thread(task, "thread1");
Thread thread2 = new Thread(task, "thread2");
thread1.start();
thread2.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
}
}
- synchronized
스레드 동기화(Synchronization): 스레드가 진행중인 작업을 다른 스레드가 침범하지 못하도록 막는 것
임계영역: 다른 스레드의 침범을 막아야하는 코드들
임계영역에는 Lock을 가진 단 하나의 스레드만 출입가능
임계영역 지정 - synchronized 사용
1. 메서드 전체
public synchronized void asyncSum() {
...침범을 막아야하는 코드...
}
2. 특정 영역
synchronized(해당 객체의 참조변수) {
...침범을 막아야하는 코드...
}
사용 예시:
public void eatApple() {
synchronized (this) {
if(storedApple > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
- wait()과 notify()
- wait(): 임계영역에서 작업을 수행하던 스레드를 Lock을 반납하고 기다리게 함(waiting pool로)
다른 스레드가 Lock을 얻어 해당 객체에 대한 작업을 수행할 수 있게 됨
- notify(): waiting pool에 있는 스레드 중 임의 스레드가 다시 Lock을 얻어 진행할 수 있게 함
public class Main {
public static String[] itemList = {
"MacBook", "IPhone", "AirPods", "iMac", "Mac mini"
};
public static AppleStore appleStore = new AppleStore();
public static final int MAX_ITEM = 5;
public static void main(String[] args) {
// 가게 점원
Runnable StoreClerk = () -> {
while (true) {
int randomItem = (int) (Math.random() * MAX_ITEM);
appleStore.restock(itemList[randomItem]);
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
}
};
// 고객
Runnable Customer = () -> {
while (true) {
try {
Thread.sleep(77);
} catch (InterruptedException ignored) {
}
int randomItem = (int) (Math.random() * MAX_ITEM);
appleStore.sale(itemList[randomItem]);
System.out.println(Thread.currentThread().getName() + " Purchase Item " + itemList[randomItem]);
}
};
new Thread(StoreClerk, "StoreClerk").start();
new Thread(Customer, "Customer1").start();
new Thread(Customer, "Customer2").start();
}
}
class AppleStore {
private List<String> inventory = new ArrayList<>();
public void restock(String item) {
synchronized (this) {
while (inventory.size() >= Main.MAX_ITEM) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 꽉 차있어서 재입고하지 않고 기다리는 중!
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 재입고
inventory.add(item);
notify(); // 재입고 되었음을 고객에게 알려주기
System.out.println("Inventory 현황: " + inventory.toString());
}
}
public synchronized void sale(String itemName) {
while (inventory.size() == 0) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 없기 때문에 고객 대기중
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while (true) {
// 고객이 주문한 제품이 있는지 확인
for (int i = 0; i < inventory.size(); i++) {
if (itemName.equals(inventory.get(i))) {
inventory.remove(itemName);
notify(); // 제품 하나 팔렸으니 재입고 하라고 알려주기
return; // 메서드 종료
}
}
// 고객이 찾는 제품이 없을 경우
try {
System.out.println(Thread.currentThread().getName() + " Waiting!");
wait();
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- Lock
synchronized의 제약: 같은 메서드 내에서만 Lock을 걸 수 있음
- ReentrantLock
- 재진입 가능한 Lock, 가장 일반적인 배타 Lock
- 특정 조건에서 Lock을 풀고, 나중에 다시 Lock을 얻어 임계영역으로 진입이 가능
- ReentrantReadWriteLock
- 읽기를 위한 Lock(공유적), 쓰기를 위한 Lock(배타적)을 따로 제공
- 읽기 Lock이 걸려있는 상태에서 쓰기 Lock을 거는 것은 허용되지 않음 (데이터 변경 방지)
- StampedLock
- ReentrantReadWriteLock + 낙관적인 Lock 기능
- 낙관적인 Lock: 데이터를 변경하기 전에 락을 걸지 않음
- 데이터 변경을 할 때 충돌이 일어날 가능성이 적은 상황에서 사용
- 읽기와 쓰기 작업 모두 빠르게 처리됨
- 낙관적인 읽기 Lock은 쓰기 Lock에 의해 바로 해제 가능
- 쓰기와 읽기가 충돌할 때만 쓰기 후 읽기 Lock을 사용
- Condition
wait() & notify()의 문제점: waiting pool 내 스레드를 구분하지 못함
Condition: 특정 조건을 만족하는 스레드만 깨울 수 있으며 ReentrantLock 클래스가 함께 사용됨
awit() & signal() 을 사용
public static final int MAX_TASK = 5;
private ReentrantLock lock = new ReentrantLock();
// lock으로 condition 생성
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private ArrayList<String> tasks = new ArrayList<>();
// 작업 메서드
public void addMethod(String task) {
lock.lock(); // 임계영역 시작
try {
while(tasks.size() >= MAX_TASK) {
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try {
condition1.await(); // wait(); condition1 쓰레드를 기다리게 합니다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
tasks.add(task);
condition2.signal(); // notify(); 기다리고 있는 condition2를 깨워줍니다.
System.out.println("Tasks:" + tasks.toString());
} finally {
lock.unlock(); // 임계영역 끝
}
}
'[스파르타코딩클럽] > Java 문법 종합반' 카테고리의 다른 글
Chapter 13. 모던 자바(람다, 스트림, Optional) (0) | 2023.10.18 |
---|---|
Chapter 11. 제네릭(Generic) (0) | 2023.10.17 |
Chapter 10. 예외처리 (0) | 2023.10.17 |
Chapter 9. 인터페이스 (0) | 2023.10.17 |
Chapter 8. 상속 (0) | 2023.10.16 |