[프래피티]

자바의 스레드란

everydeveloper 2024. 5. 19. 04:01

자바에서 스레드란?

스레드는 프로세스 내에서 실행되는 가장 작은 단위의 작업입니다. 자바에서 스레드는 동시에 여러 작업을 수행할 수 있도록 도와줍니다. 자바는 java.lang.Thread 클래스와 java.lang.Runnable 인터페이스를 통해 스레드를 생성하고 관리할 수 있습니다.

멀티스레딩을 사용해야 하는 이유

멀티스레딩은 여러 스레드가 동시에 실행되는 것을 의미합니다. 멀티스레딩을 사용해야 하는 주요 이유는 다음과 같습니다:

  1. 성능 향상: 멀티스레딩을 사용하면 여러 작업을 동시에 수행할 수 있어 프로그램의 성능이 향상됩니다. 특히 다중 코어 프로세서에서는 각 코어가 독립적으로 스레드를 실행할 수 있어 더 높은 성능을 발휘할 수 있습니다.
  2. 자원 공유: 여러 스레드가 동일한 메모리 공간을 공유하므로 자원 낭비를 줄일 수 있습니다. 예를 들어, 하나의 데이터베이스 연결을 여러 스레드가 공유하여 사용할 수 있습니다.
  3. 응답성 향상: 사용자 인터페이스(UI) 프로그램에서 멀티스레딩을 사용하면, 긴 작업이 백그라운드에서 실행되는 동안에도 사용자 인터페이스가 응답성을 유지할 수 있습니다.
  4. 단순화된 모델링: 복잡한 문제를 더 작은 작업으로 나누어 각각의 스레드에서 실행하면 프로그램의 구조를 단순화할 수 있습니다.

자바에서 멀티스레딩 구현 방법

자바에서 멀티스레딩을 구현하는 방법에는 크게 두 가지가 있습니다:

  1. Thread 클래스 상속:
    • Thread 클래스를 상속받아 새로운 클래스를 만들고, run 메서드를 재정의하여 작업을 수행합니다.
    • start 메서드를 호출하여 스레드를 시작합니다.
    class MyThread extends Thread {
        public void run() {
            // 작업 내용
            System.out.println("Thread is running");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            t1.start(); // 스레드 시작
        }
    }
    
    
  2. Runnable 인터페이스 구현:
    • Runnable 인터페이스를 구현한 클래스를 만들고, run 메서드를 재정의하여 작업을 수행합니다.
    • Thread 클래스의 인스턴스를 생성할 때 Runnable 객체를 전달하여 스레드를 시작합니다.
    class MyRunnable implements Runnable {
        public void run() {
            // 작업 내용
            System.out.println("Runnable is running");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread t1 = new Thread(myRunnable);
            t1.start(); // 스레드 시작
        }
    }
    
    

