7. 프로세스와 스레드

header

1. 프로세스와 스레드

  • 프로그램: 실행 전, 디스크에 저장된 코드의 집합입니다.

  • 프로세스: 프로그램이 메모리에 적재되어 실행 중인 인스턴스입니다.
    각 프로세스는 고유의 주소 공간(코드, 데이터, 힙, 스택)과 시스템 자원을 가지며, 서로 격리되어 있습니다.

  • 스레드: 프로세스 내부에서 실행 흐름을 담당하는 단위입니다.
    스레드들은 코드, 데이터, 영역을 공유하고, 각자 독립적인 스택을 가집니다.
    하나의 프로세스에는 최소 1개의 메인 스레드가 존재합니다.

우리는 길을 걷으며 통화하고 식사를 하며 동시에 여러 가지 일을 수행하고 있습니다.
이렇게 동시에 여러 가지 일을 할 수 있는 것은 비단 사람뿐만 아닙니다.

‼️ 멀티태스킹은 여러 프로세스 또는 여러 스레드를 통해 구현됩니다.
‼️ 스레드는 공유 자원에 접근할 때 경쟁 상태가 발생할 수 있으므로, 반드시 동기화가 필요합니다.

컬렉션 프레임워크
프로세스와 스레드

예시: 현재 스레드 조회

public class ThreadNameDemo {
    public static void main(String[] args) {
        String threadName = Thread.currentThread().getName();
        System.out.println("현재 스레드 이름: " + threadName); // 보통 "main"
    }
}

2. 스레드 생성과 실행

2.1 Thread 클래스 상속

  • run()에 작업을 구현하고, start()새 실행 흐름을 시작한다.
  • run()을 직접 호출하면 동기 실행이 된다.
class MyThread extends Thread {
    @Override
    public void run() {
        int sum = 0;
        for (int i = 0; i < 10; i++) sum += (i + 1);
        System.out.println("sum = " + sum);
        String name = Thread.currentThread().getName();
        System.out.println("현재 스레드 이름: " + name);
    }
}

public class ThreadExtDemo {
    public static void main(String[] args) {
        MyThread th = new MyThread();
        th.start();
    }
}

2.2 Runnable 구현

  • 다중 상속 제약: 자바는 다중 상속을 지원하지 않습니다.
    이미 다른 클래스를 상속받고 있다면 Thread를 상속할 수 없으므로, Runnable 인터페이스를 구현해야 합니다.

  • 역할 분리:
    • Thread: 실제 스레드 생성, 스케줄링, 생명주기 관리 담당
    • Runnable: 실행할 작업의 로직만 정의 (실행 코드 제공)
  • 장점: 작업 로직과 스레드 관리가 분리되어 코드의 재사용성과 유연성이 향상됩니다.
class CalcTask implements Runnable {
    @Override
    public void run() {
        System.out.println("작업 실행: " + Thread.currentThread().getName());
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        Runnable task = new CalcTask();
        Thread th = new Thread(task); // Runnable을 Thread에 주입
        th.start();
    }
}

2.3 익명 클래스와 람다

  • 함수형 인터페이스: Runnablerun() 메서드 하나만을 가지는 함수형 인터페이스입니다.
  • 람다식 활용:
    • 기존의 익명 클래스 대신 람다식으로 간결하게 표현 가능
    • 코드의 가독성과 작성 편의성이 크게 향상됩니다
    • 함수형 프로그래밍 패러다임을 활용한 현대적인 스타일
public class LambdaDemo {
    public static void main(String[] args) {
        Runnable white = () -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("백기 올려");
            }
        };
        Thread whiteFlag = new Thread(white);
        whiteFlag.start();
    }
}

2.4 스레드에 이름 부여

  • 기본 이름: 스레드 생성 시 자동으로 Thread-0, Thread-1, Thread-2… 형태로 이름이 부여됩니다.
  • 명시적 이름 설정: setName() 메서드로 의미 있는 이름을 부여할 수 있습니다.
    • 디버깅 시 스레드 구분이 용이해집니다
    • 로그 분석 시 어떤 작업을 수행하는 스레드인지 쉽게 파악 가능합니다
class MyTask extends Thread {
    public MyTask() {
        setName("myTask-1");
    }

    public void run() {
        System.out.println(getName());
    }
}

