2021. 5. 4. 15:05ㆍJava
프로세스와 쓰레드
- 프로세스
: 실행 중인 프로그램으로 자원(CPU와 Memory 같은 Resource)과 쓰레드로 구성된다.
- 쓰레드
: 프로세스 내에서 실제 작업을 수행
: 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.
: 싱글 쓰레드 프로세스와 멀티 쓰레드 프로세스로 구분된다.
※ 1개의 new 프로세스를 생성하는 것보다 1개의 new 쓰레드를 생성하는 것이 더 적은 비용이 든다.
※ 과거 CGI 같은 경우 멀티 쓰레드를 지원하지 않아, 요청이 올 때마다 새로운 프로세스가 생성되었는데
이는 Web 수요가 늘어나는 시기에 멀티 쓰레드가 지원되는 Java에 밀려 사라지는 이유 중 하나가 되었다.
멀티쓰레드의 장점 및 단점
- 장점
: 시스템 자원을 보다 효율적으로 사용 가능
: 사용자에 대한 응답성이 향상된다.
: 작업이 분리되어 코드가 간결해진다.
- 단점
: 동기화(Synchronization)에 주의해야 한다.
: 교착상태(Dead-Lock)가 발생하지 않도록 주의해야 한다.
: 각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야 한다.
: 즉 프로그래밍할 때 고려해야 할 사항들이 많다.
쓰레드를 구현하는 방법
1. Thread 클래스를 상속
public class Example {
public static void main(String[] args) {
MyTread myTread = new MyTread(); // 쓰레드 객체 생성
myTread.start(); // 쓰레드 시작
}
}
class MyTread extends Thread {
@Override
public void run() {
// 작업 내용
}
}
2. Runnable 인터페이스를 구현
public class Example {
public static void main(String[] args) {
Runnable runnable = new MyTread();
Thread myTread = new Thread(runnable); // 쓰레드 객체 생성
myTread.start(); // 쓰레드 시작
}
}
class MyTread implements Runnable {
@Override
public void run() {
// 작업 내용
}
}
※ 쓰레드를 구현 하는 방법 [연습] --- 더보기 클릭
public class Example {
public static void main(String[] args) {
MyTread1 myTread1 = new MyTread1(); // 쓰레드1 객체 생성
Thread myTread2 = new Thread(new MyTread2()); // 쓰레드2 객체 생성
myTread1.start(); // 쓰레드1 실행
myTread2.start(); // 쓰레드2 실행
}
}
class MyTread1 extends Thread {
@Override
public void run() {
for(int i=0; i<5; i++) {
System.out.println("쓰레드 네임: " +this.getName());
}
}
}
class MyTread2 implements Runnable {
@Override
public void run() {
for(int i=0; i<5; i++) {
System.out.println("쓰레드 네임: " +Thread.currentThread().getName());
}
}
}
// 출력 결과
쓰레드 네임: Thread-0
쓰레드 네임: Thread-0
쓰레드 네임: Thread-1
쓰레드 네임: Thread-1
쓰레드 네임: Thread-1
쓰레드 네임: Thread-1
쓰레드 네임: Thread-1
쓰레드 네임: Thread-0
쓰레드 네임: Thread-0
쓰레드 네임: Thread-0
위 출력 결과를 보면 myTread1 을 먼저 start 하였지만, 순서가 뒤죽박죽이다. 이는 OS 스케줄러가 가지고 있는 스케줄링 로직에 의해 결정되는 부분이므로 OS에 의존적이다. 그렇기에 쓰레드는 예측할 수 없는 불확실성 한 특징을 가지고 있다.
쓰레드의 종류
1. main Thread
- 사용자 스레드
2. Daemon Thread
- 보조 스레드
- GC, 자동 저장, 화면 자동갱신 등에 사용된다.
// ADT
boolean isDaemon() // daemon 인지 확인 (true: 데몬)
void setDaemon(boolean on); // daemon 또는 main 으로 변경 (true: Daemon)
- setDaemon() 여부는 start() 전에 결정해야 한다. 그렇지 않으면 IllegalThreadStateException 발생
※ 메인 쓰레드가 종료되면, 데몬 쓰레드는 자동으로 종료된다.
쓰레드의 I/O Blocking
- 입출력 작업 시, 쓰레드로 인한 블록킹 현상
ex) 사용자에게 입력 값을 받는다. 또 1~100을 출력한다. 아래 그림을 보면 싱글 쓰레드 구성 시, 사용자의 입력을 기다리는 시간동안 정지 상태가 이어지며, 1~100 을 출력 명령 실행이 늦어지는 걸 확인할 수 있다. 즉 I/O Blocking 이 지속 된다는 말이다. 반대로 멀티 쓰레드 구상 시, 사용자의 입력을 기다리는 정지 상태가 지속되더라도 1~100 을 출력하라는 명령은 서브 스레드가 대신하기 때문에 실행 시간이 지체되지 않는 걸 확인할 수 있다.
쓰레드의 우선순위 (Priority of Thread)
- 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 갖게 할 수 있다.
- 1 ~ 10 까지의 우선순위를 부여할 수 있으며, 숫자가 클수록 우선순위가 높다. (default 값은 5 이다)
// ADT
void setPriority(int newPriority); // 쓰레드의 우선순위 지정
int getPriority(); // 쓰레드의 우선순위 반환
- 하지만 실제로는 우선순위에 따라 더 많은 작업시간을 가지고 정확히 돌아가지 않는다.
- 개발자가 지정한 1 ~ 10 까지의 우선순위는 JVM 에 적용된 희망사항이다.
- 이러한 희망사항을 OS 스케줄러에게 전달하는 것뿐이라 정확히 적용되지는 않는다.
- OS 에서 돌아가는 수많은 프로세스와 쓰레드를 제치고 우리의 프로그램에 특혜를 줄 수 없기 때문이다.
쓰레드의 그룹
- 서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것- 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 속해있다. (default 값은 main 쓰레드)
// ADT
ThreadGroup getThreadGroup(); // 쓰레드의 그룹 반환
void uncaughtException(Thread t, Throwable e); // 그룹별 쓰레드 예외처리 재정의
쓰레드의 상태
쓰레드의 실행 제어
// ADT
static void sleep(); // 일시정지
void join(); // 다른 쓰레드를 기다린다
void interrupt(); // sleep() 이나 join() 된 쓰레드를 깨운다
void stop(); // 쓰레드 즉시 종료
void suspend(); // 일시정지
void resume(); // suspend() 된 쓰레드를 실행대기상태(Runnable)로 만듬
static void yield(); // 자신에게 주어진 실행 시간을 다른 쓰레드에게 양보(yield)
- static 이 붙은 sleep 과 yield 는 다른 쓰레드에게 적용이 불가능하다. 오직 자기 자신에게만 적용 가능하다.
1. sleep
- 현재 쓰레드를 지정된 시간 동안 멈추게 한다.
- Time out 또는 Interrupt() 가 발생했을 때 sleep() 은 깨진다.
- Interrupt() 경우, InterruptedException 이 발생하기에 예외처리를 필수이다.
※ sleep [연습] --- 더보기 클릭
public class Example {
public static void main(String[] args) {
MyTread1 myTread1 = new MyTread1(); // 쓰레드1 객체 생성
Thread myTread2 = new Thread(new MyTread2()); // 쓰레드2 객체 생성
myTread1.start(); // 쓰레드1 실행
myTread2.start(); // 쓰레드2 실행
/**
* 이 부분에서 !
* myTread1.sleep() 해도 sleep() 이 적용 되는건 Main 쓰레드 이다.
* 이유는 sleep 은 static 메소드라 자기 자신만이 적용 되기 때문이다.
*/
delay(2000);
System.out.println("Main 쓰레드 종료");
}
private static void delay(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyTread1 extends Thread {
@Override
public void run() {
for(int i=0; i<5; i++) {
System.out.println("쓰레드 네임: " +this.getName());
}
}
}
class MyTread2 implements Runnable {
@Override
public void run() {
for(int i=0; i<5; i++) {
System.out.println("쓰레드 네임: " +Thread.currentThread().getName());
}
}
}
/*****************
* 출력 결과
* ---------------
* 쓰레드 네임: Thread-0
* 쓰레드 네임: Thread-1
* 쓰레드 네임: Thread-1
* 쓰레드 네임: Thread-1
* 쓰레드 네임: Thread-1
* 쓰레드 네임: Thread-1
* 쓰레드 네임: Thread-0
* 쓰레드 네임: Thread-0
* 쓰레드 네임: Thread-0
* 쓰레드 네임: Thread-0
* Main 쓰레드 종료
* ---------------
*****************/
2. interrupt()
- 대기상태(Waiting)인 쓰레드를 실행 대기상태(Runnable)로 만든다.
// ADT
void interrupt(); // 쓰레드의 interrupted 상태를 true 로 변경
boolean isInterrupted(); // 쓰레드의 interrupted 상태 반환
static boolean interrupted(); // 쓰레드의 interrupted 상태 반환, false 로 초기화
3. suspend() <<Deprecated 됨. Dead-Lock 의 원인이 될 수 있음>>
- 쓰레드를 일시정지시킨다.
4. resume() <<Deprecated 됨. Dead-Lock 의 원인이 될 수 있음>>
- suspend() 에 의해 일시 정지된 쓰레드를 실행 대기상태(Runnable)로 만든다.
5. stop() <<Deprecated 됨. Dead-Lock 의 원인이 될 수 있음>>
- 쓰레드를 즉시 종료시킨다.
6. join()
- 지정된 시간 동안 특정 쓰레드가 작업하는 것을 기다린다.
7. yield()
- 남은 시간을 다음 쓰레드에게 양보하고, 자신은 실행 대기한다.
- yield() 와 interrupt() 를 적절히 사용하면, 응답성과 효율을 높일 수 있다.
- yield 는 OS 스케줄러에게 통보하는 것이므로 정확히 실행된다는 보장은 없다.
쓰레드의 동기화 (Synchronization)
- 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.
- 진행 중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 "동기화"가 필요
- 동기화하려면 간섭받지 않아야 하는 문장들을 "임계 영역"으로 설정한다.
- 임계 영역은 Lock 을 얻은 단 하나의 쓰레드만 출입이 가능하다. (객체 1개에 락 1개)
synchronized 를 이용한 동기화
- 키워드 synchronized 를 사용해 임계 영역(Critical Section) 을 설정하는 방법 2가지
1. 메서드 전체를 임계 영역 지정
public synchronized void calcSum() {
...
}
2. 특정한 영역을 임계 영역 지정
synchronized(객체의 참조변수) {
...
}
※ synchronized 를 이용한 동기화 [연습] --- 더보기 클릭
public class Example {
public static void main(String[] args) {
// 쓰레드 시작
for(int i=0; i<20; i++) {
new Thread(new ticketingTread()).start();
}
}
}
class Ticket {
// 총 티켓 수
private int totalTicket = 10;
public synchronized void ticketing() {
try {
// 출력을 위해 딜레이 추가
Thread.sleep(100);
// 티켓 감소
if (totalTicket >= 0) {
totalTicket --;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getTotalTicket() {
return totalTicket;
}
}
class ticketingTread implements Runnable {
static Ticket ac = new Ticket();
@Override
public void run() {
synchronized(this) {
System.out.println("Thread ID -> " + Thread.currentThread().getName());
// 티켓팅
ac.ticketing();
int remainTicket = ac.getTotalTicket();
if (remainTicket < 0)
System.out.println("티켓팅 실패 ㅠㅠ - 남은 티켓: " + remainTicket);
else
System.out.println("티켓팅 성공 !!! - 남은 티켓: " + remainTicket);
}
}
}
// 출력 결과
Thread ID -> Thread-0
Thread ID -> Thread-3
Thread ID -> Thread-4
Thread ID -> Thread-5
Thread ID -> Thread-1
Thread ID -> Thread-2
Thread ID -> Thread-6
Thread ID -> Thread-8
Thread ID -> Thread-7
Thread ID -> Thread-9
Thread ID -> Thread-10
Thread ID -> Thread-11
Thread ID -> Thread-12
Thread ID -> Thread-13
Thread ID -> Thread-14
Thread ID -> Thread-15
Thread ID -> Thread-16
Thread ID -> Thread-17
Thread ID -> Thread-18
Thread ID -> Thread-19
티켓팅 성공 !!! - 남은 티켓: 9
티켓팅 성공 !!! - 남은 티켓: 8
티켓팅 성공 !!! - 남은 티켓: 7
티켓팅 성공 !!! - 남은 티켓: 6
티켓팅 성공 !!! - 남은 티켓: 5
티켓팅 성공 !!! - 남은 티켓: 4
티켓팅 성공 !!! - 남은 티켓: 3
티켓팅 성공 !!! - 남은 티켓: 2
티켓팅 성공 !!! - 남은 티켓: 1
티켓팅 성공 !!! - 남은 티켓: 0
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
티켓팅 실패 ㅠㅠ - 남은 티켓: -1
synchronized 를 이용한 동기화 추가 내용
- 동기화를 하면 데이터는 보호되지만, 1번에 1 Thread 만 임계 영역에 들어갈 수 있기에 비효율적이다. 프로그램 효율이 떨어지는 이유이다.
- 동기화의 효율을 높이기 위해 wait(), notify() 를 사용하다.
- Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
1. wait()
- 객체의 Lock 을 풀고 쓰레드를 해당 객체의 waiting pool 에 넣는다.
2. notify()
- waiting pool 에서 대기중인 쓰레드 중의 하나를 깨운다. (랜덤)
3. nofityAll()
- waiting pool 에서 대기중인 모든 쓰레드를 깨운다.
※ wait 와 notify 를 이용한 동기화 [연습] --- 더보기 클릭
import java.util.ArrayList;
public class Example {
public static void main(String[] args) throws InterruptedException {
Table table = new Table();
// 요리사 쓰레드 시작
new Thread(new Chef(table), "CHEF").start();
// 손님 쓰레드 시작
boolean flag = true;
for(int i=0; i<20; i++) {
if (flag) {
new Thread(new Customer(table, "donut"), "CUST-" + i).start();
flag = false;
} else {
new Thread(new Customer(table, "burger"), "CUST-" + i).start();
flag = true;
}
Thread.sleep(500);
}
// 시스템 종료 조건
Thread.sleep(30000);
System.exit(0);
}
}
// 손님
class Customer implements Runnable {
private Table table;
private String food;
public Customer(Table table, String food) {
this.table = table;
this.food = food;
}
@Override
public void run() {
String curThreadNm = Thread.currentThread().getName();
System.out.println("손님: " + curThreadNm + " 등장");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// 음식 먹기
table.remove(food);
System.out.println("손님: " + curThreadNm + " 은 음식( " + food + " ) 를 먹었습니다.");
}
}
// 요리사
class Chef implements Runnable {
private Table table;
public Chef(Table table) {
this.table = table;
}
@Override
public void run() {
while(true) {
int idx = (int)(Math.random() * table.getDishNum());
table.add(table.dishNames[idx]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
}
}
}
// 테이블
class Table {
String[] dishNames = { "donut", "donut", "burger" };
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
public synchronized void add(String dish) {
String curThreadNm = Thread.currentThread().getName();
// 테이블에 음식이 가득 찼을 경우
while (dishes.size() >= MAX_FOOD) {
System.out.println("┏━테이블에 음식이 가득 찼습니다.");
System.out.println("┖━요리사: " + curThreadNm + " is waiting");
// 요리사를 기다리게 하기
waitAndDelay(1000);
}
// 음식 추가
dishes.add(dish);
notify(); // 손님 깨우기
System.out.println("요리사: " + curThreadNm + " 가 음식( " + dish + " ) 를 만들었습니다. " + dishes.toString());
}
public void remove(String dishName) {
synchronized (this) {
String curThreadNm = Thread.currentThread().getName();
// 테이블에 음식이 존재하지 않을 경우
while (dishes.size() == 0) {
System.out.println("┏━테이블에 음식이 없습니다.");
System.out.println("┖━손님: " + curThreadNm + " is waiting");
waitAndDelay(1000);
}
// 음식 먹기
while (true) {
// 원하는 음식 탐색
for(int i=0; i<dishes.size(); i++) {
if (dishName.equals(dishes.get(i))) {
dishes.remove(i);
notify(); // 잠자고 있는 요리사를 꺠우기 위함
return;
}
}
// 고객이 원하는 음식이 없을 경우, 기다리게 한다.
System.out.println("┏━테이블에 원하는 음식( " + dishName + " )이 없습니다.");
System.out.println("┖━손님: " + curThreadNm + " is waiting");
waitAndDelay(1000);
}
}
}
public int getDishNum() {
return dishNames.length;
}
private void waitAndDelay(int millis) {
try {
wait();
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 출력결과
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut]
손님: CUST-0 등장
손님: CUST-1 등장
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut]
손님: CUST-0 은 음식( donut ) 를 먹었습니다.
손님: CUST-2 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-1 is waiting
손님: CUST-3 등장
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut]
손님: CUST-4 등장
손님: CUST-5 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-1 is waiting
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [donut, donut, burger]
손님: CUST-3 은 음식( burger ) 를 먹었습니다.
손님: CUST-2 은 음식( donut ) 를 먹었습니다.
손님: CUST-6 등장
손님: CUST-7 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-1 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
손님: CUST-4 은 음식( donut ) 를 먹었습니다.
손님: CUST-8 등장
손님: CUST-9 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [donut, burger]
손님: CUST-7 은 음식( burger ) 를 먹었습니다.
손님: CUST-6 은 음식( donut ) 를 먹었습니다.
손님: CUST-10 등장
손님: CUST-11 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-1 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-9 is waiting
손님: CUST-8 은 음식( donut ) 를 먹었습니다.
손님: CUST-12 등장
손님: CUST-13 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [burger]
손님: CUST-11 은 음식( burger ) 를 먹었습니다.
┏━테이블에 음식이 없습니다.
┖━손님: CUST-10 is waiting
손님: CUST-14 등장
손님: CUST-15 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-9 is waiting
손님: CUST-16 등장
손님: CUST-17 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-1 is waiting
┏━테이블에 음식이 없습니다.
┖━손님: CUST-15 is waiting
┏━테이블에 음식이 없습니다.
┖━손님: CUST-14 is waiting
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [burger]
손님: CUST-13 은 음식( burger ) 를 먹었습니다.
┏━테이블에 음식이 없습니다.
┖━손님: CUST-12 is waiting
손님: CUST-18 등장
손님: CUST-19 등장
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-17 is waiting
손님: CUST-16 은 음식( donut ) 를 먹었습니다.
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-9 is waiting
┏━테이블에 음식이 없습니다.
┖━손님: CUST-10 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-19 is waiting
손님: CUST-18 은 음식( donut ) 를 먹었습니다.
┏━테이블에 음식이 없습니다.
┖━손님: CUST-15 is waiting
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-1 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut]
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut]
손님: CUST-12 은 음식( donut ) 를 먹었습니다.
손님: CUST-14 은 음식( donut ) 를 먹었습니다.
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [burger]
┏━테이블에 원하는 음식( donut )이 없습니다.
┖━손님: CUST-10 is waiting
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [burger, burger]
손님: CUST-19 은 음식( burger ) 를 먹었습니다.
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [burger, donut]
손님: CUST-9 은 음식( burger ) 를 먹었습니다.
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-17 is waiting
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut, donut]
손님: CUST-10 은 음식( donut ) 를 먹었습니다.
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [donut, donut, burger]
손님: CUST-1 은 음식( burger ) 를 먹었습니다.
요리사: CHEF 가 음식( burger ) 를 만들었습니다. [donut, donut, burger]
손님: CUST-15 은 음식( burger ) 를 먹었습니다.
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut, donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-17 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut, donut, donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut, donut, donut, donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-17 is waiting
요리사: CHEF 가 음식( donut ) 를 만들었습니다. [donut, donut, donut, donut, donut, donut]
┏━테이블에 원하는 음식( burger )이 없습니다.
┖━손님: CUST-5 is waiting
┏━테이블에 음식이 가득 찼습니다.
┖━요리사: CHEF is waiting
'Java' 카테고리의 다른 글
[Java] 오브젝트(Object) 란? - 특징과 사용법 (0) | 2021.06.17 |
---|---|
[Java] 예외처리(Exception) 란? - 특징과 사용법 (0) | 2021.04.25 |
[Java] 스트림(Stream) 이란? - 특징과 사용법 (0) | 2021.04.24 |
[Java] Java8 에 새롭게 추가된 기능을 알아보자 (0) | 2021.04.12 |
[Java] 리플렉션(Reflection) 이란? (0) | 2021.03.10 |