챕터 7: 동시성 프로그래밍
Java에서 동시성 프로그래밍은 멀티스레드 환경에서 효율적으로 작업을 수행할 수 있도록 지원하는 프로그래밍 기법입니다. 이를 통해 프로그램의 성능을 향상시키고, 복잡한 작업을 동시에 처리할 수 있습니다.
7.1 스레드와 Runnable 인터페이스
Java에서 스레드는 Thread 클래스나 Runnable 인터페이스를 사용하여 생성할 수 있습니다. 스레드는 동시에 실행되는 프로세스의 단위입니다.
7.1.1 스레드 생성과 실행
- Thread 클래스를 상속받아 스레드 생성
- Thread 클래스를 상속받고 run 메서드를 오버라이드하여 스레드를 정의합니다. 스레드의 실행은 start 메서드를 호출하여 시작됩니다.
public class MyThread extends Thread { @Override public void run() { // 스레드가 실행될 코드 System.out.println("Thread is running"); } public static void main(String[] args) { // MyThread 인스턴스를 생성하고 실행 MyThread thread = new MyThread(); thread.start(); // 스레드 실행 } }
- Thread 클래스를 상속받고 run 메서드를 오버라이드하여 스레드를 정의합니다. 스레드의 실행은 start 메서드를 호출하여 시작됩니다.
- Runnable 인터페이스를 구현하여 스레드 생성
- Runnable 인터페이스를 구현하고 run 메서드를 정의한 후, 이를 Thread 객체의 생성자에 전달하여 스레드를 생성합니다.
public class MyRunnable implements Runnable { @Override public void run() { // 스레드가 실행될 코드 System.out.println("Runnable is running"); } public static void main(String[] args) { // Runnable을 구현한 객체를 Thread에 전달하여 스레드 실행 Thread thread = new Thread(new MyRunnable()); thread.start(); // 스레드 실행 } }
- Runnable 인터페이스를 구현하고 run 메서드를 정의한 후, 이를 Thread 객체의 생성자에 전달하여 스레드를 생성합니다.
설명: Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법 두 가지가 있습니다. Runnable 인터페이스를 구현하는 방법이 더 선호되는 이유는 다른 클래스를 상속받을 수 있다는 유연성을 제공하기 때문입니다.
7.2 동기화와 Lock
멀티스레드 환경에서 데이터 일관성을 유지하기 위해 동기화가 필요합니다. 동기화는 특정 코드 블록이나 메서드에 동시에 하나의 스레드만 접근하도록 제한하는 것을 의미합니다. Java는 synchronized 키워드와 Lock 인터페이스를 통해 동기화를 지원합니다.
7.2.1 동기화 블록과 메서드
- synchronized 메서드
- 메서드 전체를 동기화하여 동시 접근을 제한합니다.
public class Counter { private int count = 0; // synchronized 메서드로 동기화 public synchronized void increment() { count++; } public synchronized int getCount() { return count; } public static void main(String[] args) { Counter counter = new Counter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + counter.getCount()); // 출력: Final count: 2000 } }
- 메서드 전체를 동기화하여 동시 접근을 제한합니다.
- synchronized 블록
- 코드 블록을 동기화하여 동시 접근을 제한합니다. 필요한 부분만 동기화하여 성능을 향상시킬 수 있습니다.
public class Counter { private int count = 0; private final Object lock = new Object(); public void increment() { // synchronized 블록으로 동기화 synchronized (lock) { count++; } } public int getCount() { synchronized (lock) { return count; } } public static void main(String[] args) { Counter counter = new Counter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + counter.getCount()); // 출력: Final count: 2000 } }
- 코드 블록을 동기화하여 동시 접근을 제한합니다. 필요한 부분만 동기화하여 성능을 향상시킬 수 있습니다.
설명: synchronized 키워드는 임계 구역(critical section)을 정의하여 하나의 스레드만 접근할 수 있도록 합니다.
synchronized 메서드는 메서드 전체를 동기화하지만, synchronized 블록은 필요한 부분만 동기화할 수 있어 효율적입니다.
7.2.2 Lock 인터페이스와 그 활용
Java의 java.util.concurrent.locks 패키지는 동기화를 제어하는 다양한 락을 제공합니다. ReentrantLock은 가장 많이 사용되는 락 중 하나로, 재진입 가능한 락(Reentrant Lock)을 지원합니다.
- ReentrantLock 사용 예제
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Counter { private int count = 0; private final 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(); } } public static void main(String[] args) { Counter counter = new Counter(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + counter.getCount()); // 출력: Final count: 2000 } }
설명: ReentrantLock은 synchronized 블록보다 더 많은 기능을 제공합니다.
예를 들어, 락을 시도하는 동안 타임아웃을 설정하거나, 락을 비동기적으로 획득하려고 할 수 있습니다.
lock() 메서드를 호출하여 락을 획득하고, unlock() 메서드를 호출하여 락을 해제합니다.
try-finally 블록을 사용하여 락이 항상 해제되도록 보장합니다.
7.3 고급 동시성 도구
Java는 고급 동시성 프로그래밍을 지원하기 위해 다양한 도구와 프레임워크를 제공합니다.
7.3.1 Executors 프레임워크
Executors 프레임워크는 스레드 풀을 관리하고, 스레드 생성을 간단하게 합니다. 스레드 풀은 스레드를 재사용하여 성능을 최적화하는 데 유용합니다. 주요 클래스는 ExecutorService와 Executors입니다.
- ExecutorService 사용 예제
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) { // 고정된 스레드 풀 생성 ExecutorService executor = Executors.newFixedThreadPool(2); // 작업 제출 executor.submit(() -> { System.out.println("Task 1 executed by " + Thread.currentThread().getName()); }); executor.submit(() -> { System.out.println("Task 2 executed by " + Thread.currentThread().getName()); }); // ExecutorService 종료 executor.shutdown(); } }
설명: ExecutorService는 스레드 풀을 관리하는 인터페이스입니다.
Executors 클래스는 ExecutorService 인스턴스를 생성하기 위한 정적 팩토리 메서드를 제공합니다. newFixedThreadPool(int n) 메서드는 고정된 수의 스레드를 가진 스레드 풀을 생성합니다.
submit() 메서드는 실행할 작업을 제출합니다.
7.3.2 Concurrent 컬렉션
java.util.concurrent 패키지는 스레드에 안전한 컬렉션을 제공합니다. 주요 클래스는 ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue 등이 있습니다.
- ConcurrentHashMap 사용 예제
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public class Main { public static void main(String[] args) { ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("one", 1); map.put("two", 2); // ConcurrentMap을 사용하는 작업 Runnable task = () -> { map.put("three", 3); System.out.println("Value for 'three': " + map.get("three")); }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } map.forEach((key, value) -> System.out.println(key + ": " + value)); // 출력: // one: 1 // two: 2 // three: 3 } }
설명: ConcurrentHashMap은 스레드에 안전한 해시맵 구현체로, 동시성 문제를 피하기 위해 내부적으로 세그먼트를 사용합니다. 이로 인해 높은 동시성을 유지하면서도 동기화 오버헤드를 줄일 수 있습니다.
- BlockingQueue 사용 예제
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class Main { public static void main(String[] args) { // 크기가 10인 BlockingQueue 생성 BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); // 생산자 스레드 Runnable producer = () -> { try { queue.put("Message 1"); System.out.println("Produced: Message 1"); queue.put("Message 2"); System.out.println("Produced: Message 2"); } catch (InterruptedException e) { e.printStackTrace(); } }; // 소비자 스레드 Runnable consumer = () -> { try { String message; while ((message = queue.take()) != null) { System.out.println("Consumed: " + message); } } catch (InterruptedException e) { e.printStackTrace(); } }; Thread producerThread = new Thread(producer); Thread consumerThread = new Thread(consumer); producerThread.start(); consumerThread.start(); try { producerThread.join(); consumerThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
설명: BlockingQueue는 스레드에 안전한 큐로, 생산자-소비자 패턴을 구현할 때 유용합니다.
put() 메서드는 큐에 요소를 추가하고, 큐가 가득 차면 공간이 생길 때까지 대기합니다.
take() 메서드는 큐에서 요소를 제거하고, 큐가 비어 있으면 요소가 추가될 때까지 대기합니다.
이로써 Java의 동시성 프로그래밍에 대해 자세히 설명하고, 각 개념을 코드와 예시를 통해 설명했습니다. 다음 챕터에서는 Java의 스트림과 컬렉션 프레임워크를 다루겠습니다.
'IT 강좌(IT Lectures) > Java' 카테고리의 다른 글
9강. 파일 I/O 및 네트워킹 (0) | 2024.06.26 |
---|---|
8강. 스트림과 컬렉션 프레임워크 (0) | 2024.06.25 |
6강. 고급 객체 지향 기법 (0) | 2024.06.23 |
5강. Java API 활용 (0) | 2024.06.22 |
4강. 기본 클래스 사용 (0) | 2024.06.21 |