public class NameDemo {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println(Thread.currentThread().getName());
        Thread th = new Thread(r);
        th.setName("thread-blue");
        th.start();
    }
}

2.5 멀티 스레드 시작 순서 예시

  • 비결정적 실행: 스레드의 시작 순서는 비결정적(Non-deterministic)입니다.
    • JVM과 OS의 스케줄링에 따라 실행 순서가 달라질 수 있습니다
    • start() 메서드 호출 순서와 실제 실행 순서는 다를 수 있습니다
    • 멀티코어 환경에서는 진정한 병렬 실행이 가능합니다
  • 실험적 관찰: 두 개의 스레드를 동시에 시작하여 실행 패턴을 관찰해보세요.
    • 매번 실행할 때마다 다른 결과가 나올 수 있습니다
    • 이는 멀티스레딩의 비결정적 특성을 보여주는 좋은 예시입니다
public class MultiStartDemo {
    public static void main(String[] args) {
        Runnable blue = () -> {
            while (true) {
                System.out.println("청기 올려");
            }
        };
        Runnable white = () -> {
            while (true) {
                System.out.println("백기 올려");
            }
        };

        Thread blueFlag = new Thread(blue, "blue-flag");
        Thread whiteFlag = new Thread(white, "white-flag");

        blueFlag.start();
        whiteFlag.start();
    }
}

문제 1 - 스레드 기본 사용법 (기초)

아래 코드의 주석을 참고하여, 현재 스레드 이름을 출력하고 새로운 스레드를 생성하여 시작하는 코드를 완성하세요.

public class Exercise01 {
    public static void main(String[] args) {
        // TODO: 현재 스레드 이름 출력 (Thread.currentThread().getName())
        
        // TODO: Runnable 구현으로 새 스레드 생성 (람다식 사용)
        
        // TODO: Thread로 감싸서 시작 (start() 메서드 사용)
    }
}
정답 보기

  public class Exercise01 {
      public static void main(String[] args) {
          // TODO: 현재 스레드 이름 출력 (Thread.currentThread().getName())
          System.out.println("현재 스레드: " + Thread.currentThread().getName());
          
          // TODO: Runnable 구현으로 새 스레드 생성 (람다식 사용)
          Runnable task = () -> {
              System.out.println("새 스레드: " + Thread.currentThread().getName());
          };
          
          // TODO: Thread로 감싸서 시작 (start() 메서드 사용)
          Thread newThread = new Thread(task);
          newThread.start();
      }
  }
  

문제 2 - Thread 상속 vs Runnable 구현 비교 (중급)

Thread 상속 방식과 Runnable 구현 방식 각각으로 1~5를 출력하는 스레드를 만들고 동시에 실행해보세요.
Thread.sleep(10)을 사용하여 출력이 섞이는 현상을 관찰해보세요.

public class Exercise02 {
    // TODO: Thread 상속 클래스 정의 (1~5 출력, sleep(10) 포함)
    static class NumberThread {}

