Programming Language/Java / / 2025. 3. 5. 15:35

동시성 (Concurrency)

자바 동시성은 현대 프로그래밍에서 매우 중요한 개념입니다. 멀티 코어 프로세서의 등장으로 인해, 여러 작업을 동시에 효율적으로 처리하는 능력은 애플리케이션의 성능과 반응성을 극대화하는 데 필수적입니다.


이 글에서는 자바 동시성의 핵심 개념부터 심화 주제까지 자세히 다루며, 분석적인 시각과 함께 심층적인 이해를 돕고자 합니다.

 


1. 자바 동시성 개요

 

1.1 동시성이란 무엇인가?

 

동시성(Concurrency)은 프로그램의 여러 부분이 독립적으로, 그러나 동시에 진행되는 것처럼 보이게 만드는 능력을 의미합니다.


실제로는 시간 분할 (Time-Slicing) 방식 또는 병렬 실행 (Parallel Execution) 방식을 통해 처리될 수 있습니다.

 

동시성의 핵심 목표는 다음과 같습니다.


✔ 여러 작업이 동시에 진행되는 느낌을 주는 것
✔ 실행 성능 및 반응성 향상

 

1.2 동시성이 필요한 이유

 

  • 향상된 성능 → 멀티 코어 프로세서를 최대한 활용하여 작업을 병렬 처리
  • 높은 반응성 → UI 작업과 백그라운드 작업을 분리하여 끊김 없는 사용자 경험 제공
  • 효율적인 자원 활용 → CPU가 I/O 대기 시간 동안 다른 작업 수행

 

 


2. 자바 동시성 핵심 개념

 

2.1 스레드 생성 및 실행

 

자바에서는 Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하여 스레드를 생성할 수 있습니다.

 

① Thread 클래스 상속

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread using Thread class");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // 스레드 실행
    }
}

출력 결과

Thread using Thread class

 

② Runnable 인터페이스 구현

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread using Runnable interface");
    }
}

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

출력 결과

Thread using Runnable interface

 

③ 람다 표현식 사용

public class LambdaThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("Thread using lambda Runnable"));
        thread.start();
    }
}

출력 결과

Thread using lambda Runnable

 

 


2.2 스레드 생명주기 (Thread Lifecycle)

 

상태 설명

NEW 스레드 객체 생성, 아직 start() 호출되지 않음
RUNNABLE 실행 가능하지만, CPU 할당을 기다리는 상태
BLOCKED 특정 락을 기다리는 중
WAITING wait() 메서드로 인해 무기한 대기 상태
TIMED_WAITING sleep(), join(timeout) 등으로 일정 시간 대기
TERMINATED 실행이 완료되어 종료된 상태

 

 


2.3 동기화 (Synchronization)

 

동기화는 여러 스레드가 공유 자원에 동시에 접근할 때 발생하는 데이터 불일치 문제를 해결하기 위해 사용됩니다.

 

① synchronized 키워드

class Counter {
    private int count = 0;

    public synchronized void increment() { // 메서드 동기화
        count++;
    }

    public int getCount() {
        return count;
    }
}

 

② Lock 인터페이스 사용 (ReentrantLock)

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockCounter {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

synchronized는 블록/메서드 단위로 적용되지만, Lock 인터페이스는 더 세밀한 제어가 가능하다

 


2.4 데드락 (Deadlock) 

 

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class DeadlockExample {
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();

    public void method1() {
        lock1.lock();
        try {
            Thread.sleep(100); // 일부러 지연
            lock2.lock();
            try {
                System.out.println("Method1 실행");
            } finally {
                lock2.unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock1.unlock();
        }
    }

    public void method2() {
        lock2.lock();
        try {
            Thread.sleep(100);
            lock1.lock();
            try {
                System.out.println("Method2 실행");
            } finally {
                lock1.unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock2.unlock();
        }
    }
}

데드락 발생 가능성:

이 코드는 데드락이 발생할 수 있는 전형적인 상황을 보여줍니다.

만약 두 개의 스레드가 동시에 다음 순서로 실행된다면 데드락이 발생할 수 있습니다.

  1. 스레드 A가 method1()을 실행하여 lock1을 획득합니다.
  2. 스레드 B가 method2()를 실행하여 lock2를 획득합니다.
  3. 스레드 A는 method1() 내부에서 lock2.lock()을 호출하여 lock2를 획득하려고 시도합니다. 하지만 lock2는 이미 스레드 B가 획득한 상태이므로, 스레드 A는 lock2가 해제될 때까지 대기합니다.
  4. 스레드 B는 method2() 내부에서 lock1.lock()을 호출하여 lock1을 획득하려고 시도합니다. 하지만 lock1은 이미 스레드 A가 획득한 상태이므로, 스레드 B는 lock1이 해제될 때까지 대기합니다.

이제 스레드 A는 lock2를 기다리고, 스레드 B는 lock1을 기다리는 상황이 발생합니다. 서로가 가진 락을 놓지 않고 상대방의 락을 기다리기 때문에, 두 스레드는 영원히 멈춰있는 데드락 상태에 빠지게 됩니다.

 

 

해결 방법:


✔ 락 획득 순서를 일관되게 유지
✔ tryLock() 메서드를 사용하여 타임아웃을 설정

if (lock1.tryLock(1, TimeUnit.SECONDS)) {
    try {
        if (lock2.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println("작업 실행");
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}

 


2.5 스레드 풀 (Thread Pool) 사용

 

스레드 풀을 사용하면 스레드 생성 비용을 줄이고, 자원을 효율적으로 관리할 수 있습니다.

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

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            final int taskNumber = i;
            executorService.execute(() -> System.out.println(Thread.currentThread().getName() + " - Task " + taskNumber));
        }

        executorService.shutdown();
    }
}

 

 


마무리

  • 자바의 동시성 기능을 사용하면 멀티코어 환경에서 성능을 최적화할 수 있다.
  • 하지만 동기화, 데드락, 스레드 풀 등의 개념을 잘 이해하고 사용해야 한다.
  • 동시성 관련 도구 (synchronized, Lock, ExecutorService) 를 적절히 활용하면 안전하고 효율적인 코드를 작성할 수 있다.

'Programming Language > Java' 카테고리의 다른 글

[Collection] Map 인터페이스와 주요 구현체 학습  (0) 2025.03.06
Jackson 라이브러리  (0) 2025.03.05
Record  (0) 2025.03.05
Exception handling  (0) 2025.03.05
Optional (예제 주의)  (0) 2025.03.05
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유