Java 멀티스레딩과 동시성(Concurrency) 프로그래밍 심화과정에 대해 알아보기

Java는 멀티스레딩(Multi-threading)과 동시성(Concurrency) 프로그래밍을 지원합니다. 그리고 대규모 애플리케이션의 성능을 향상 시킬 수 있습니다. 멀티스레딩을 활용하면 여러 작업을 병렬로 수행하여 응답성을 개선하고, CPU 사용률을 높일 수 있습니다.

이번 포스팅에서는 Java의 멀티스레딩 기법과 동시성 프로그래밍의 핵심 개념, 그리고 실무에서 활용할 수 있는 최적화 기법을 알려 드리겠습니다.


1. 멀티스레딩과 동시성의 차이

  • 멀티스레딩(Multi-threading): 하나의 프로세스 내에서 여러 개의 Thread가 실행되며, 각 Thred는 독립적으로 작업을 수행합니다.
  • 동시성(Concurrency): 여러 작업이 동시에 실행되는 개념으로, 멀티코어 CPU 환경에서 효율적인 자원 활용을 목표로 합니다.

Java에서는 Thread 클래스를 상속하거나 Runnable 인터페이스를 구현하여 멀티스레딩을 구현이 가능합니다.


2. Java에서의 멀티스레딩 구현

1) Thread 클래스 상속

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running");
        }
    }
}

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

2) Runnable 인터페이스 구현

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running");
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}

Runnable을 사용하면 다중 상속 문제를 피할 수 있으며, 더 유연한 코드 구성도 가능하며, 활용도가 높습니다.


3. ExecutorService를 이용한 Thread 관리

Java에서는 ExecutorService를 사용하여 Thread를 효율적으로 관리할 수 있습니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing");
            });
        }

        executor.shutdown();
    }
}
  • newFixedThreadPool(n): 고정된 개수의 Thread 풀을 생성 합니다.
  • newCachedThreadPool(): 필요에 따라 동적으로 Thread를 생성합니다.
  • newSingleThreadExecutor(): 단일 Thread를 사용하여 순차적으로 작업을 처리를 합니다.

4. 동기화(Synchronization)와 경쟁 상태 해결

멀티스레딩 환경에서는 여러 Thread가 공유 자원에 동시에 접근하면 경쟁 상태(Race Condition) 문제가 발생할 수 있으며, 이를 해결하기 위해 동기화 기법을 사용합니다.

1) synchronized 키워드 사용

class SharedResource {
    synchronized void printMessage(String message) {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + message);
        }
    }
}

public class SyncExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        
        Thread t1 = new Thread(() -> resource.printMessage("Hello"));
        Thread t2 = new Thread(() -> resource.printMessage("World"));
        
        t1.start();
        t2.start();
    }
}

2) ReentrantLock 사용

synchronized의 단점을 보완하기 위해 ReentrantLock을 사용할 수 있습니다.

import java.util.concurrent.locks.ReentrantLock;

class SharedResource {
    private final ReentrantLock lock = new ReentrantLock();
    
    void printMessage(String message) {
        lock.lock();
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + message);
            }
        } finally {
            lock.unlock();
        }
    }
}

5. 동시성 프로그래밍을 위한 고급 기법

1) CompletableFuture 활용

비동기 작업을 쉽게 관리할 수 있도록 CompletableFuture를 사용할 수 있습니다.

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> "Hello")
                .thenApply(s -> s + " World")
                .thenAccept(System.out::println);
    }
}

2) Atomic 변수 활용 (CAS 알고리즘)

멀티스레드 환경에서 synchronized 없이 동기화를 수행할 수 있습니다.

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private static final AtomicInteger counter = new AtomicInteger(0);
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(counter.incrementAndGet());
            }).start();
        }
    }
}

6. 멀티스레딩 및 동시성 프로그래밍의 모범 사례

  1. 공유 자원 최소화: 불필요한 공유 상태를 줄이고 ThreadLocal을 활용할 수 있습니다.
  2. 스레드 풀 활용: ExecutorService를 사용하여 스레드 생성 비용 절감할 수 있습니다.
  3. 동기화 최소화: synchronized 대신 ConcurrentHashMap, Atomic 클래스를 활용할 수 있습니다.
  4. 데드락(Deadlock) 방지: 락 순서를 정하고 tryLock()을 활용하여 교착 상태 방지할 수 있습니다.

결론

Java의 멀티스레딩과 동시성 프로그래밍은 성능을 극대화할 수 있는 강력한 Tool입니다. Thread, ExecutorService, CompletableFuture 등을 활용하게 된다면 효율적인 병렬 처리를 구현할 수 있습니다. 최적화된 멀티스레드 애플리케이션을 한번 Test 해보시길 바랍니다. 감사합니다.

 

Leave a Comment