    public static void main(String[] args) throws Exception {
        // TODO: Thread 상속 방식으로 스레드 생성 및 시작
        
        // TODO: Runnable 구현 방식으로 스레드 생성 및 시작 (람다식 사용)
        
        // TODO: 두 스레드가 모두 완료될 때까지 대기
    }
}
정답 보기

  public class Exercise02 {
      // Thread 상속 클래스
      static class NumberThread extends Thread {
          @Override
          public void run() {
              for (int i = 1; i <= 5; i++) {
                  System.out.println("Thread 상속: " + i);
                  try {
                      Thread.sleep(10);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
      public static void main(String[] args) throws Exception {
          // Thread 상속 방식
          NumberThread thread1 = new NumberThread();
          thread1.start();
          
          // Runnable 구현 방식
          Thread thread2 = new Thread(() -> {
              for (int i = 1; i <= 5; i++) {
                  System.out.println("Runnable 구현: " + i);
                  try {
                      Thread.sleep(10);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          });
          thread2.start();
          
          // 두 스레드 완료 대기
          thread1.join();
          thread2.join();
          
          System.out.println("모든 스레드 완료!");
      }
  }
  

3. 스레드 동기화

3.1 경쟁 상태 데모

  • 경쟁 상태(Race Condition): 여러 스레드가 동시에 공유 자원에 접근할 때 발생하는 문제입니다.
  • 데이터 손실: 락 없이 공유 자원을 갱신하면 읽기-수정-쓰기 과정에서 값이 누락되거나 덮어써질 수 있습니다.
  • 원자성 부족: balance += amount 같은 연산은 원자적이지 않아 중간에 다른 스레드가 개입할 수 있습니다.
class Bank {
    private int balance = 0;

    public void deposit(int amount) {
        balance += amount; // 원자적이지 않은 연산
    }

    public int getBalance() {
        return balance;
    }
}

class AddThread implements Runnable {
    private String name;
    private Bank bank;

    public AddThread(String name, Bank bank) {
        this.name = name;
        this.bank = bank;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                Thread.sleep(100); // 스레드 전환을 유도
                bank.deposit(1000);
                System.out.println(name + " 현재 잔고: " + bank.getBalance());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class RaceDemo {
    public static void main(String[] args) throws InterruptedException {
        Bank bank = new Bank();
        
        Thread thread1 = new Thread(new AddThread("Thread-1", bank));
        Thread thread2 = new Thread(new AddThread("Thread-2", bank));
        
        thread1.start();
        thread2.start();
        
        thread1.join();
        thread2.join();
        
        System.out.println("최종 잔고: " + bank.getBalance()); // 예상: 20,000, 실제: 종종 20,000 미만
    }
}

3.2 메서드 동기화

  • synchronized 메서드: 메서드 전체를 동기화하여 한 번에 하나의 스레드만 접근 가능합니다.
  • 인스턴스 단위 락: 같은 객체의 synchronized 메서드들은 모두 같은 락을 공유합니다.
  • 임계 영역: 동기화된 메서드 내부는 원자적으로 실행되는 안전한 영역입니다.
class SafeBankA {
    private int balance = 0;

    public synchronized void deposit(int amount) {
        balance += amount; // 임계 영역 - 원자적 실행 보장
    }

    public synchronized int getBalance() {
        return balance; // 임계 영역 - 일관된 값 읽기 보장
    }
}

3.3 블록 동기화

  • 세밀한 제어: 메서드 전체가 아닌 필요한 최소 영역만 동기화합니다.
  • 성능 향상: 동기화 범위를 줄여 병렬성을 향상시킵니다.
  • 유연성: 메서드 내에서 일부만 동기화하거나 다른 객체를 락으로 사용할 수 있습니다.
@Override
public void run() {
    synchronized (bank) {
        try {
            for (int i = 0; i < 1000; i++) {
                Thread.sleep(1); // 스레드 전환을 유도
                bank.deposit(1);
                System.out.println(name + " 현재 잔고: " + bank.getBalance());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

문제 3 - 동기화 기법 비교 (중급)

메서드 동기화는 Bank 클래스에서, 블록 동기화는 AddThread에서 처리하는 방식으로 20000을 보장하세요.

public class Exercise03 {
    // TODO: SafeBankA 구현 (메서드 동기화 - Bank에서 처리)
    
    // TODO: SafeAddThread 구현 (블록 동기화 - AddThread에서 처리)
    
    public static void main(String[] args) throws InterruptedException {
        // TODO: 각 방식으로 2개 스레드 10 회 입금 테스트
    }
}
정답 보기

  public class Exercise03 {
      // 메서드 동기화 - Bank 클래스에서 처리
      static class SafeBankA {
          private int balance = 0;
          
          public synchronized void deposit(int amount) {
              balance += amount;
          }
          
          public synchronized int getBalance() {
              return balance;
          }
      }
      
      // 블록 동기화 - AddThread에서 처리
      static class SafeAddThread implements Runnable {
          private String name;
          private Bank bank;
          
          public SafeAddThread(String name, Bank bank) {
              this.name = name;
              this.bank = bank;
          }
          
          @Override
          public void run() {
              try {
                  for (int i = 0; i < 10000; i++) {
                      Thread.sleep(1);
                      synchronized (bank) { // 블록 동기화
                          bank.deposit(1);
                          System.out.println(name + " 현재 잔고: " + bank.getBalance());
                      }
                  }
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      
      public static void main(String[] args) throws InterruptedException {
          // 메서드 동기화 테스트
          SafeBankA bankA = new SafeBankA();
          Thread t1 = new Thread(new AddThread("Thread-1", bankA));
          Thread t2 = new Thread(new AddThread("Thread-2", bankA));
          
          t1.start(); t2.start();
          t1.join(); t2.join();
          System.out.println("메서드 동기화 결과: " + bankA.getBalance());
          
          // 블록 동기화 테스트
          Bank bankB = new Bank();
          Thread t3 = new Thread(new SafeAddThread("SafeThread-1", bankB));
          Thread t4 = new Thread(new SafeAddThread("SafeThread-2", bankB));
          
          t3.start(); t4.start();
          t3.join(); t4.join();
          System.out.println("블록 동기화 결과: " + bankB.getBalance());
      }
  }
  

4. 스레드 상태와 전이

4.1 상태표

상태 상수 설명
생성 NEW new Thread()로 생성. start()
대기/실행 가능 RUNNABLE 러닝 또는 러너블 큐 대기
일시정지 WAITING 타 스레드 신호를 무기한 대기(wait(), join() 등)
일시정지(시간지정) TIMED_WAITING 제한 시간 대기(sleep(ms), wait(ms) 등)
일시정지(블로킹) BLOCKED 모니터 락 획득 대기
종료 TERMINATED run() 완료 또는 예외 종료

4.2 sleep: 시간 지연

  • 상태 전이: 현재 스레드를 TIMED_WAITING 상태로 전이시킵니다.
  • 락 해제: 스레드가 대기하는 동안 락을 유지하지 않습니다.
    • 다른 스레드가 동기화된 메서드나 블록에 접근할 수 있습니다
    • CPU 자원을 다른 스레드가 사용할 수 있습니다
  • 자동 복귀: 지정된 시간이 지나면 자동으로 RUNNABLE 상태로 돌아갑니다.
public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("A");
        Thread.sleep(500); // 0.5s 대기
        System.out.println("B");
    }
}

4.3 wait() / notify(): 조건 대기와 신호

  • 호출 제한: 모니터 락이 걸린 동기화 블록/메서드 내부에서만 호출 가능합니다.
    • synchronized 블록이나 메서드 밖에서는 IllegalMonitorStateException 발생
    • 스레드가 해당 객체의 락을 보유하고 있을 때만 사용 가능
  • wait() 메서드:
    • 현재 스레드가 락을 반납하고 WAITING 상태로 전이
    • 다른 스레드가 notify() 또는 notifyAll()을 호출할 때까지 대기
    • 깨어나면 다시 락을 획득한 후 실행 계속
  • notify() / notifyAll() 메서드:
    • 대기 중인 스레드에게 신호를 보냅니다
    • notify(): 대기 중인 스레드 중 하나만 깨움 (선택은 JVM이 결정)
    • notifyAll(): 대기 중인 모든 스레드를 깨움
    • 락은 호출 스레드가 동기화 블록을 완전히 빠져나갈 때 해제됩니다

4.4 생산자-소비자 미니 예제

생산자-소비자 미니 예제
생산자-소비자 미니 예제
public class Worker {
    private int stackCount = 10;

    public synchronized void addStack(int stackCount) {
        this.stackCount += stackCount;

        if (this.stackCount > 10) {
            system.out.println("=====작업자 깨우기=====");
            notify();
        }
    }

    public synchronized void popStack(int leaveCount) {
        try {
            if (leaveCount > this.stackCount) {
                this.stackCount = 0;
            } else {
                this.stackCount -= leaveCount;
            }

            if (this.stackCount == 0) {
                system.out.println("=====작업자 대기=====");
                wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public int getStackCount() {
        return this.stackCount;
    }
}

public class AddStackThread extends Thread {
    private Worker worker;

    public AddStackThread(Worker worker) {
        this.worker = worker;
    }

    @Override
    public void run() {
        try {
            while (true) {
                Thread.sleep(1000);
                if (this.worker.getStackCount() == 10) {
                    system.out.println("=====짐 10개 추가=====")
                    this.worker.addStack(10);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class PopStackThread extends Thread {
    private Worker worker;

    public PopStackThread(Worker worker) {
        this.worker = worker;
    }

    @Override
    public void run() {
        try {
            while (true) {
                Thread.sleep(1000);
                system.out.println("=====짐 5개 나르기=====");
                this.worker.popStack(5);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Worker worker = new Worker();
        AddStackThread addStackThread = new AddStackThread(worker);
        PopStackThread popStackThread = new PopStackThread(worker);
        addStackThread.start();
        popStackThread.start();
    }
}

주의

  • wait/notifysynchronized 없이 호출하면 IllegalMonitorStateException.
  • if 대신 while로 조건을 검사해 spurious wakeup 방지.
  • 다수 스레드라면 notifyAll()이 안전.

문제 4 - 생산자-소비자 패턴 (중급)

용량 5인 버퍼를 만들고 생산자 1명, 소비자 2명으로 구성하세요. while 조건과 notifyAll을 사용하세요.

public class Exercise04 {
    static class BoundedBuffer {
        private final int[] buffer = new int[5]; // 용량 5인 버퍼
        private int count = 0; // 현재 저장된 아이템 수
        private int putIndex = 0; // 다음 저장 위치
        private int takeIndex = 0; // 다음 가져올 위치
        
        // TODO: offer 메서드 구현 (버퍼가 가득 찬 경우 대기)
        public synchronized void offer(int item) throws InterruptedException {
            // TODO: while 조건으로 버퍼가 가득 찬지 확인
            // TODO: wait() 호출
            // TODO: 아이템 저장 및 인덱스 업데이트
            // TODO: notifyAll() 호출
        }
        
        // TODO: take 메서드 구현 (버퍼가 비어있는 경우 대기)
        public synchronized int take() throws InterruptedException {
            // TODO: while 조건으로 버퍼가 비어있는지 확인
            // TODO: wait() 호출
            // TODO: 아이템 반환 및 인덱스 업데이트
            // TODO: notifyAll() 호출
            return 0; // 임시 반환값
        }
    }
    
    public static void main(String[] args) {
        BoundedBuffer buffer = new BoundedBuffer();
        
        // TODO: 생산자 스레드 생성 (1~10 숫자 생성)
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    buffer.offer(i);
                    System.out.println("생산: " + i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        // TODO: 소비자 스레드 2개 생성
        Thread consumer1 = new Thread(() -> {
            try {
                while (true) {
                    int item = buffer.take();
                    System.out.println("소비자1: " + item);
                    Thread.sleep(150);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        Thread consumer2 = new Thread(() -> {
            try {
                while (true) {
                    int item = buffer.take();
                    System.out.println("소비자2: " + item);
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        // TODO: 모든 스레드 시작
    }
}
정답 보기

  public class Exercise04 {
      // TODO: BoundedBuffer 구현 (offer/take 동기화, 용량 5)
      static class BoundedBuffer {
          private final int[] buffer = new int[5];
          private int count = 0;
          private int putIndex = 0;
          private int takeIndex = 0;
          
          public synchronized void offer(int item) throws InterruptedException {
              while (count == buffer.length) {
                  wait();
              }
              buffer[putIndex] = item;
              putIndex = (putIndex + 1) % buffer.length;
              count++;
              notifyAll();
          }
          
          public synchronized int take() throws InterruptedException {
              while (count == 0) {
                  wait();
              }
              int item = buffer[takeIndex];
              takeIndex = (takeIndex + 1) % buffer.length;
              count--;
              notifyAll();
              return item;
          }
      }
      
      public static void main(String[] args) {
          // TODO: 생산자 1, 소비자 2 스레드 생성
          BoundedBuffer buffer = new BoundedBuffer();
          
          Thread producer = new Thread(() -> {
              try {
                  for (int i = 1; i <= 10; i++) {
                      buffer.offer(i);
                      System.out.println("생산: " + i);
                      Thread.sleep(100);
                  }
              } catch (InterruptedException e) {}
          });
          
          Thread consumer1 = new Thread(() -> {
              try {
                  for (int i = 0; i < 5; i++) {
                      int item = buffer.take();
                      System.out.println("소비자1: " + item);
                      Thread.sleep(150);
                  }
              } catch (InterruptedException e) {}
          });
          
          Thread consumer2 = new Thread(() -> {
              try {
                  for (int i = 0; i < 5; i++) {
                      int item = buffer.take();
                      System.out.println("소비자2: " + item);
                      Thread.sleep(150);
                  }
              } catch (InterruptedException e) {}
          });
          
          producer.start();
          consumer1.start();
          consumer2.start();
      }
  }
  

5. 실전 체크리스트

  • 공유 상태 최소화: 공유 가변 상태를 최소화하고, 가능하면 불변 객체 또는 스레드 한정을 사용하세요.
    • 불변 객체: String, Integer 등 한 번 생성되면 변경되지 않는 객체
    • 스레드 한정: 각 스레드가 독립적인 데이터를 가지도록 설계
  • 동기화 범위 최소화: 꼭 필요한 최소 영역만 synchronized로 보호하세요.
    • 성능 향상: 동기화 범위가 작을수록 병렬성 증가
    • 데드락 방지: 락을 보유하는 시간을 최소화
  • 안전한 조건 대기: 조건 대기는 while 루프 + notifyAll() 패턴을 사용하세요.
    • if 대신 while 사용으로 spurious wakeup 방지
    • notifyAll() 사용으로 모든 대기 스레드에게 신호 전달
  • 디버깅 지원: 스레드 이름을 부여해 로깅 가독성을 확보하세요.
    • Thread.currentThread().setName("Worker-1") 또는 생성자에서 이름 설정
    • 로그에서 어떤 스레드에서 발생한 문제인지 쉽게 식별 가능
  • 올바른 스레드 시작: run() 직접 호출 금지. 항상 start()를 사용하세요.
    • run() 직접 호출: 동기 실행 (새 스레드 생성 안됨)
    • start() 호출: 비동기 실행 (새 스레드에서 실행)

6. 종합 문제

문제 1 - 기본 스레드 생성 (기초)

두 개의 스레드를 생성하여 각각 “Hello”와 “World”를 출력하세요.

public class BasicThreadDemo {
    public static void main(String[] args) throws Exception {
        // TODO: "Hello"를 출력하는 스레드 생성
        Thread t1 = new Thread(() -> {
            System.out.println("Hello");
        });
        
        // TODO: "World"를 출력하는 스레드 생성
        Thread t2 = new Thread(() -> {
            System.out.println("World");
        });
        
        // TODO: 두 스레드 시작
        // TODO: 두 스레드 완료 대기
    }
}
정답 보기

  public class BasicThreadDemo {
      public static void main(String[] args) throws Exception {
          // "Hello"를 출력하는 스레드 생성
          Thread t1 = new Thread(() -> {
              System.out.println("Hello");
          });
          
          // "World"를 출력하는 스레드 생성
          Thread t2 = new Thread(() -> {
              System.out.println("World");
          });
          
          // 두 스레드 시작
          t1.start();
          t2.start();
          
          // 두 스레드 완료 대기
          t1.join();
          t2.join();
          
          System.out.println("모든 스레드 완료!");
      }
  }
  

문제 2 - 스레드 이름 설정 (기초)

두 개의 스레드를 “Worker-1”, “Worker-2”로 이름을 설정하고 각각 1~5까지 숫자를 출력하세요.

public class NamedThreadDemo {
    public static void main(String[] args) throws Exception {
        // TODO: "Worker-1" 스레드 생성 (1~5 출력)
        Thread worker1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }, "Worker-1");
        
        // TODO: "Worker-2" 스레드 생성 (1~5 출력)
        Thread worker2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }, "Worker-2");
        
        // TODO: 두 스레드 시작 및 완료 대기
    }
}
정답 보기

  public class NamedThreadDemo {
      public static void main(String[] args) throws Exception {
          // "Worker-1" 스레드 생성 (1~5 출력)
          Thread worker1 = new Thread(() -> {
              for (int i = 1; i <= 5; i++) {
                  System.out.println(Thread.currentThread().getName() + ": " + i);
              }
          }, "Worker-1");
          
          // "Worker-2" 스레드 생성 (1~5 출력)
          Thread worker2 = new Thread(() -> {
              for (int i = 1; i <= 5; i++) {
                  System.out.println(Thread.currentThread().getName() + ": " + i);
              }
          }, "Worker-2");
          
          // 두 스레드 시작
          worker1.start();
          worker2.start();
          
          // 두 스레드 완료 대기
          worker1.join();
          worker2.join();
          
          System.out.println("모든 작업 완료!");
      }
  }
  

문제 3 - 간단한 동기화 (기초)

공유 변수 counter를 두 스레드가 각각 5번씩 증가시키는 프로그램을 만드세요. synchronized를 사용하여 동기화하세요.

public class SimpleSyncDemo {
    private static int counter = 0;
    
    public static void main(String[] args) throws Exception {
        // TODO: counter를 5번 증가시키는 스레드 1
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (SimpleSyncDemo.class) {
                    counter++;
                    System.out.println("스레드1: counter = " + counter);
                }
            }
        });
        
        // TODO: counter를 5번 증가시키는 스레드 2
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (SimpleSyncDemo.class) {
                    counter++;
                    System.out.println("스레드2: counter = " + counter);
                }
            }
        });
        
        // TODO: 두 스레드 시작 및 완료 대기
        // TODO: 최종 counter 값 출력
    }
}
정답 보기

  public class SimpleSyncDemo {
      private static int counter = 0;
      
      public static void main(String[] args) throws Exception {
          // counter를 5번 증가시키는 스레드 1
          Thread t1 = new Thread(() -> {
              for (int i = 0; i < 5; i++) {
                  synchronized (SimpleSyncDemo.class) {
                      counter++;
                      System.out.println("스레드1: counter = " + counter);
                  }
              }
          });
          
          // counter를 5번 증가시키는 스레드 2
          Thread t2 = new Thread(() -> {
              for (int i = 0; i < 5; i++) {
                  synchronized (SimpleSyncDemo.class) {
                      counter++;
                      System.out.println("스레드2: counter = " + counter);
                  }
              }
          });
          
          // 두 스레드 시작
          t1.start();
          t2.start();
          
          // 두 스레드 완료 대기
          t1.join();
          t2.join();
          
          // 최종 counter 값 출력
          System.out.println("최종 counter 값: " + counter);
      }
  }
  

문제 4 - Thread.sleep() 사용 (기초)

세 개의 스레드가 각각 다른 간격으로 “작업 중…“을 출력하는 프로그램을 만드세요. Thread.sleep()을 사용하세요.

public class SleepDemo {
    public static void main(String[] args) throws Exception {
        // TODO: 1초 간격으로 3번 출력하는 스레드
        Thread t1 = new Thread(() -> {
            try {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("스레드1: 작업 " + i);
                    Thread.sleep(1000); // 1초 대기
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        // TODO: 2초 간격으로 3번 출력하는 스레드
        Thread t2 = new Thread(() -> {
            try {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("스레드2: 작업 " + i);
                    Thread.sleep(2000); // 2초 대기
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        // TODO: 세 스레드 시작 및 완료 대기
    }
}
정답 보기

  public class SleepDemo {
      public static void main(String[] args) throws Exception {
          // 1초 간격으로 3번 출력하는 스레드
          Thread t1 = new Thread(() -> {
              try {
                  for (int i = 1; i <= 3; i++) {
                      System.out.println("스레드1: 작업 " + i);
                      Thread.sleep(1000); // 1초 대기
                  }
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          });
          
          // 2초 간격으로 3번 출력하는 스레드
          Thread t2 = new Thread(() -> {
              try {
                  for (int i = 1; i <= 3; i++) {
                      System.out.println("스레드2: 작업 " + i);
                      Thread.sleep(2000); // 2초 대기
                  }
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          });
          
          // 세 스레드 시작
          t1.start();
          t2.start();
          
          // 세 스레드 완료 대기
          t1.join();
          t2.join();
          
          System.out.println("모든 작업 완료!");
      }
  }
  

7. 자주 발생하는 오류와 해결

  • ConcurrentModificationException: 순회 중 컬렉션 수정. Iterator.remove() 사용.

  • IllegalMonitorStateException: wait/notify를 동기화 블록 밖에서 호출.

  • Deadlock: 상호 잠금. 락 획득 순서 통일, 타임아웃 락 사용 고려.

  • Visibility 문제: 스레드 간 가시성. volatile 또는 동기화로 해결.

부록: 익명 클래스 간단 예시

public class AnonymousDemo {
    interface Greeter { void greet(); }

    public static void main(String[] args) {
        Greeter g = new Greeter() {
            @Override
            public void greet() {
                System.out.println("Hello");
            }
        };
        g.greet();
    }
}