본문으로 건너뛰기
김신건의 로그

[Java] ConcurrentSkipListMap

· 수정 · 📖 약 2분 · 676자/단어 #java #concurrent #map #skip-list #sorted #lock-free #navigable
ConcurrentSkipListMap, java.util.concurrent.ConcurrentSkipListMap, skip list map, 동시성 정렬 맵, ConcurrentNavigableMap

정의

java.util.concurrent.ConcurrentSkipListMap<K,V>Skip List 기반의 thread-safe 정렬 Map. TreeMap 의 동시성 버전.

ConcurrentHashMap 과의 결정적 차이: 정렬 순서를 유지 + ConcurrentNavigableMap 인터페이스 구현 (floor, ceiling, subMap 등).

Skip List 는 평균 O(log n) 의 균형 트리 대체 자료구조. 락 없이 동시 수정이 가능한 알고리즘이 잘 알려져 있어 동시성 구현에 적합.

JDK 1.6 도입.

언제 쓰나

  • 정렬 + 동시성: 여러 스레드가 동시에 읽고 쓰면서 정렬 순서가 필요할 때
  • Range 쿼리 + 동시성: subMap, headMap, tailMap 으로 범위 조회가 필요할 때
  • 시간순 이벤트 로그: 타임스탬프 기준 정렬, 최근 N 초 이벤트 조회
  • 계단식 요금/등급 룩업: floorEntry 로 구간 매핑
  • 리더보드: 점수 기준 정렬, 상위 N 명 조회

시각화: Skip List 구조

flowchart TD
    subgraph "Level 3 (sparse)"
        H3["head"] --> N25_3["25"] --> T3["null"]
    end
    subgraph "Level 2"
        H2["head"] --> N10_2["10"] --> N25_2["25"] --> N40_2["40"] --> T2["null"]
    end
    subgraph "Level 1"
        H1["head"] --> N5_1["5"] --> N10_1["10"] --> N17_1["17"] --> N25_1["25"] --> N40_1["40"] --> T1["null"]
    end
    subgraph "Level 0 (base)"
        H0["head"] --> N5_0["5"] --> N10_0["10"] --> N17_0["17"] --> N25_0["25"] --> N33_0["33"] --> N40_0["40"] --> T0["null"]
    end
  • 각 노드가 확률적으로 여러 레벨에 포함 (1/2 확률로 한 레벨씩 위로)
  • 상위 레벨은 “스킵 인덱스” 처럼 작용해 O(log n) 검색
  • 새 노드 삽입 시 동전 던지듯 레벨 결정

시각화: floor/ceiling/subMap 동작

flowchart LR
    subgraph "ConcurrentSkipListMap keys: 5, 10, 17, 25, 33, 40"
        direction LR
        K5["5"] --- K10["10"] --- K17["17"] --- K25["25"] --- K33["33"] --- K40["40"]
    end

    F["floorKey(22)\n22 이하 최대 = 17"] --> K17
    C["ceilingKey(22)\n22 이상 최소 = 25"] --> K25
    Sub["subMap(10, 30)\n= {10, 17, 25}"] --> K10

핵심 NavigableMap 메서드

import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentNavigableMap;

ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(10, "ten");
map.put(25, "twenty-five");
map.put(40, "forty");
map.put(5, "five");

// 경계값 조회
map.firstKey();              // 5 (최솟값)
map.lastKey();               // 40 (최댓값)

// 인접 키 탐색
map.floorKey(22);            // 10 (22 이하 최대)
map.ceilingKey(22);          // 25 (22 이상 최소)
map.lowerKey(25);            // 10 (25 미만 최대)
map.higherKey(25);           // 40 (25 초과 최소)

// 범위 view (live view, 동시 수정 반영)
ConcurrentNavigableMap<Integer, String> sub = map.subMap(10, true, 30, false);
ConcurrentNavigableMap<Integer, String> head = map.headMap(25, false);
ConcurrentNavigableMap<Integer, String> tail = map.tailMap(25, true);

