📌 프로세스(공장) vs 쓰레드(일꾼)
- 프로세스 : 운영체제로부터 자원을 할당받는 작업의 단위
- 쓰레드 : 프로세스가 할당받은 자원을 이용하는 실행의 단위
실행 중인 프로그램, 자원과 쓰레드로 구성
OS가 프로그램 실행을 위한 프로세스를 할당해줄때, 프로세스안에 프로그램 Code와 Data, 메모리 영역을 함께 할당해줌
프로세스 내에서 실제 작업을 수행
프로세스가 작업중인 프로그램에서 실행요청이 들어오면 쓰레드를 만들어 명령을 처리하도록 함
일반 쓰레드와 동일하며 JVM 프로세스 안에서 실행되는 쓰레드
📌 Java는 메인 쓰레드가 main() 메서드를 실행시키면서 시작
- 메인 쓰레드는 필요에 따라서 작업 쓰레드들을 생성해서 병렬로 코드를 실행 시킬 수 있음
- 즉, Java는 멀티 쓰레드 지원
프로세스 안에서 하나의 쓰레드만 실행되는 것
main()
메서드만 실행시키면 싱글 쓰레드main()
메서드의 쓰레드를 ‘메인 쓰레드’라고 함프로세스 안에서 여러개의 쓰레드가 실행되는 것
Thread
클래스를 상속 받아 쓰레드 구현run()
를 오버라이딩해서 수행할 작업 작성start()
로 쓰레드 실행public class TestThread extends Thread { // 1. Thread 상속
@Override
public void run() {
// 쓰레드 수행작업
}
}
public class Main {
public static void main(String[] args) {
TestThread thread = new TestThread(); // 2. 쓰레드 생성
thread.start() // 쓰레드 실행
}
}
Runnable
인터페이스 구현run()
를 오버라이딩해서 수행할 작업 작성start()
로 쓰레드 실행class TestRunnable implements Runnable { // 1. Runnable 구현
@Override
public void run() {
// 쓰레드 수행작업
}
}
public class Main {
public static void main(String[] args) {
Runnable run = new TestRunnable(); // 2. Runnable 생성
Thread thread = new Thread(run); // 3. 쓰레드 생성
thread.start(); // 쓰레드 실행
}
}
🤷🏻♀️ 왜 굳이 Runnable 인터페이스 사용하나요?
- Thread는 클래스이므로 다중 상속되지 않아 확장성이 매우 떨어짐
- 반대로 Runnable은 인터페이스이기 때문에 다른 필요한 클래스를 상속받을 수 있어 확정성에 매우 유리!
run()
메서드에 작성했던 쓰레드가 수행할 작업을 실행 블록 { } 안에 작성하는게 람다식setName()
메서드: 쓰레드에 이름 부여Thread.currentThread().getName()
: 현재 실행 중인 쓰레드의 이름 반환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();
}
}
쓰레드 작업의 중요도에 따라 쓰레드의 우선순위 부여 가능
setPriority(n)
메서드로 설정getPriority()
로 우선순위를 반환보이지 않는곳(background) 에서 실행되는 낮은 우선순위를 가진 쓰레드
void setDaemon(boolean on)
: 쓰레드를 데몬쓰레드 혹은 사용자쓰레드로 변경start()
전에 실행해야함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");
}
}
}
보이는 곳(foregorund) 에서 실행되는 높은 우선순위를 가진 쓰레드
🚨 JVM 은 사용자 쓰레드의 작업이 끝나면 데몬 쓰레드도 자동으로 종료시킴
서로 관련이 있는 쓰레드들을 그룹으로 묶어서 다루기 위한 것(보안상)
// 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");
// Thread에 ThreadGroup 이 할당된것을 확인할 수 있습니다.
System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName());
// interrupt()는 일시정지 상태인 쓰레드를 실행대기 상태로 만듭니다.
group1.interrupt();
상태 | Enum | 설명 |
---|---|---|
객체생성 | NEW | 쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태 |
실행대기 | RUNNABLE | start() 메서드 호출 후, 실행 상태로 언제든지 갈 수 있는 상태 |
일시정지 | WAITING | 다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태 |
일시정지 | TIMED_WAITING | 주어진 시간 동안 기다리는 상태 |
일시정지 | BLOCKED | 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태 |
종료 | TERMINATED | 쓰레드의 작업이 종료된 상태 |
지정된 시간동안 현재 쓰레드를 일시정지시킴
지정한 시간 후 자동적으로 다시 실행대기상태가 됨
try {
Thread.sleep(2000); // 2초
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.sleep(ms);
: static메소드, ms(밀리초) 단위로 설정interrupt()
를 만나면 다시 실행되기 때문에 InterruptedException 발생일시정지 상태인 쓰레드를 깨워서 실행대기상태로 만듦
InterruptedException 발생함으로서 일시정지상태를 벗어나게됨
start()
된 후 동작하다 interrupt()
를 만나 실행하면 interrupted 상태가 true가 됨isInterrupted()
메서드를 사용하여 상태값을 확인!Thread.currentThread().isInterrupted()
로 interrupted 상태를 체크해서 예외 방지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());
}
}
정해진 시간동안 지정한 쓰레드가 작업하는 것을 기다림(일시정지 상태)
Thread thread = new Thread(task, "thread");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.join(ms);
: ms(밀리초) 단위로 설정(선택)interrupt()
를 만나면 다시 실행되기 때문에 InterruptedException 발생남은 시간을 다음 쓰레드에게 양보하고 쓰레드 자신은 실행대기 상태가 됨
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();
}
}
thread1과 thread2가 같이 1초에 한번씩 출력되다가 5초뒤에 thread1에서 InterruptedException이 발생하면서 Thread.yield(); 이 실행되어 thread1은 실행대기 상태로 변경되면서 남은 시간은 thread2에게 리소스가 양보된다.
한 번에 하나의 쓰레드만 객체에 접근할 수 있도록 객체에 Lock을 걸어서 데이터의 일관성을 유지하는 것
synchronized
를 붙여서 임계영역을 지정하여 다른 쓰레드의 침범을 막을 수 있다. (침범을 막다. = Lock을 걸다.)public synchronized void asyncSum() {
...침범을 막아야하는 코드...
}
synchronized(해당 객체의 참조변수) {
...침범을 막아야하는 코드...
}
침범을 막은 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, wait()
을 호출하여 쓰레드가 Lock을 반납하고 기다리게 할 수 있다.
notify()
를 호출해서,실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 통지를 기다림(일시정시 상태)
해당 객체의 대기실(waiting pool)에 있는 모든 쓰레드 중에서 하나의 쓰레드만 깨움(실행대기 상태로)
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();
}
}
}
}
이 코드에서는 고객은 원하는 제품이 없어서 기다리고, 점원은 재고가 다차서 기다려서 결국 둘다 무한정 기다리게되면서 병목현상이 발생할 수 있음
이 때문에 notify()와 wait()를 쓸때는 주의해야함
synchronized 블럭으로 동기화하면 자동적으로 Lock이 걸리고 풀리지만, 같은 메서드 내에서만 Lock을 걸 수 있다는 제약이 있다.
이런 제약을 해결하기 위해 Lock 클래스를 사용한다.
(이 부분은 일단 이런게 있구나 하고 넘어가자)
public class MyClass {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void methodA() {
synchronized (lock1) {
methodB();
}
}
public void methodB() {
synchronized (lock2) {
// do something
methodA();
}
}
}
wait() & notify()의 문제점인 waiting pool 내 쓰레드를 구분하지 못한다는 것을 해결한 것이 Condition
wait()
& notify()
대신 Condition의 await()
& signal()
사용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 문법 종합반
🔗 자바의 정석
🔗 https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html
🔗 https://math-coding.tistory.com/173
🔗 https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=qbxlvnf11&logNo=220921178603