❓
물음표살인마 블로그
  • README
  • ALGORITHM
    • Sieve of Eratosthenes
    • Round Up
    • Binary Search
    • Union Find
    • Sorting Array
    • Lcm, Gcd
  • TechTalk Review
    • Template
  • Books
    • CS Note for Interview
      • Ch1. Design Pattern & Programming paradigm
        • 1.1.1 Singleton Pattern
        • 1.1.2 Factory Pattern
        • 1.1.4 Observer Pattern
        • 1.1.5 Proxty Pattern & Proxy Server
        • 1.1.8 Model-View-Controller Pattern
        • 1.2.1 Declarative and Functional Programming
        • 1.2.2 Object Oriented Programming
      • Ch2. Network
        • 2.2.1 TCP/IP Four-Layer Model
        • 2.2.1-1 TCP 3, 4 way handshake
        • 2.3 Network Devices L4, L7
        • 2.4.1 ARP, RARP
        • 2.4.2 Hop By Hop Communication
        • 2.4.3 IP Addressing Scheme
      • Ch3. Operating System
        • 3.1.1 Roles and Structure of Operating Systems
        • 3.2.1 Memory Hierarchy
        • 3.2.2 Memory Management
        • 3.3.1 Processes and Compilation Process
        • 3.3.3 Memory Structure of a Process
        • 3.3.4 Process Control Block (PCB)
        • 3.3.5 Multiprocessing
        • 3.3.6 Threads and Multithreading
        • 3.3.7 Shared Resources and Critical Sections
        • 3.3.8 Deadlock
        • 3.4 CPU Scheduling Algorithm
      • Ch4. Database
        • 4.1 Database Basic
        • 4.2 Normalization
        • 4.3 Transaction and Integrity
        • 4.4 Types of Databases
        • 4.5 Indexes
        • 4.6 Types of Joins
        • 4.7 Principles of Joins
      • Ch5. Data Structure
    • Learning the Basics of Large-Scale System Design through Virtual Interview Cases
      • 1. Scalability based on user counts(1/2)
      • 1. Scalability based on user counts(2/2)
      • 2.Back-of-the-envelope estimation
      • 3. Strategies for System Design Interviews
      • 4. Rate Limiter
      • 5. Consistent Hashing
      • 6. Key-Value System Design
      • 7. Designing a Unique ID Generator for Distributed Systems
      • 8. Designing a URL Shortener
      • 9. Designing a Web Crawler
      • 10. Notification System Design
      • 11. Designing a News Feed System
      • 12. Chat System Design
      • 13. AutoComplete
      • 14. Design YouTube
      • 15. Design Google Drive
      • Loadbalancer Algorithms
      • Cache tier
      • CDN, Content Delivery Network
      • Stateless Web tier
    • Computer System A programmer's perspective
    • Effective Java
      • Item 1. Consider Static Factory Methods Instead of Constructors
      • Item 2. Consider a Builder When Faced with Many Constructor Parameters
      • Item 3. Ensure Singleton with Private Constructor or Enum Type
      • Item 4. Enforce Noninstantiability with a Private Constructor
      • Item 5. Prefer Dependency Injection to Hardwiring Resources
      • Item 6. Avoid Creating Unnecessary Objects
      • Item 7. Eliminate Obsolete Object References
      • Item 8. Avoid Finalizers and Cleaners
      • Item 9.Prefer try-with-resources to try-finally
      • Item10. Adhering to General Rules When Overriding equals
        • Handling Transitivity Issues
        • Ensuring Consistency
      • Item11. Override hashCode When You Override equals
      • Item12. Always Override toString
        • Always Override toString
      • Item13. Override Clone Judiciously
      • Item14. Consider Implementing Comparable
      • Item15. Minimize the Accessibility of Classes and Members
      • Item16. Accessor Methods Over Public Fields
      • Item17. Minimize Mutability
      • Item18. Composition over inherentance
      • Item19. Design and Document for Inheritance, or Else Prohibit It
      • Item20. Prefer Interfaces to Abstract Classes
      • Item21. Design Interfaces with Implementations in Mind
      • Item22. Use Interfaces Only to define Types
      • Item23. Prefer Class Hierarchies to Tagged Classes
      • Item24. Favor Static Member Classes Over Non-Static
      • Item28. Use Lists Instead of Arrays
      • Item29. Prefer Generic Types
      • Item30. Favor Generic Methods
    • Head First Design Patterns
      • Ch1. Strategy Pattern
      • Ch2. Observer Pattern
        • Ver1. Ch2. Observer Pattern
      • Ch3. Decorator Pattern
        • Ch3. Decorator Pattern
      • Ch4. Factory Pattern
      • Ch5. Singleton Pattern
      • Ch6. Command Pattern
      • Ch7. Adapter and Facade Pattern
      • Ch8. Template Method Pattern
    • Digging Deep into JVM
      • Chapter 2. Java Memory Area & Memory Overflow
      • Chapter 3. Garbage Collector & Memory Allocation Strategy (1/2)
      • Chapter 3. Garbage Collector & Memory Allocation Strategy (2/2)
      • Chapter 5. Optimization Practice
      • Chapter 6. Class file structure
      • Chapter 8. Bytecode Executor Engine (1/2)
  • Interview Practices
    • Restful API Practices
      • Url Shortener API
      • Event Ticket Reservation API
      • Course Management API
      • Search posts by tags API
      • Online Code platform API
      • Simple Task Management API
      • Event Participation API
      • Review System API
      • Car management API
      • Online Library
    • Tech Review
      • if(kakao)
        • Kakao Account Cache Migration / if(kakao)2022
        • Improving the Anomaly Detection System for KakaoTalk Messaging Metrics / if(kakao) 2022
        • Standardizing API Case Handling Without Redeployment / if(kakaoAI)2024
        • JVM warm up / if(kakao)2022
    • Naver Computer Science
      • Process & Thread
      • TCP & UDP
      • Spring & Servlet
      • Filter & Interceptor & AOP
      • Equals() & ==
      • Dependency Injection
      • Object Oriented Programming
  • F-Lab
    • Week1
      • Client & Server
      • HTTP
      • TCP/UDP
      • REST API
      • Questions
        • Object Oriented Programming
        • HTTP
        • Process & Thread
        • Data Structure
    • Week2
      • OSI 7 layer
      • Web vs WAS
    • Week3
      • RDB vs NoSQL
      • RDB Index
      • Cache
      • Redis
      • Messaging Queue
    • Week4
      • Project - Ecommerce
    • Week5
      • ERD - 1
    • Week6
      • Ecommerce - 2
      • Role
      • pw hashing && Salt
      • CreatedAt, ModifiedAt
      • JWT
      • Copy of ERD - 1
    • Week7
      • Vault (HashiCorp Vault)
    • Week 8
      • Api Endpoints
    • Week10
      • Product Create Workflow
  • TOY Project
    • CodeMentor
      • Implementation of Kafka
      • Project Improvement (Architectural Enhancements)
      • Communication between servers in msa
  • JAVA
    • MESI protocol in CAS
    • CAS (Compare and Set)
    • BlockingQueue
    • Producer & Consumer
    • Synchronized && ReentrantLock
    • Memory Visibility
    • Checked vs Unchecked Exception
    • Thread
    • Batch delete instead of Cascade
    • Java Questions
      • Week 1(1/2) - Basic Java
      • Week 1(2/2) - OOP
      • Week 2(1/2) - String, Exception, Generic
      • Week2(2/2) Lambda, Stream, Annotation, Reflection
      • Week3(1/2) Collections
      • Week3(2/2) Threads
      • Week4 Java Concurrency Programming
      • Week5 JVM & GC
    • Java 101
      • JVM Structure
      • Java Compiles and Execution Method
      • Override, Overload
      • Interface vs Abstract Class
      • Primitive vs Object Type
      • Identity and equality
      • String, StringBuilder, StringBuffer
      • Checked Exceptions and Unchecked Exceptions
      • Java 8 methods
      • Try-with-reources
      • Strong Coupling and Loose Coupling
      • Serialization and Deserialization
      • Concurrency Programming in Java
      • Mutable vs Immutable
      • JDK vs JRE
  • SPRING
    • DIP. Dependency Inversion Principal
    • Ioc container, di practice
    • @Transactional
    • Proxy Pattern
    • Strategy Pattern
    • Template Method Pattern
    • using profile name as variable
    • Spring Questions
      • Spring Framework
      • Spring MVC & Web Request
      • AOP (Aspect-Oriented Programming)
      • Spring Boot
      • ORM & Data Access
      • Security
      • ETC
  • DATABASE
    • Enhancing Query Performance & Stability - User list
    • Ensuring Data Consistency, Atomicity and UX Optimization (feat.Firebase)
    • Redis: Remote Dictionary Server
    • Database Questions
      • Week1 DBMS, RDBMS basics
      • Week2 SQL
      • Week3 Index
      • Week4 Anomaly, Functional Dependency, Normalization
      • Week5 DB Transaction, Recovery
    • Normalization
      • 1st Normal Form
      • 2nd Normal Form
      • 3rd Normal Form
  • NETWORK
    • HTTP & TCP head of line blocking
    • HTTP 0.9-3.0
    • Blocking, NonBlocking and Sync, Async
    • Network Questions
      • Week1 Computer Network Basic
      • Week2(1/3) Application Layer Protocol - HTTP
      • Week2(2/3) Application Layer Protocol - HTTPS
      • Week2(3/3) Application Layer Protocol - DNS
      • Week3 Application Layer
      • Week4 Transport Layer - UDP, TCP
      • Week5 Network Layer - IP Protocol
    • Network 101
      • https://www.google.com
      • TCP vs UDP
      • Http vs Https
      • TLS Handshake 1.2
      • HTTP Method
      • CORS & SOP
      • Web Server Software
  • OS
    • Operating System Questions
      • Week1 OS & How Computer Systems Work
      • Week2(1/2) Process
      • Week2(2/2) Thread
      • Week3 CPU Scheduling
      • Week4 Process Synchronize
      • Week5 Virtual Memory
    • Operating System 101
      • Operating system
        • The role of the operating system
        • The composition of the operating system.
      • Process
        • In Linux, are all processes except the initial process child processes?
        • Zombie process, orphan process
        • (Linux) Daemon process
        • Process address space
        • Where are uninitialized variables stored?
        • Determination of the size of the Stack and Heap
        • Access speed of Stack vs Heap
        • Reason for memory space partitioning
        • Process of compiling a process
        • sudo kill -9 $CURRENT_PID
      • Thread
        • Composition of a thread's address space
      • Process vs Thread
        • Creation of processes and threads in Linux
      • Multiprocessing
        • Web Browser
        • Implementation of multiprocessing
        • Application areas of multiprocessing
      • Multithreading
        • Application areas of multithreading
      • Interrupt
        • HW / SW Interrupt
        • Method of handling interrupts
        • Occurrence of two or more interrupts simultaneously
      • Polling
      • Dual Mode
        • Reason for distinguishing between user mode and kernel mode
      • System call
        • Differentiation between system calls
        • Types of system calls
        • Execution process of a system call
      • Process Control Block (PCB)
        • PCB의 구조
        • 쓰레드는 PCB를 갖고 있을까?
        • 프로세스 메모리 구조
      • Context switching
        • Timing of context switching
        • Registers saved during context switching
        • Context switching in processes
        • Context switching in threads
        • Difference between context switching in processes and threads
        • Information of the current process during context switching
      • Interprocess Communication (IPC)
        • Cases where IPC is used
        • Process address space in IPC Shared Memory technique
        • Types of IPC
  • COMPUTER SCIENCE
    • Computer Architecture 101
      • 3 components of a computer
      • RAM vs ROM
      • CPU vs GPU
      • SIMD
      • Two's complement
      • Harvard Architecture vs. von Neumann Architecture
      • The structure of a CPU.
      • Instruction cycle (CPU operation method)
      • Instruction pipelining
      • Bus
      • Memory area
      • Memory hierarchy structure
        • Reason for using memory hierarchy structure
      • Cache memory
      • L1, L2, L3 Cache
      • Locality of reference (cache)
      • Fixed-point vs Floating-point
        • epresentation of infinity and NaN (Not a Number) in floating-point
      • RISC vs CISC
      • Hamming code
      • Compiler
      • Linking
      • Compiler vs Interpreter
      • Mutex vs Semaphore
      • 32bit CPU and 64bit CPU
      • Local vs Static Variable
      • Page
  • Programming Paradigm
    • Declarative vs Imperative
  • JPA, QueryDsl
    • why fetchResults() is deprecated
  • PYTHON
    • Icecream
  • FASTAPI
    • Template Page
  • LINUX
    • Template Page
  • DATA STRUCTURE
    • Counting Sort
    • Array vs Linked List
  • GIT, Github
    • git clone, invalid path error
  • INFRA
    • Template Page
  • AWS
    • Server Log Archive Pipeline
    • Image Processing using Lambda
  • DOCKER
    • Docker and VM
    • Python Executable Environment
    • Docker commands
  • docker-compose
    • Kafka, Multi Broker
  • KUBERNATES
    • !Encountered Errors
      • my-sql restarts
      • kafka producer: disconnected
    • Kubernetes Components
    • Helm
      • Helm commands
    • Pod network
    • Service network
      • deployment.yaml
      • services.yaml
    • Service type
      • Cluster IP
      • NodePort
    • service-name-headless?
    • kube-proxy
  • GraphQL
    • Template Page
  • WEB
    • Template Page
  • Reviews
    • Graphic Intern Review
    • Kakao Brain Pathfinder Review
    • JSCODE 자바 1기 Review
  • 😁Dev Jokes
    • Image
      • Plot twist
      • Priorities
      • SQL join guide
      • Google is generous
      • Genie dislikes cloud
      • buggy bugs
      • last day of unpaid internship
      • what if clients know how to inspect
      • its just game
      • how i wrote my achievement on resume
      • self explanatory
      • chr(sum(range(ord(min(str(not))))))