// 역순 view
ConcurrentNavigableMap<Integer, String> desc = map.descendingMap();

내부 구현: lock-free CAS

// 단순화된 내부 구조
class ConcurrentSkipListMap<K,V> {
    // 각 노드는 여러 레벨의 next 포인터를 가짐
    static final class Node<K,V> {
        final K key;
        volatile Object value;   // volatile: 가시성 보장
        volatile Node<K,V> next; // CAS 대상
    }

    static final class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;   // 아래 레벨
        volatile Index<K,V> right; // CAS 대상
    }
}
  • 삽입: CAS 로 노드를 base level 에 추가 후, 확률적으로 상위 레벨 인덱스 추가
  • 삭제: 논리적 삭제 (value 를 null 로 CAS) 후 물리적 제거
  • 검색: 상위 레벨부터 내려오며 스킵, 락 없음

복잡도

작업평균최악
get, put, removeO(log n)O(n) (매우 드뭄)
firstKey, lastKeyO(1)O(1)
floorKey, ceilingKeyO(log n)O(n)
subMap (view 생성)O(log n)O(n)
순회O(n)O(n)
size()O(n)O(n)

WARNING

size() 는 O(n) 이다. 크기를 자주 확인해야 한다면 별도 AtomicInteger 카운터를 유지하는 것이 낫다.

스레드 안전성

  • 완전 lock-free: 읽기/쓰기 모두 CAS 기반. 락 없음.
  • weakly consistent iterator: 순회 중 다른 스레드가 수정해도 ConcurrentModificationException 없음. 단, 수정 내용이 반영될 수도 있고 안 될 수도 있음.
  • 원자적 복합 연산: putIfAbsent, replace, remove(key, value) 모두 원자적.
ConcurrentSkipListMap<String, Integer> scores = new ConcurrentSkipListMap<>();

// 원자적 복합 연산
scores.putIfAbsent("alice", 100);
scores.replace("alice", 100, 150);   // CAS: 100 이면 150 으로
scores.merge("alice", 10, Integer::sum);   // 없으면 10, 있으면 합산

Java 17+ 실전: 시간순 이벤트 로그

import java.util.concurrent.*;
import java.time.Instant;

record Event(String type, String payload) {}

class EventLog {
    private final ConcurrentSkipListMap<Instant, Event> log =
        new ConcurrentSkipListMap<>();

    // 여러 스레드에서 동시에 이벤트 추가
    void record(Event event) {
        log.put(Instant.now(), event);
    }

    // 최근 N 초 이벤트 조회 (live view)
    ConcurrentNavigableMap<Instant, Event> recent(int seconds) {
        return log.tailMap(Instant.now().minusSeconds(seconds));
    }

    // 특정 시간 범위 이벤트
    ConcurrentNavigableMap<Instant, Event> between(Instant from, Instant to) {
        return log.subMap(from, true, to, true);
    }

    // 오래된 이벤트 정리
    void evictBefore(Instant cutoff) {
        log.headMap(cutoff).clear();
    }
}

Java 17+ 실전: 계단식 요금 룩업

import java.util.concurrent.*;

// 동시성 환경에서 계단식 요금 조회
// 0원 이상: 0%, 1000원 이상: 5%, 5000원 이상: 10%, 10000원 이상: 15%
class PricingTable {
    private final ConcurrentSkipListMap<Integer, Integer> table =
        new ConcurrentSkipListMap<>();

    PricingTable() {
        table.put(0,     0);
        table.put(1000,  5);
        table.put(5000,  10);
        table.put(10000, 15);
    }

    int getDiscountRate(int amount) {
        var entry = table.floorEntry(amount);
        return entry != null ? entry.getValue() : 0;
    }

    // 동적으로 요금 구간 추가/수정 (thread-safe)
    void updateRate(int threshold, int rate) {
        table.put(threshold, rate);
    }
}

Java 17+ 실전: 동시성 리더보드

import java.util.concurrent.*;