멀티스레딩 사용 시 고려사항

  1. 동기화: 여러 스레드가 동시에 접근하는 자원에 대한 동기화가 필요합니다. 자바에서는 synchronized 키워드를 사용하여 임계 영역을 설정할 수 있습니다.
  2. class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
  3. 스레드 풀: 많은 스레드를 생성하고 관리하는 것은 오버헤드가 크므로, ExecutorService를 사용하여 스레드 풀을 관리할 수 있습니다.
  4. import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executor.execute(new MyRunnable()); } executor.shutdown(); } }
  5. 예외 처리: 스레드 내에서 발생하는 예외는 개별적으로 처리해야 합니다. 각 스레드의 run 메서드 내에서 적절한 예외 처리를 구현해야 합니다.
  6. class MyRunnable implements Runnable { public void run() { try { // 작업 내용 } catch (Exception e) { e.printStackTrace(); } } }

이렇게 자바에서 스레드를 사용하고, 멀티스레딩을 구현하는 방법에 대해 설명드렸습니다. 스레드를 적절히 활용하면 프로그램의 성능을 크게 향상시킬 수 있지만, 동기화와 자원 관리를 철저히 하여야 합니다.

자바에서 스레드 간 통신을 위해 사용되는 메커니즘과 그 예시는 무엇이 있을까요? 자바에서 제공하는 동기화 방법 중 어떤 것들이 있고 각각 어떤 용도로 사용되는지 알아보세요.

자바에서 스레드 간 통신을 위해 사용되는 메커니즘과 동기화 방법에 대해 설명드리겠습니다.

자바에서 스레드 간 통신 메커니즘

  1. wait(), notify(), notifyAll() 메서드
    • Object 클래스에서 제공하는 이 메서드들은 모니터를 사용하여 스레드 간 통신을 가능하게 합니다.
    • wait() 메서드는 현재 스레드를 대기 상태로 만들고, 다른 스레드가 notify() 또는 notifyAll() 메서드를 호출할 때까지 기다리게 합니다.
    • notify() 메서드는 대기 상태에 있는 하나의 스레드를 깨웁니다.
    • notifyAll() 메서드는 대기 상태에 있는 모든 스레드를 깨웁니다.
    class SharedResource {
        private boolean available = false;
    
        public synchronized void produce() throws InterruptedException {
            while (available) {
                wait();
            }
            // 생산 작업
            available = true;
            notifyAll();
        }
    
        public synchronized void consume() throws InterruptedException {
            while (!available) {
                wait();
            }
            // 소비 작업
            available = false;
            notifyAll();
        }
    }
    
    
  2. BlockingQueue
    • java.util.concurrent 패키지에서 제공하는 BlockingQueue 인터페이스는 스레드 간에 안전하게 데이터를 주고받을 수 있게 해줍니다.
    • put() 메서드는 큐가 가득 찬 경우 대기하고, take() 메서드는 큐가 비어 있는 경우 대기합니다.
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    
    class Producer implements Runnable {
        private BlockingQueue<Integer> queue;
    
        public Producer(BlockingQueue<Integer> queue) {
            this.queue = queue;
        }
    
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    class Consumer implements Runnable {
        private BlockingQueue<Integer> queue;
    
        public Consumer(BlockingQueue<Integer> queue) {
            this.queue = queue;
        }
    
        public void run() {
            try {
                while (true) {
                    Integer item = queue.take();
                    System.out.println("Consumed: " + item);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
            Thread producer = new Thread(new Producer(queue));
            Thread consumer = new Thread(new Consumer(queue));
    
            producer.start();
            consumer.start();
        }
    }
    
    

자바에서 제공하는 동기화 방법

  1. synchronized 키워드
    • 특정 메서드나 블록에 대한 동기화를 제공합니다.
    • 메서드: synchronized 메서드를 호출하면, 해당 메서드에 대해 하나의 스레드만 접근할 수 있습니다.
    • 블록: 특정 객체에 대해 동기화 블록을 설정할 수 있습니다.
    class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    
        public synchronized int getCount() {
            return count;
        }
    }
    
    class Counter {
        private int count = 0;
        private final Object lock = new Object();
    
        public void increment() {
            synchronized (lock) {
                count++;
            }
        }
    
        public int getCount() {
            synchronized (lock) {
                return count;
            }
        }
    }
    
    
  2. Lock and ReentrantLock
    • java.util.concurrent.locks 패키지에서 제공하는 Lock 인터페이스와 그 구현체 ReentrantLock은 좀 더 유연한 동기화 메커니즘을 제공합니다.
    • lock()과 unlock() 메서드를 사용하여 명시적으로 락을 획득하고 해제할 수 있습니다.
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class Counter {
        private int count = 0;
        private Lock lock = new ReentrantLock();
    
        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    
        public int getCount() {
            lock.lock();
            try {
                return count;
            } finally {
                lock.unlock();
            }
        }
    }
    
    
  3. ReadWriteLock
    • java.util.concurrent.locks 패키지에서 제공하는 ReadWriteLock 인터페이스와 그 구현체 ReentrantReadWriteLock은 읽기와 쓰기 작업을 구분하여 동기화할 수 있습니다.
    • 여러 스레드가 동시에 읽을 수 있지만, 쓰기 작업 중에는 읽기와 쓰기가 모두 제한됩니다.
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    class DataStructure {
        private int data = 0;
        private ReadWriteLock lock = new ReentrantReadWriteLock();
    
        public void write(int value) {
            lock.writeLock().lock();
            try {
                data = value;
            } finally {
                lock.writeLock().unlock();
            }
        }
    
        public int read() {
            lock.readLock().lock();
            try {
                return data;
            } finally {
                lock.readLock().unlock();
            }
        }
    }
    
    
  4. Semaphore
    • java.util.concurrent 패키지에서 제공하는 Semaphore 클래스는 지정된 수의 스레드만 동시에 접근할 수 있게 제어합니다.
    • acquire() 메서드는 허가를 얻고, release() 메서드는 허가를 반환합니다.
    import java.util.concurrent.Semaphore;
    
    class SharedResource {
        private Semaphore semaphore = new Semaphore(3); // 동시에 3개의 스레드만 접근 가능
    
        public void access() {
            try {
                semaphore.acquire();
                // 자원에 접근하는 작업
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                semaphore.release();
            }
        }
    }
    
    

이처럼 자바는 다양한 동기화 방법과 스레드 간 통신 메커니즘을 제공하여 멀티스레딩 환경에서의 안정성과 효율성을 높일 수 있습니다. 각 방법을 적절히 사용하여 스레드 간의 자원 공유와 통신을 효과적으로 관리할 수 있습니다.

자바에서 wait(), notify(), notifyAll() 메서드를 사용할 때 발생할 수 있는 Race Condition에 대해 설명해보세요. 또한, 자바에서 제공하는 ReentrantLock과 ReadWriteLock의 차이점은 무엇인가요?

wait(), notify(), notifyAll() 메서드와 Race Condition

Race Condition이란?

Race Condition은 여러 스레드가 공유 자원에 접근할 때 발생할 수 있는 문제로, 스레드 간의 실행 순서에 따라 결과가 달라질 수 있는 상황을 말합니다. 이는 동기화가 제대로 이루어지지 않거나, 예상하지 못한 시점에 스레드 간의 간섭이 발생할 때 주로 발생합니다.

wait(), notify(), notifyAll()에서 발생할 수 있는 Race Condition

  1. Spurious Wakeups: wait() 메서드는 'Spurious Wakeups'(예상치 못한 깨어남)이 발생할 수 있기 때문에 항상 while 루프 안에서 호출해야 합니다. 이렇게 하지 않으면, 잘못된 시점에 스레드가 깨어나게 되어 Race Condition이 발생할 수 있습니다.
  2. synchronized (lock) { while (!condition) { lock.wait(); } // 작업 수행 }
  3. Missed Signals: notify()나 notifyAll()이 wait()를 호출하기 전에 호출되면, 신호가 사라져서 대기 중인 스레드가 신호를 받지 못하는 문제가 발생할 수 있습니다. 이를 방지하려면 notify() 또는 notifyAll()을 호출하기 전에 항상 모든 스레드가 wait() 상태에 들어가도록 보장해야 합니다.이 경우 notify()를 호출하기 전에 모든 스레드가 이미 wait() 상태에 있어야 합니다.
  4. synchronized (lock) { lock.notifyAll(); }
  5. Thread Interference: wait()와 notify() 메서드를 사용할 때 여러 스레드가 동시에 공유 자원에 접근할 경우, 동기화가 제대로 이루어지지 않으면 예상하지 못한 결과를 초래할 수 있습니다. 따라서, 항상 올바르게 동기화된 블록 안에서 wait()와 notify()를 호출해야 합니다.
  6. synchronized (lock) { while (!condition) { lock.wait(); } // 작업 수행 lock.notifyAll(); }

ReentrantLock과 ReadWriteLock의 차이점

ReentrantLock

  • ReentrantLock은 Lock 인터페이스를 구현한 클래스 중 하나로, 스레드가 동일한 락을 여러 번 획득할 수 있는 기능을 제공합니다. 이는 synchronized 블록보다 더 세밀한 락 제어를 가능하게 합니다.
  • ReentrantLock은 공정성(fairness) 정책을 설정할 수 있어, 대기 중인 스레드가 공정하게 락을 획득할 수 있도록 합니다.
  • 주요 메서드:
    • lock(): 락을 획득합니다.
    • unlock(): 락을 해제합니다.
    • tryLock(): 락을 시도합니다(즉시 반환).
    • lockInterruptibly(): 인터럽트 가능한 락 획득을 시도합니다.
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockExample {
        private final Lock lock = new ReentrantLock();
    
        public void performTask() {
            lock.lock();
            try {
                // 작업 수행
            } finally {
                lock.unlock();
            }
        }
    }
    
    

ReadWriteLock

  • ReadWriteLock은 읽기 및 쓰기 작업을 구분하여 동기화하는 데 사용됩니다. 이 인터페이스는 ReentrantReadWriteLock 클래스에 의해 구현됩니다.
  • 여러 스레드가 동시에 읽기를 수행할 수 있지만, 쓰기 작업이 있을 때는 읽기와 쓰기 모두 제한됩니다. 이는 읽기 작업이 빈번하고 쓰기 작업이 적은 경우 성능 향상을 가져올 수 있습니다.
  • 주요 메서드:
    • readLock(): 읽기 락을 획득합니다.
    • writeLock(): 쓰기 락을 획득합니다.
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLockExample {
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        private int value;
    
        public int read() {
            lock.readLock().lock();
            try {
                return value;
            } finally {
                lock.readLock().unlock();
            }
        }
    
        public void write(int value) {
            lock.writeLock().lock();
            try {
                this.value = value;
            } finally {
                lock.writeLock().unlock();
            }
        }
    }
    
    

요약

  • Race Condition: wait(), notify(), notifyAll() 사용 시 발생할 수 있으며, while 루프 내에서 wait()을 호출하고, 올바르게 동기화된 블록에서 notify() 및 notifyAll()을 호출하여 방지할 수 있습니다.
  • ReentrantLock: 동일한 스레드가 여러 번 락을 획득할 수 있으며, 공정성 정책 설정이 가능합니다. 세밀한 락 제어가 필요할 때 사용합니다.
  • ReadWriteLock: 읽기와 쓰기 작업을 구분하여 동기화할 수 있으며, 읽기 작업이 빈번한 경우 성능 향상을 가져올 수 있습니다.

이와 같이 자바의 스레드 동기화 및 통신 메커니즘을 이해하고 적절히 활용하면, 멀티스레드 환경에서 안정적이고 효율적인 프로그램을 작성할 수 있습니다.

'[프래피티]' 카테고리의 다른 글

자바의 NIO  (0) 2024.05.20
Blocking IO / Non-Blocking IO  (0) 2024.05.20
자바의 제네릭  (0) 2024.05.18
StringBuilder와 StringBuffer  (0) 2024.05.18
자바에서 자주 쓰는 자료구조 2  (0) 2024.05.17