Java 동시성

김희영·2025년 11월 3일

토막개발지식

목록 보기
20/27

동시성은 여러 작업을 동시에 처리하는 것이다.
이는 2가지 방법으로 구현할 수 있는데, 첫번째는 진짜로 한번에 여러가지 작업을 하는 거고(멀티코어), 두번째는 빠르게 번갈아가며 하는 거다.

멀티코어는 말 그대로 코어(CPU)가 여러개인것이다. 일하는 놈이 여러명이 정말 좋지만, 이건 하드웨어 기반이라 사용자 컴퓨터가 구리면 말짱 꽝이다. 그리고 이건 동시성이 아니라 병렬성이다.

구분의미예시
동시성(Concurrency)여러 작업을 “번갈아가며” 빠르게 처리한 코어에서 여러 스레드를 번갈아 실행
병렬성(Parallelism)여러 작업을 “진짜 동시에” 처리여러 코어에서 각각의 스레드를 동시에 실행

-> 병렬성은 동시성의 한 형태라고 볼 수 있음.

그럼 두번째 방법을 보자. 작업을 작은 일(Thread)로 나누고, 이를 돌아가며 하는 건 꽤 좋은 방법이지만, 신경쓸 것이 좀 있다.

스레드 (Thread)

자바에서는 병렬 작업이나 비동기 처리를 스레드로 할 수 있는데, 스레드는 직접 관리할 수 있다. 클래스도 제공한다. 다음은 Thread 클래스다.

Thread t = new Thread(() -> {
    System.out.println("Hello from another thread!");
});
t.start();

이때 이 스레드는 OS 커널 스레드와 1:1로 연결되어 돌아가는데, 이를 플랫폼 스레드(Platform Thread)라고 한다. 근데 이러면 하나를 만들 때마다 운영체제가 리소스를 할당하므로 가볍지 않고, 느리다.

그리고 이렇게 일을 돌아가면서 하라고 하면 공유자원 충돌이 일어날 수 있다.
이건 다행히 모니터(Monitor)와 synchronized로 해결이 가능하다.

모니터(Monitor)

모니터는 객체 단위로 존재하는 동기화(synchronization) 메커니즘이다.
그니까 객체 단위로 존재하는 잠금 장치를 두는 거다. 이렇게 잠궈두고 하나의 스레드에게만 권한을 주면 충돌을 피할 수 있다.

synchronized

synchronized는 모니터 락을 제어하는 키워드다.
블록이나 메서드에 붙여서 임계 구역(Critical Section)을 설정한다. 이걸로 데이터 무결성을 지킬 수 있다.

하지만 이렇게 락을 막 걸어두면 락 경쟁이 많아진다. 그럼 당연히 성능 저하가 뒤따른다.

그러니 이렇게 스레드를 직접 관리하면 생성 비용과 컨텍스트 스위칭 오버헤드가 크고,
공유 자원 동기화나 예외 처리도 어렵다. 쓰지말라면 쓰지마라

어 그럼 저는 ThreadLocal로 각 스레드가 값 가지게 해서 공유자원문제 해결 할래요 < 이럴 수 도 있긴 한데 그럼 동기화 문제가 터진다. ThreadLocal은 스레드 별 독립적인 상태만 보장해주니 불변 객체만 쓰고,

그냥 Executor 프레임워크와 비동기 API(Future, CompletableFuture)를 써서 스레드 관리를 추상화 하자.

Executor Framework

먼저번에서 Thread pool executor에 대해 다룬 적이 있다.

요약하자면 스레드는 재활용하는게 효율적이고, 재활용하려고 미리 만들어 둔 스레드 집합(pool)에서 꺼내 쓴다.
그리고 그 스레드 풀 관리하기 쉽게 표준화 한게 Executor Framework.

ExecutorService executor = Executors.newFixedThreadPool(5); //풀 사이즈 : 5

for (int i = 0; i < 10; i++) {
    executor.submit(() -> doWork()); //꺼내 씀
}

executor.shutdown(); //다쓰면 스레드 정리

이르케 씀

Future / CompletableFuture

그럼 스레드 풀로 병렬 실행 관리는 할 수 있겠다, 결과는 어떻게 할까?
작업을 하는데는 시간이 걸리기 때문에 보통 비동기 처리를 한다. 그 비동기 처리를 위한 게 Future와 CompletableFuture

Future (JAVA 5)

Future<Integer> f = executor.submit(() -> 1 + 2);
System.out.println(f.get());

이러면 결과를 바로 받을 수는 있지만, get()은 결과를 받을 때까지 존버 블로킹한다.
-> 기다리는 동안 다른 일을 못하는 거다...
그리고 콜백 등록 불가능 (결과가 나왔을 때 자동 실행 불가),여러 비동기 작업을 조합하거나 예외를 처리하기 어려운 문제도 있다.

CompletableFuture (JAVA 8)

CompletableFuture를 쓰면 비동기 처리를 체이닝 형태로 실행할 수 있다.
즉, 앞에 거 하고 다음일 하고 받고 하고 받고 하고...

CompletableFuture.supplyAsync(() -> {
    return 1 + 2;
}).thenApply(sum -> sum * 2)
  .thenAccept(System.out::println);

이렇게 하면 여러 비동기 작업을 조합할 수 있을 뿐더러 블로킹도 없다.
스레드풀 지정도 가능하고 exceptionally, handle로 예외를 처리할 수도 있다.
역시 기술은 최신걸 써야한다.

+)

profile
내는 반드시 엄청난 개발자가 되고 말것어

0개의 댓글