Powered by GitBook
On this page
  • 1. 자바 메모리 구조
  • 2. 스레드 생성
  • 2.1. Thread 상속
  • 2.1. start() vs run()
  • 2.2. 데몬 스레드 vs 사용자 스레드(non-daemon)
  • 2.3. Runnable
  • 2.4. 로거 만들기
  • 2.5. 여러 스레드 만들기
  • 2.6. Runnable을 만드는 여러 방법들
  • 3. 스레드 제어와 생명 주기
  • 3.1. 스레드 제어
  • 3.2. 스레드 생명주기
  • 3.3. 체크 예외 재정의
  • 3.4. Sleep 유틸리티
  • 3.5. join
  • 4. 스레드 제어와 생명 주기2
  • 4.1. 인터럽트
  • 4.2. Yield
  1. JAVA

Thread

쓰레드

PreviousChecked vs Unchecked ExceptionNextBatch delete instead of Cascade

Last updated 6 months ago

1. 자바 메모리 구조

  • 메서드 영역: 프로그램을 실행하는데 필요한 공통 데이터를 관리. 모든 영역에서 공유함

    • 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재.

    • static 영역: static 변수들을 보관

    • 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관.

  • 스택 영역: 자바 실행 시, 하나의 실행 스택이 생성. 각 스텍 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함

    • 스택 프레임: 메서드를 호출할 떄 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거

    • 스택 영역은 각 스레드별로 하나의 실행 스택이 생성. 따라서 스레드 수 만큼 스택이 생성됨

  • 힙 영역: 객체와 배열이 생성되는 영역. GC가 이루어지는 주요 영역.