// 점수 기준 정렬 리더보드 (여러 스레드에서 동시 업데이트)
class Leaderboard {
    // key: 점수 (내림차순), value: 플레이어 이름
    private final ConcurrentSkipListMap<Integer, String> board =
        new ConcurrentSkipListMap<>(java.util.Comparator.reverseOrder());

    void updateScore(String player, int score) {
        // 기존 점수 제거 후 새 점수 추가 (단순화)
        board.put(score, player);
    }

    // 상위 N 명 조회
    List<Map.Entry<Integer, String>> topN(int n) {
        return board.entrySet().stream()
            .limit(n)
            .toList();
    }

    // 특정 점수 이상 플레이어
    ConcurrentNavigableMap<Integer, String> aboveScore(int minScore) {
        return board.headMap(minScore, true);   // 내림차순이므로 headMap
    }
}

ConcurrentHashMap vs ConcurrentSkipListMap

항목ConcurrentHashMapConcurrentSkipListMap
get/put 시간O(1) 평균O(log n)
순서없음정렬
Range 쿼리✓ (subMap 등)
floor/ceiling
size()O(1)O(n)
동시성 메커니즘CAS + 버킷 synclock-free skip list
null key/value

순서가 중요하지 않으면 ConcurrentHashMap, 정렬/범위 쿼리가 필요하면 ConcurrentSkipListMap.

함정

1. size() 는 O(n)

// 위험: 루프 안에서 size() 반복 호출
while (map.size() > 0) {   // 매번 O(n) 순회
    map.pollFirstEntry();
}

// 올바름: isEmpty() 사용 (O(1))
while (!map.isEmpty()) {
    map.pollFirstEntry();
}

2. null key/value 불허

ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();
map.put(null, 1);    // NullPointerException
map.put("a", null);  // NullPointerException

3. subMap view 는 live view

ConcurrentNavigableMap<Integer, String> sub = map.subMap(10, 30);
// sub 를 통한 수정은 원본 map 에 반영됨
sub.put(20, "twenty");   // map 에도 추가됨
sub.put(50, "fifty");    // IllegalArgumentException (범위 밖)

4. weakly consistent iterator

// 순회 중 다른 스레드가 추가한 원소가 보일 수도, 안 보일 수도 있음
for (Map.Entry<Integer, String> e : map.entrySet()) {
    // 다른 스레드가 map.put(99, "x") 해도 CME 없음
    // 단, 99 가 이 순회에서 보일지는 보장 안 됨
}

관련 위키

이 글의 용어 (8개)
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] ConcurrentHashMapjava
정의 는 고동시성 환경에서 사용 가능한 구현. JSR-166 (Java 5) 도입, Java 8 에서 내부 구조가 크게 재작성됐다. 과 같은 인터페이스를 제공하면서 thread-…
[Java] ConcurrentModificationExceptionjava
정의 (CME) 는 fail-fast iterator 가 컬렉션의 구조 변경을 탐지했을 때 던지는 unchecked 예외 ( ). 이름과 달리 반드시 동시성 (멀티스레드) 문제일…
[Java] ConcurrentSkipListSetjava
정의 는 을 백킹으로 사용하는 정렬된 thread-safe . 인터페이스를 구현하며 의 동시성 버전에 해당한다. - JDK 1.6 추가 - 내부는 skip list (확률적 자료…
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Java] Objectjava
정의 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. 가 명시되지 않은 클래스는 컴파일러가 자동으로 를 붙인다. 인터페이스는 클래스가 아니라 를 직접 상속하지는 …
[Java] TreeMapjava
정의 는 Red-Black Tree 기반의 정렬된 Map. (확장 인터페이스) 을 구현해 범위 쿼리, floor/ceiling, sub-map 같은 정렬 기반 연산을 제공한다. …
집합과 맵 (Set and Map)algorithm
정의 집합 (Set) 은 고유한 원소들의 모임을 나타내는 추상 자료구조로, 멤버십 검사, 삽입, 삭제를 지원한다. 맵 (Map, Dictionary) 은 키-값 쌍을 저장하며, …

이 개념을 다룬 위키 페이지 (2)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기