• Feed
  • Explore
  • Ranking
/

    [TIL] 자바 동시성. 스레드 실습

    박
    박상준
    2024.12.18
    ·
    8 min read

    1. 스레드 생성 방법

    1-1. Runnable 인터페이스 사용

    방법

    • Runnable 인터페이스를 구현한 클래스를 생성한다

    • 해당 객체를 Thread 의 생성자에 전달함

    public class Thread implements Runnable { // Runnable 구현
    
    //생성자에 전달됨.
    public Thread(Runnable task) {
       this(null, null, 0, task, 0, null);
    }
    

    1-2. Thread 클래스를 확장

    • 방법

      • Thread 클래스를 확장한 새로운 클래스를 생성한다

    • 특징

      • Thread 가 이미 Runnable 클래스를 구현하고 있음.

      • run만 오버라이딩하면된다.

    실습 : 금고 해킹 시뮬레이션

    시나리오

    1. 금고 설계

      • 임의 비밀번호가 설정된 금고 있음

    2. 목표

      1. 해커가 금고 비밀번호를 맞추기 전에 경찰이 잡으러 도착한다

    3. 스레드 역할

      1. 해커 스레드

        • 금고 비밀번호를 추측한다

      2. 경찰 스레드

        • 10초 후 도착하여 해커를 체포

    public class Main {
        public static final int MAX_PASSWORD = 9999;
    
        public static void main(String[] args) {
            Random random = new Random();
    
            Vault vault = new Vault(random.nextInt(MAX_PASSWORD));
    
            List<Thread> threads = List.of(
                    new AscendingHackerThread(vault),
                    new DescendingHackerThread(vault),
                    new PoliceThread()
            );
    
            threads.forEach(Thread::start);
        }
    
    // 금고 클래스
    // 비밀번호 맞는지 체크
    // 해커의 속도 늦추기 위해 5ms 지연
        private static class Vault {
            private int password;
    
            public Vault(int password) {
                this.password = password;
            }
    
            public boolean isCorrectPassword(int guess) {
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                return this.password == guess;
            }
        }
    
    //해커 스레드 추상체
    // Vault 라는 금고 객체가 있으며
    // 해당 스레드 객체의 우선순위는 TOP 이다.
        private static abstract class HackerThread extends Thread {
            protected Vault vault;
    
            public HackerThread(Vault vault) {
                this.vault = vault;
                this.setName(this.getClass().getSimpleName());
                this.setPriority(Thread.MAX_PRIORITY);
            }
    
            @Override
            public void start() {
                System.out.println("Starting thread: " + this.getName());
                super.start();
            }
        }
    
    //구현체 1 0 부터 9999까지 맞추기
        private static class AscendingHackerThread extends HackerThread {
            public AscendingHackerThread(Vault vault) {
                super(vault);
            }
    
            @Override
            public void run() {
                for (int guess = 0; guess < MAX_PASSWORD; guess++) {
                    if (vault.isCorrectPassword(guess)) {
                        System.out.println(this.getName() + " guessed the password: " + guess);
                        System.exit(0);
                    }
                }
            }
        }
    
    // 구현체 2 9999 부터 0까지 맞추기
        private static class DescendingHackerThread extends HackerThread {
            public DescendingHackerThread(Vault vault) {
                super(vault);
            }
    
            @Override
            public void run() {
                for (int guess = MAX_PASSWORD; guess >= 0; guess--) {
                    if (vault.isCorrectPassword(guess)) {
                        System.out.println(this.getName() + " guessed the password: " + guess);
                        System.exit(0);
                    }
                }
            }
        }
    
    // 경찰 스레드
    // 총 10의 숫자를 센다. ( 10초 )
        private static class PoliceThread extends Thread {
            @Override
            public void run() {
                for (int i = 10; i > 0; i--) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(i);
                }
    
                System.out.println("Game over for you hackers");
                System.exit(0);
            }
        }
    }
    

    실습 : 코드1

    코딩 연습 1: 스레드 생성 - MultiExecutor

    이번 연습에서는 MultiExecutor를 구현해 보겠습니다.

    이 클래스의 클라이언트는 Runnable 작업의 목록을 생성해서 해당 목록을 MultiExecutor의 생성자에게 제공할 것입니다.

    클라이언트가 executeAll()을 실행하면, MultiExecutor가 주어진 모든 작업을 실행하게 됩니다.

    멀티코어 CPU를 최대한 활용하기 위해, 우리는 각 작업을 서로 다른 스레드로 전달해서 MultiExecutor가 모든 작업을 동시에 진행하게 하려고 합니다.

    import java.util.ArrayList;
    import java.util.List;
    
    public class MultiExecutor {
    
        private final List<Runnable> tasks;
    
        /*
         * @param tasks to executed concurrently
         */
        public MultiExecutor(List<Runnable> tasks) {
            this.tasks = tasks;
        }
    
        /**
         * Executes all the tasks concurrently
         */
        public void executeAll() {
            List<Thread> threads = new ArrayList<>(tasks.size());
    
            for (Runnable task : tasks) {
                Thread thread = new Thread(task);
                threads.add(thread);
            }
    
            for(Thread thread : threads) {
                thread.start();
            }
        }
    }
    

    스레드 종료 및 데몬 스레드

    스레드 조정 필요성

    1. 스레드 리소스 정리

      • 스레드는 실행 중이 아니더라도 메모리, CPU 캐시, 커널 리소스를 사용한다.

      • 작업 완료 후에 종료되지 않은 스레드는 시스템 리로스를 낭비함

    2. 오작동 방지

      • 스레드가 무한 루프, 긴 계산, 잘못된 요청 등을 실행하는 경우 중단이 필요하다

    3. 어플 종료

      • 실행 중인 스레드가 하나라도 있으면 어플 종료 안됨.

      • 모든 스레드 중단해야함

    스레드 중단 법

    1. Thread.interrupt

      public class Main {
          public static void main(String[] args) {
              Thread thread = new Thread(new BlockingTask());
              thread.start();
      
              thread.interrupt();
          }
      
          private static class BlockingTask implements Runnable {
              @Override
              public void run() {
                  try {
                      Thread.sleep(500000);
                  } catch (InterruptedException e) {
                      System.out.println("Exiting blocking thread");
                  }
              }
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              Thread thread = new Thread(new LongComputationTask(new BigInteger("2"), new BigInteger("10")));
      
              thread.start();
      
              thread.interrupt();
          }
      
          private static class LongComputationTask implements Runnable {
              private final BigInteger base;
              private final BigInteger power;
      
              public LongComputationTask(BigInteger base, BigInteger power) {
                  this.base = base;
                  this.power = power;
              }
      
              @Override
              public void run() {
                  System.out.println(base + "^" + power + " = " + pow(base, power));
              }
      
              private BigInteger pow(BigInteger base, BigInteger power) {
                  // 0
                  BigInteger result = BigInteger.ONE;
      
                  //의미 : power만큼 반복
                  for (BigInteger i = BigInteger.ZERO; i.compareTo(power) != 0; i = i.add(BigInteger.ONE)) {
                      result = result.multiply(base);
                  }
      
                  return result;
              }
          }
      }
      
    • 위와 같은 코드에서 2 ^ 10은 얼마 안걸림

      • 근데 20000 ^ 100000 같은 경우 겁나 오래걸림

    • thread.interrupt(); 를 수행해도

      • interrupt 를 받는 부분이 없어서 무한 루프에 걸리는 건 여전함

    • 그래서 for문 중간에

                  //의미 : power만큼 반복
                  for (BigInteger i = BigInteger.ZERO; i.compareTo(power) != 0; i = i.add(BigInteger.ONE)) {
                      // 스레드가 interrupt되었는지 확인
                      if (Thread.currentThread().isInterrupted()) {
                          System.out.println("Prematurely interrupted computation");
                          return BigInteger.ZERO;
                      }
      
                      result = result.multiply(base);
                  }
      
      • 해당 내용을 추가해야한다.

    데몬 스레드

    • 긴 계산 작업을 수행하는 스레드를 Daemon 으로 설정한다

    • 메인 스레드가 종료되는 경우 Daemon 스레드도 자동으로 종료된다.

    public class Main {
        public static void main(String[] args) {
            Thread thread = new Thread(new LongComputationTask(new BigInteger("20000"), new BigInteger("100000")));
            
            //데몬 설정으로 -> 메인 스레드 종료시 같이 스레드가 종료되게 설정
            thread.setDaemon(true);
            thread.start();
            
            //interrupt 가 수행되는 경우 메인 스레드가 종료되게 되는데
            thread.interrupt();
            //메인 스레드 종료와 함께 새로운 데몬 스레드도 같이 종료처리.
        }
        
        private static class LongComputationTask implements Runnable {
            private final BigInteger base;
            private final BigInteger power;
            
            public LongComputationTask(BigInteger base, BigInteger power) {
                this.base = base;
                this.power = power;
            }
            
            @Override
            public void run() {
                System.out.println(base + "^" + power + " = " + pow(base, power));
            }
            
            private BigInteger pow(BigInteger base, BigInteger power) {
                // 0
                BigInteger result = BigInteger.ONE;
                
                //의미 : power만큼 반복
                for (BigInteger i = BigInteger.ZERO; i.compareTo(power) != 0; i = i.add(BigInteger.ONE)) {
                    result = result.multiply(base);
                }
                
                return result;
            }
        }
    }