2. 스레드 생성

스레드를 만들 때는 Thread 클래스를 상속 받는 방법과 Runnable 인터페이스를 구현하는 방법이 있다.

2.1. Thread 상속

//메인 스레드
package thread.start;

public class HelloThreadMain {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");

        HelloThread helloThread = new HelloThread();
        System.out.println(Thread.currentThread().getName() + ": start() 호출 전");
        helloThread.start();
        System.out.println(Thread.currentThread().getName() + ": start() 호출 후");

        System.out.println(Thread.currentThread().getName() + ": main() end");
    }
}
  • 앞서 만든 HelloThread 스레드 객체를 생성하고 start() 메서드를 호출한다.

  • start() 메서드는 스레드를 실행하는 아주 특별한 메서드이다.

  • start() 를 호출하면 HelloThread 스레드가 run() 메서드를 실행한다.

// 새로운 스레드
package thread.start;

public class HelloThread  extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": run()");
    }
}
  • Thread 클래스를 상속하고, 스레드가 실행할 코드를 run() 메서드에 재정의한다.

  • Thread.currentThread()를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.

  • Thread.currentThread().getName()` : 실행 중인 스레드의 이름을 조회한다.

// 실행결과
main: main() start
main: start() 호출 전
main: start() 호출 후
main: main() end
Thread-0: run()
(main() end가 먼저 실행됐지만, 이건 스케줄링에 의해 순서가 변할 수 있음)
  • HelloThread 스레드 객체를 생성한 다음에 start() 메서드를 호출하면 자바는 스레드를 위한 별도의 스택 공간을 할당한다.

  • 스레드 객체를 생성하고 반드시 start() ( run()이 아님)를 호출해야 스택 공간을 할당 받고 스레드가 작동한다.

  • 스레드에 이름을 주지 않으면 자바는 스레드에 Thread-0, Thread-1 같은 임의의 이름 부여

  • 새로운 Thread-0 스레드가 사용할 전용 스택 공간 마련됨

  • Thread-0 스레드는 run() 메서드의 스택 프레임을 스택에 올리면서 run() 메서드를 시작.

  • main 스레드가 HelloThread 인스턴스를 생성.

  • start() 메서드 호출 후 Thread-0 스레드가 run() 메서드 호출. main 스레드가 run()을 호출하는게 아니라 Thread-0 스레드가 run() 메서드를 실행.

  • main 스레드는 단지 start() 메서드를 통해 Thread-0 스레드에게 실행을 지시할 뿐.

  • main, Thread-0 스레드는 동시에 실행

2.1. start() vs run()

// start()가 아니라 run()을 실행
package thread.start;

public class HelloThreadMain {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");

        HelloThread helloThread = new HelloThread();
        System.out.println(Thread.currentThread().getName() + ": start() 호출 전");
        helloThread.run();
        System.out.println(Thread.currentThread().getName() + ": start() 호출 후");

        System.out.println(Thread.currentThread().getName() + ": main() end");
    }
}
// 실행 결과
main: main() start
main: start() 호출 전
main: run()
main: start() 호출 후
main: main() end
  • 별도의 스레드가 run()을 실행하지 않고, main 스레드가 run() 메서드를 호출

2.2. 데몬 스레드 vs 사용자 스레드(non-daemon)

  • 사용자 스레드

    • 프로그램의 주요 작업 수행

    • 작업이 완료될 때까지 실행된다.

    • 모든 user 스레드가 종료되면 JVM도 종료된다.

  • 데몬 스레드

    • 백그라운드에서 보조적인 작업을 수행한다.

    • 모든 user 스레드가 종료되면 데몬 스레드는 자동으로 종료된다.

// 데몬 스레드
package thread.start;

public class DaemonThreadMain {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");

        DaemonThread daemonThread = new DaemonThread();
        System.out.println(Thread.currentThread().getName() + ": start() 호출 전");
        daemonThread.setDaemon(true);
        daemonThread.start();
        System.out.println(Thread.currentThread().getName() + ": start() 호출 후");

        System.out.println(Thread.currentThread().getName() + ": main() end");
    }

    static class DaemonThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ": run() start");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + ": run() end");
        }
    }
}
// run() 메서드 안에서 Thread.sleep()을 호출할때 
// 체크 예외인 InterruptedException을 밖으로 던질 수 없고 반드시 잡아야한다.
  • setDaemon(true) 데몬 스레드로 설정

  • 데몬 스레드 여부는 start() 실행전에 결정.

  • 기본 값은 false (user 스레드가 기본)

// 실행 결과
main: main() start
main: start() 호출 전
main: start() 호출 후
main: main() end
Thread-0: run() start
  • 데몬 스레드가 종료되지 않았는데, 메인 스레드가 종료됨으로서 자동으로 종료됨.

2.3. Runnable

Thread 클래스를 상속받지 않고 Runnable 인터페이스를 구현하는 방법

// Runnable Main
package thread.start;

public class HelloRunnableMain {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");

        HelloRunnable helloRunnable = new HelloRunnable();
        Thread thread = new Thread(helloRunnable);
        System.out.println(Thread.currentThread().getName() + ": start() 호출 전");
        thread.start();
        System.out.println(Thread.currentThread().getName() + ": start() 호출 후");

        System.out.println(Thread.currentThread().getName() + ": main() end");
    }
}
// Runnable Thread
package thread.start;

public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": run()");
    }
}
// 실행 결과
main: main() start
main: start() 호출 전
main: start() 호출 후
main: main() end
Thread-0: run()
  • 실행 결과는 같음

  • 다른 점은, 스레드와 해당 스레드가 실행할 작업이 서로 분리.

핵심 차이

  1. Thread 상속:

    • 실행 작업(run 메서드)과 스레드 제어(start, join 등)가 하나의 클래스에 결합됩니다.

    • 즉, 작업(비즈니스 로직)과 스레드 제어 로직이 혼재합니다.

    • 실행 작업을 다른 스레드에서 재사용하거나 독립적으로 분리하기 어렵습니다.

  2. Runnable 구현:

    • 실행 작업(run 메서드)은 Runnable 인터페이스에 의해 정의되고, 스레드 제어는 별도로 Thread 객체가 담당합니다.

    • 실행 작업과 스레드 제어가 완전히 분리되어 더 유연한 설계가 가능합니다.

작업이 분리되어 있다는 의미

Runnable 구현의 장점: 작업과 스레드의 분리

  1. 작업 로직의 재사용 가능:

    • Runnable 객체는 여러 Thread 객체에서 재사용할 수 있습니다.

    • 동일한 작업(HelloRunnable)을 여러 스레드에서 실행하거나, 다른 컨텍스트에서 실행할 때 유용합니다.

    예시:

    HelloRunnable runnable = new HelloRunnable();
    Thread thread1 = new Thread(runnable, "thread-1");
    Thread thread2 = new Thread(runnable, "thread-2");
    
    thread1.start();
    thread2.start();
  2. 작업과 스레드 관리가 독립적:

    • Runnable 구현은 스레드와 작업 로직을 명확히 분리합니다.

    • 스레드 제어(start, join, interrupt)는 Thread 클래스에서, 작업 로직은 Runnable 객체에서 관리합니다.

    • 작업 로직을 다른 스레드 관리 메커니즘(ex. ExecutorService)에서도 쉽게 사용할 수 있습니다.


실행 흐름 비교

HelloThread 상속 방식

HelloThread helloThread = new HelloThread();
helloThread.start();
  • 결합 구조:

    • HelloThread 객체는 스레드의 실행 로직과 스레드 관리 로직을 모두 포함합니다.

    • run 메서드를 실행하려면 반드시 HelloThread를 start()로 시작해야 합니다.


Runnable 구현 방식

HelloRunnable helloRunnable = new HelloRunnable();
Thread thread = new Thread(helloRunnable);
thread.start();
  • 분리 구조:

    • Runnable 객체는 실행 작업만 정의하고, 스레드 제어는 Thread가 담당합니다.

    • 실행 작업은 재사용 가능하며, 스레드 관리와 별도로 설계할 수 있습니다.

2.4. 로거 만들기

package util;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class MyLogger {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss:SSS");
    public static void log(Object obj) {
        String time = LocalTime.now().format(formatter);
        System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), obj);
    }
}
package util;

import static util.MyLogger.log;

public class MyLoggerMain {
    public static void main(String[] args) {
        log("Hello, MyLogger");
        log(123);
    }
}
05:53:14:395 [     main] Hello, MyLogger
05:53:14:397 [     main] 123

2.5. 여러 스레드 만들기

package thread.start;

import static util.MyLogger.log;

public class ManyThreadMainV1 {
    public static void main(String[] args) {
        log("main() start");

        HelloRunnable helloRunnable = new HelloRunnable();
        Thread thread1 = new Thread(helloRunnable);
        thread1.start();
        Thread thread2 = new Thread(helloRunnable);
        thread2.start();
        Thread thread3 = new Thread(helloRunnable);
        thread3.start();

        log("main() end");
    }
}
05:54:59:467 [     main] main() start
05:54:59:471 [     main] main() end
Thread-2: run()
Thread-0: run()
Thread-1: run()
  • 스레드 3개를 생성할 때 모두 같은 HelloRunnable 인스턴스(x001)를 스레드의 실행 작업으로 전달.

  • Thread-0, 1, 2는 모두 HelloRunnable 인스턴스에 있는 run() 메서드를 실행.

2.6. Runnable을 만드는 여러 방법들

  • 중첩 클래스 사용

package thread.start;

import static util.MyLogger.log;

public class InnerRunnableMainV1 {
    public static void main(String[] args) {
        log("main() start");

        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();

        log("main() end");
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            log("run()");
        }
    }
}
  • 익명 클래스 사용

package thread.start;

import static util.MyLogger.log;

public class InnerRunnableMainV2 {
    public static void main(String[] args) {
        log("main() start");

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log("run()");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();

        log("main() end");
    }
}
  • 익명 클래스 변수 없이 직접 전달

package thread.start;

import static util.MyLogger.log;

public class InnerRunnableMainV3 {
    public static void main(String[] args) {
        log("main() start");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                log("run()");
            }
        });
        thread.start();

        log("main() end");
    }
}
  • 람다

package thread.start;

import static util.MyLogger.log;

public class InnerRunnableMainV4 {
    public static void main(String[] args) {
        log("main() start");

        Thread thread = new Thread(() -> log("run()"));
        thread.start();

        log("main() end");
    }
}

3. 스레드 제어와 생명 주기

3.1. 스레드 제어

3.1.1. 스레드 생성

 Thread myThread = new Thread(new HelloRunnable(), "myThread")
  • Runnable 인터페이스: 실행할 작업을 포함하는 인터페이스. HelloRunnable 클래스는 Runnable 인터페이스를 구현한 클래스.

  • 스레드 이름: "myThread" 라는 이름으로 스레드 생성. 디버깅/로깅 목적. (default : Thread-0, -1, ...)

3.1.2. 스레드 객체 정보

log("myThread = " + myThread);
  • myThread 객체를 문자열로 변환하여 출력.

  • toString() -> 스레드ID, 스레드 이름, 우선순위, 스레드 그룹을 포함 (Thread[#21,myThread,5,main])

3.1.3. 스레드ID

log("myThread.threadId() = " + myThread.threadId());
  • threadId(): 스레드 고유 식별자를 반환. ID는 JVM내에서 유일. 직접 지정할 수 없으며 생성시 할당.

3.1.4. 스레드 이름

 log("myThread.getName() = " + myThread.getName());
  • getName(): 스레드 이름 반환. 스레드 이름은 중복 가능

3.1.5. 스레드 우선순위

 log("myThread.getPriority() = " + myThread.getPriority());
  • getPriority(): 스레드의 우선순위를 반환하는 메서드. 1(가장 낮음)~ 10(가장 높음). 기본값은 5. setPriority()를 통해 우선순위 변경 가능.

  • 스레드 스케줄러가 어떤 스레드를 우선 실행할지 결정하는데 사용. JVM구성과 OS따라 달라질 수 있음

3.1.6. 스레드 그룹 (잘 안쓰임)

 log("myThread.getThreadGroup() = " + myThread.getThreadGroup());
  • getThreadGroup(): 스레드가 속한 그룹을 반환. 스레드 그룹은 스레드를 그룹화하여 관리할 수 있는 기능 제공. 여러 스레드를 묶어서 특정 작업 수행가능(일괄 종료, 우선순위 설정 등)

  • 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속한다.

  • 부모 스레드: 새로운 스레드를 생성하는 스레드를 의미. 스레드는 다른 스레드에 의해 생성되는데, 이러한 생성 관계에서 새로 생성된 스레드는 생성한 스레드를 부모로 간주.

3.1.7. 스레드 상태

 log("myThread.getState() = " + myThread.getState());
  • getState(): 스레드의 현재 상태를 반환.

    • NEW: 아직 시작되지 않은 상태

    • RUNNABLE: 스레드가 실행중 / 실행될 준비가 된 상태

    • BLOCKED: 동기화 락을 기다리는 상태

    • WAITING: 다른 스레드의 특정 작업이 완료되기를 기다리는 상태

    • TIMED_WAITING: 일정 시간 동안 기다리는 상태

    • TERMINATED: 스레드가 실행을 마친 상태

3.2. 스레드 생명주기

  • NEW: 아직 시작되지 않은 상태

    • 스레드가 생성되고 아직 시작되지 않은 상태

    • Thread 객체가 생성되지만 start() 메서드가 아직 호출되지 않은 상태.

  • RUNNABLE: 스레드가 실행중 / 실행될 준비가 된 상태

    • 스레드가 실행될 준비가 된 상태. 실제 CPU에서 실행될 수 있음. start() 메서드가 호출되면 이 상태로 됨.

    • RUNNABLE은 실행되고 있는 뜻이 아니라, OS의 스케줄러가 각 스레드에 CPU 시간을 할당하여 실행하기 떄문에, Runnable 상태에 있는 스레드는 스케줄러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행.

  • BLOCKED: 동기화 락을 기다리는 상태

    • synchronized (lock) 같은 코드 블록에 진입하려고 할때, 다른 스레드가 이미 lock을 가지고 있는 경우

  • WAITING: 다른 스레드의 특정 작업이 완료되기를 기다리는 상태

    • wait(), join() 메서드가 호출될때 이 상태로 됨. 다른 스레드가 notify() / notifyAll() 메서드를 호출하거나 join()이 완료될때까지 기다림.

  • TIMED_WAITING: 일정 시간 동안 기다리는 상태

    • sleep(XXms) / wait(long timeout), join(long millis) 메서드가 호출될때 이 상태로 ㅗ딤.

    • 주어진 시간이 경과 / 다른 스레드가 해당 스레드를 꺠우면 상태에서 벗어남.

  • TERMINATED: 스레드가 실행을 마친 상태

    • 정상 종료 / 예외가 발생하여 종료된 경우. 스레드는 한번 종료되면 재시작 불가.

상태 전이 과정:

  • NEW -> RUNNABLE: start() 메서드를 호출

  • RUNNABLE -> BLOCKED / WAITING / TIMED_WAITING: 락을 얻지 못하거나 wait() / sleep() 메서드를 호출

  • BLOCKED / WAITING / TIMED_WAITING -> RUNNABLE: 스레드가 락을 얻음 / 기다림이 완료

  • RUNNABLE -> TERMINATED: run() 메서드가 완료됨

import static util.MyLogger.log;

public class ThreadStateMain {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyRunnable(), "myThread");
        log("myThread.state1 = " + thread.getState()); // NEW
        log("myThread.start()");
        thread.start();
        Thread.sleep(1000);
        log("myThread.state3 = " + thread.getState()); // TIMED_WAITING
        Thread.sleep(4000);
        log("myThread.state5 = " + thread.getState()); // TERMINATED
        log("end");
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            try {
                log("start");
                log("myThread.state2 = " +
                        Thread.currentThread().getState()); // RUNNABLE
                log("sleep() start");
                Thread.sleep(3000);
                log("sleep() end");
                log("myThread.state4 = " +
                        Thread.currentThread().getState()); // RUNNABLE
                log("end");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
// 실행 결과
 11:40:31.503 [     main] myThread.state1 = NEW
 11:40:31.505 [     main] myThread.start()
 11:40:31.505 [ myThread] start
 11:40:31.505 [ myThread] myThread.state2 = RUNNABLE
 11:40:31.505 [ myThread] sleep() start
 11:40:32.507 [     main] myThread.state3 = TIMED_WAITING
 11:40:34.510 [ myThread] sleep() end
 11:40:34.512 [ myThread] myThread.state4 = RUNNABLE
 11:40:34.512 [ myThread] end
 11:40:36.511 [     main] myThread.state5 = TERMINATED
 11:40:36.512 [     main] end
  • Thread.currentThread() 를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.

  • Thread.sleep(): 해당 코드를 호출한 스레드는 TIMED_WAITING 상태가 되면서 특정 시간 만큼 대기한다. 시간은 밀리초(ms) 단위이다. 1밀리초 = 1/1000 초, 1000밀리초 = 1초이다.

  • Thread.sleep(): InterruptedException 이라는 체크 예외를 던진다. 따라서 체크 예외를 잡아서 처리하거나 던져야 한다

  • InterruptedException: 은 인터럽트가 걸릴 때 발생.

3.3. 체크 예외 재정의

public interface Runnable {
    void run();
}

Runnable 인터페이스는 위와 같이 정의 되어있다.

체크 예외는:

  • 부모 메서드가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없다.

  • 자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.

위 규칙에 따라, runnable 인터페이스의 run() 메서드를 재정의하는 곳에서는 체크 예외를 밖으로 던질 수 없음.

체크 예외를 run() 메서드에서 던질 수 없게 강제하고 있는데, 이는 개발자가 반드시 예외를 try-cahtch 블록 내에서 처리해야된다. (프로그램의 비정상 종료 상황 방지). 특히 멀티스레딩 환경에서는 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지할 수 있음.

3.4. Sleep 유틸리티

Thread.sleep() 메서드는 InterruptedException 체크 예외를 발생시킨다. 이를 매번 try-catch로 감싸주지 않는 방식으로 변경할 수 있음.

package util;

import static util.MyLogger.log;

public abstract class ThreadUtils {
    public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            log("인터럽트 발생, " + e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

3.5. join

Join은 특정 시간 만큼만 대기함.

  • join(): 호출 스레드는 대상 스레드가 완료될 때까지 무한정 대기

  • join(ms): 호출 스레드는 특정 시간 동안 만큼만 대기. 호출 스레드는 지정한 시간이 지나면 다시 RUNNABLE 상태가됨.

package thread.control.join;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class JoinMainV4 {
    public static void main(String[] args) throws InterruptedException {
        log("Start");
        SumTask task1 = new SumTask(1, 50);
        Thread thread1 = new Thread(task1, "thread-1");

        thread1.start();

        //스레드가 종료될 때 까지 대기
        log("join(1000) - main 스레드가 thread1 종료까지 1초 대기");
        thread1.join(1000);
        log("main 스레드 대기 완료");

        log("task1.result = " + task1.result);
    }

    static class SumTask implements Runnable {
        int startValue;
        int endValue;
        int result = 0;

        public SumTask(int startValue, int endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }

        @Override
        public void run() {
            log("작업 시작");
            sleep(2000);
            int sum = 0;
            for (int i = startValue; i <= endValue; i++) {
                sum += i;
            }
            result = sum;
            log("작업 완료 result = " + result);
        }
    }
}
  • 별도의 스레드에서 1-50까지 더하고, 그 결과를 조회

  • join(1000)을 사용해서 1초만 대기

// 실행 결과
17:34:54.572 [     main] Start
17:34:54.575 [     main] join(1000) - main 스레드가 thread1 종료까지 1초 대기
17:34:54.575 [ thread-1] 작업 시작
17:34:55.580 [     main] main 스레드 대기 완료
17:34:55.585 [     main] task1.result = 0
17:34:56.580 [ thread-1] 작업 완료 result = 1275
  • main 스레드는 join(1000)을 사용해서 thread-1을 1초간 기다린다. 이때 main 스레드의 생태는 TIMED_WAITING이다.

  • thread-1의 작업에는 2초가 걸린다.

  • 1초가 지나도 thread-1의 작업이 완료되지 않으므로, main 스레드는 대기를 중단. 그리고 main 스레드는 다시 RUNNABLE 상태로 바뀌면서 다음 코드를 수행. 이때 thread-1의 작업이 아직 완료되지 않았기 때문에 task1.result=0이 출력.

  • main 스레드가 종료된 이후에 thread-1이 계산을 끝낸다. result=1275가 출력.

package thread.control.test;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class JoinTest1Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyTask(), "t1");
        Thread t2 = new Thread(new MyTask(), "t2");
        Thread t3 = new Thread(new MyTask(), "t3");
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        t3.start();
        t3.join();
        System.out.println("모든 스레드 실행 완료");
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i <= 3; i++) {
                log(i);
                sleep(1000);
            }
        }
    }
}
 
  • 위 코드는 총 9초의 실행이 걸림. join()이 될때까지 3초를 기다림

package thread.start;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class JoinTest2Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyTask(), "t1");
        Thread t2 = new Thread(new MyTask(), "t2");
        Thread t3 = new Thread(new MyTask(), "t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println("모든 스레드 실행 완료");
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i <= 3; i++) {
                log(i);
                sleep(1000);
            }
        }
    }
}
 
  • 위 코드는 효율적으로 스레드를 사용하고 있다.

  • 총 걸리는 시간은 3초.

4. 스레드 제어와 생명 주기2

4.1. 인터럽트

package thread.control.interrupt;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class ThreadStopMainV1 {
    public static void main(String[] args) {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "work");
        thread.start();
        sleep(4000);
        log("작업 중단 지시 runFlag=false");
        task.runFlag = false;
    }

    static class MyTask implements Runnable {
        volatile boolean runFlag = true; // 제어 변수

        @Override
        public void run() {
            while (runFlag) {
                log("작업 중");
                sleep(3000);
            }
            log("자원 정리");
            log("작업 종료");
        }
    }
}
// 실행 결과
14:58:27.520 [     work] 작업 중
14:58:30.525 [     work] 작업 중
14:58:31.510 [     main] 작업 중단 지시 runFlag=false
14:58:33.532 [     work] 자원 정리  <- 2초
14:58:33.533 [     work] 작업 종료
  • 특정 스레드의 작업을 중단하는 가장 쉬운 방법은 변수를 사용. 하지만, 변수를 사용하더라도 스레드가 즉각 반응을 하지 않음.

  • sleep(3000)으로 인해 3초동안 잠들어 있기때문에, 변수가 변경되어도 자원 정리까지 대기 시간 발생

이를 해결하기 위해 인터럽트를 사용할 수 있다. 인터럽트를 사용하면 WAITING, TIMED_WAITING같은 대기 상태의 스레드를 직접 꺠워서, RUNNABLE 상태로 만들 수 있음.

package thread.control.interrupt;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class ThreadStopMainV2 {
    public static void main(String[] args) {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "work");
        thread.start();
        sleep(4000);
        log("작업 중단 지시 thread.interrupt()");
        thread.interrupt();
        log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    log("작업 중");
                    Thread.sleep(3000);
                }
            } catch (InterruptedException e) {
                log("work 스레드 인터럽트 상태2 = " +
                        Thread.currentThread().isInterrupted());
                log("interrupt message=" + e.getMessage());
                log("state=" + Thread.currentThread().getState());
            }
            log("자원 정리");
            log("작업 종료");
        }
    }
}
  • 특정 스레드의 인스턴스에 interrupt() 메서드가 호출되면, 해당 스레드에 인터럽트가 발생.

  • 인터럽트가 발생하면 해당 스레드에 InterruptedException 발생. 이로 인해 RUNNABLE 상태로 전환.

  • 다만, 즉각적으로 InterruptedException이 발생하지는 않는다.

// 실행 결과
18:10:40.024 [     work] 작업 중
18:10:43.026 [     work] 작업 중
18:10:44.011 [     main] 작업 중단 지시 thread.interrupt()
18:10:44.021 [     main] work 스레드 인터럽트 상태1 = true
18:10:44.021 [     work] work 스레드 인터럽트 상태2 = false
18:10:44.022 [     work] interrupt message=sleep interrupted
18:10:44.022 [     work] state=RUNNABLE
18:10:44.022 [     work] 자원 정리
18:10:44.023 [     work] 작업 종료
  • Thread.interrupt를 통해 작업을 중단하면 거의 즉각적으로 인터럽트가 발생

  • 이떄 work 스레드는 TIMED_WAITING -> RUNNABLE 상태로 변경.

  • 하지만, while (true) 부분을 체크하지 않기때문에 인터럽트가 발생해도 이 부분은 항상 true 이기 떄문에 다음 코드로 넘어감. 그리고 sleep()을 호출하고 나서야 인터럽트가 발생.

while( isInterrupted() )으로 변경하면 조금 더 빨리 반응할 수 있음. 하지만 이렇게 하게되면 work 상태가 계속 true로 유지되는게 문제.

이를 해결하기 위해, Thread.interrupted() 메서드를 사용.

  • 스레드가 인터럽트 상태라면 true를 반환하고 해당 스레드의 인터럽트 상태를 false로 변경

  • 스레드가 인터럽트 상태가 아니라면 false를 반환하고, 해당 스레드의 인터럽트 상태를 변경하지 않음.

package thread.control.interrupt;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class ThreadStopMainV4 {
    public static void main(String[] args) {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "work");
        thread.start();
        sleep(100); //시간을 줄임
        log("작업 중단 지시 - thread.interrupt()");
        thread.interrupt();
        log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
        
            // 중요
            while (!Thread.interrupted()) { //인터럽트 상태 변경O
                log("작업 중"); 
            }
            
            
            log("work 스레드 인터럽트 상태2 = " +
                    Thread.currentThread().isInterrupted());
            try {
                log("자원 정리 시도");
                Thread.sleep(1000);
                log("자원 정리 완료");
            } catch (InterruptedException e) {
                log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
                log("work 스레드 인터럽트 상태3 = " +
                        Thread.currentThread().isInterrupted());
            }
            log("작업 종료");
        }
    }
}
// 실행 결과
...
15:40:45:356 [     work] 작업 중
15:40:45:356 [     work] 작업 중
15:40:45:357 [     work] 작업 중
15:40:45:357 [     work] 작업 중
15:40:45:357 [     main] 작업 중단 지시 - thread.interrupt()
15:40:45:357 [     work] 작업 중
15:40:45:364 [     work] work 스레드 인터럽트 상태2 = false
15:40:45:364 [     main] work 스레드 인터럽트 상태1 = false
15:40:45:364 [     work] 자원 정리 시도
15:40:46:365 [     work] 자원 정리 완료
15:40:46:365 [     work] 작업 종료
  • 결과적으로 while문을 탈출하는 시점에, 스레드의 인터럽트 상태도 false로 변경.

  • 자바는 인터럽트 예외가 한번 발생하면, 스레드의 인터럽트 상태를 다시 정상(false)로 되돌림. 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생.

4.2. Yield

어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다. 그런데 특정 스레드가 크게 바쁘지 않은 상황 이어서 다른 스레드에 CPU 실행 기회를 양보하고 싶을 수 있다. 이렇게 양보하면 스케줄링 큐에 대기 중인 다른 스레드 가 CPU 실행 기회를 더 빨리 얻을 수 있다

자바의 스레드가 RUNNABLE 상태일때, 운영체제의 스케줄링은 아래의 상태를 가질 수 있다:

  • Running: CPU에서 실행중

  • Ready: CPU에서 실행되길 기다리며 큐에 대기중

yield()는 현재 실행 중인 스레드가 자발적으로 CPU를 양보하여 다른 스레드가 실행될 수 있도록한다. 메서드를 호출한 스레드는 RUNNABLE 상태를 유지하면서 CPU를 양보한다. 즉, 이 스레드는 다시 스케줄링 큐에 들어가면서 다른 스레드에게 CPU 사용 기회를 넘긴다. 다만, CPU가 비어있다면 큐에서 다시 꺼내서 실행된다.

package thread.control.printer;

import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;

import static util.MyLogger.log;

public class MyPrinterV4 {
    public static void main(String[] args) throws InterruptedException {
        Printer printer = new Printer();
        Thread printerThread = new Thread(printer, "printer");
        printerThread.start();
        Scanner userInput = new Scanner(System.in);
        while (true) {
            System.out.println("프린터할 문서를 입력하세요. 종료 (q): ");
            String input = userInput.nextLine();
            if (input.equals("q")) {
                printerThread.interrupt();
                break;
            }
            printer.addJob(input);
        }
    }

    static class Printer implements Runnable {
        Queue<String> jobQueue = new ConcurrentLinkedQueue<>();

        @Override
        public void run() {
            while (!Thread.interrupted()) {
                if (jobQueue.isEmpty()) {
                    Thread.yield(); //추가
                    continue;
                }
                try {
                    String job = jobQueue.poll();
                    log("출력 시작: " + job + ", 대기 문서: " + jobQueue);
                    Thread.sleep(3000); //출력에 걸리는 시간
                    log("출력 완료: " + job);
                } catch (InterruptedException e) {
                    log("인터럽트!");
                    break;
                }
            }
            log("프린터 종료");
        }

        public void addJob(String input) {
            jobQueue.offer(input);
        }
    }
}
// 입력과 실행 결과
프린터할 문서를 입력하세요. 종료 (q): 
a
프린터할 문서를 입력하세요. 종료 (q): 
15:46:30:670 [  printer] 출력 시작: a, 대기 문서: []
b
프린터할 문서를 입력하세요. 종료 (q): 
c
프린터할 문서를 입력하세요. 종료 (q): 
d
프린터할 문서를 입력하세요. 종료 (q): 
15:46:33:675 [  printer] 출력 완료: a
15:46:33:675 [  printer] 출력 시작: b, 대기 문서: [c, d]
15:46:36:690 [  printer] 출력 완료: b
15:46:36:690 [  printer] 출력 시작: c, 대기 문서: [d]
q
15:46:37:343 [  printer] 인터럽트!
15:46:37:343 [  printer] 프린터 종료

Process finished with exit code 0