본문 바로가기

웹 & 앱 꿀 TIP

[Java] Collections Map + concurrent Collections

728x90
반응형

Collections Task_V0.3

취지

실무 데이터와 흡사한 map의 데이터 형태를 측정하여 실리적인 정보 추출

JVM Min(Xms),Max(Xmx)설정 여부를 명확히 확인

결론적으로 어떠한 상황에서 어떤 클래스를 사용하는 것이 가장 메모리에 부담이 덜가는지 결론을 낸다.

테스트 환경 및 개요

Map 테스트의 실제 데이터

  • key의 값은 random()(을)를 활용하여 1000byte 크기로 설정
  • value의 값은 object로 멤버변수 3개를 넣어서 다량의(+복잡한) 데이터를 .put

JVM Min(Xms),Max(Xmx) 설정값과 설정 이유

  • 효율적인 값으로 측정
  • 64bit 환경에서는 -xmx 옵션을 32GB이하로 설정하는 것이 적절 (이상을 할당 시 비효율적 방식으로 변경_큰 오버헤드 발생)
  • Xms와 Xmx를 동일하게 설정 (힙 크기를 늘릴 때마다 시간이 소요됨, xms를 작게 설정하면 GC가 많이 발생됨,..등)
  • 결론 : Xms 모두 기본값 2018m(으)로 설정*

구현 방안

만건, 5만건 10만건 20만건 50만건 100만건, 200만건 까지의 메모리 사용량을 확인

HashMap, LinkedHashMap, TreeMap 3개의 interface를 하나의 그래프에서 사용량을 시각화

key값

public static String TestRandom() {
        int leftLimit = 48; // numeral '0'
        int rightLimit = 122; // letter 'z'
        int targetStringLength = 1000;
        Random random = new Random();
        String generatedString = random.ints(leftLimit, rightLimit + 1)
                .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
                .limit(targetStringLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();

        return generatedString;
    }

value값

MapObjedtVal{time='1674605678617', userAgent='Mozilla/5.0 (Windows 98) AppleWebKit/5330 (KHTML, like Gecko) Chrome/37.0.847.0 Mobile Safari/5330', randomString=V1GKtpEX7PZTnXXw0LTxqJQDQ3rO9FjneqvpIQImvAoFJ81VR0SQUCuCUPcbRasIi9HQDCAD5E71ykqRqOu2yLA6qxDz4Jsc8y1ovA5zUWGRBcgs3LkAm9UftHK6xIMhFp0OnlZSqoWHGwqlcQS38uEMEsxasTLO6NbzIXUbpLJMYskXVM5iJB6e4abJsba8Wg5FVCbCMD0fq7afrCOplv4Hctd93dc2IzloVXusuEBW6pDt9Yjl1VwQMFIrd24wRxNbiffEbaUuY7Kj8W6g3wgLuIlZd29irAWweBJGMqHALB7KO0o3DpZypHudOPD7LHvXdypEXY0YA1fXqss8RebjRJRNJrClPlmuq7fiabtemuoaFNXY8qzxTWDY7QcSLShIhue0Yq3DaoCbTLaLVvYQxjCIyeVJvxD354DUrJyrHez95100BIzCRRb9CeqVnM4nbYDtSVbobAdQygkeiPMf5jGQWAlf3gAonNHM2IJZgZjgTo5SWfKoxvEkJbFmJRF00K0NDAV6VwE0sYCutoHRIfG6sMaA1Z9cainhRhz6EjhypkYueZQgqO7gRlrhkBnLw5gRnB7eWJo2VIEgmgXhy2r5HHJ3tnpKMgE3vRQjHSD4zERoKPzKeP2oyEWpiijPV2lT5M9lBLQnXkUKaOYO34yTAMbm4ktyljYci6CzezWkhmamvTzybo29K5wEeETxPz06s5JNpnKsRBbAAqm1b4kEhVLZDpO9MiKbn92dzH3zzGzIvmFzv85ZnwBDpDiIIVgu9XuqnyQP3NKFyV4RHr5fOBeay2EJN0XGqv74x1cUb8GwM9vLGkKsv60ydICeTEiWMbTqwlLysxfHEij1dTqjQutLw79zWxvdK2LVRijbeFkvUTaztm1Zk0pSptP3RU28jbi3xfW7oU1UJRiIXTMy43OHxse6BS9CWbr4U6kbUEPDQrJwCiNJZET9N3vIWhI7au8w1hKhpPujAavSxQJAQTRPgJcxb4s7}

데이터 측정 방법

  • 힙 메모리에 착오가 가지 않도록 만건부터 200만건까지 일일히 기입하여 테스트
  • 10번을 수행 후 그에 대한 평균 값으로 산출
  • interface에 따른 시각화 형태로 결과를 확인
  • 대표적인 확실한 결과값(예시 : ###건의 데이터에서는 메모리 #를 사용)도 내용 보고

출력 결과

White___Blue_Modern_Line_Chart_Graph

결론 (부제 : 취지에 맞는 결론인가?)

근소한 차이지만, TreeMap을 사용하는 것이 가장 메모리의 사용량이 적어 효율이 가장 좋다고 할 수 있으며 LinkedHashMap이 (메모리측면에서) 결과적으로 가장 비효율적이라고 설명할 수 있다.

200만건의 경우 TreeMap은 4460, HashMap은 4326, LinkedHashMap은 4734가 출력되었다.

결론 : 큰 데이터를 map에 넣을 때에 TreeMap(을)를 사용하는 것이 가장 메모리 측면에서 좋다.

코드 구현 방안

import java.lang.management.*;
import java.util.*;

public class CollectionsMap {

    public static void main(String[] args) {
        // variable declaration
        int NUM = 2000000;

        TreeMap<String,MapObjectVal> hashMap = new TreeMap<>();


        for (int i = 0; i <= NUM; i++) {
            hashMap.put(TestRandom(), new MapObjectVal());
        }

        //used Memory Check
        long aftermemory = ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024);

        System.out.println(aftermemory);

    }

    public static String TestRandom() {
        int leftLimit = 48; // numeral '0'
        int rightLimit = 122; // letter 'z'
        int targetStringLength = 1000;
        Random random = new Random();
        String generatedString = random.ints(leftLimit, rightLimit + 1)
                .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
                .limit(targetStringLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();

        return generatedString;
    }
}

etc

jdk version : "17.0.5" 2022-10-18 LTS

xms xmx result : 2018m

OS : Windows 10 Pro

System : 24.0GB (RAM) / 64bit

추가적인 데이터 형태가 다름으로 인한 Map 처리 속도에 대한 보고서

image

            ※ 책 "Java Generics and Collections"라는 책에서 발췌(pages : 188,211,222,240)

HashMapTreeMap의 시간복잡도는 O(n)과 O(log n)으로 큰 차이는 없으나, 분명 차이가 있다.

해당 내용을 시각화 자료가 아닌 확실한 값(예시 : ###건의 데이터에서는 HashMap의 경우 #초, TreeMap의 경우 #초)(을)를 구현하고자 한다.

구현 방안

시간적 측면
시간적 측면 : 10만건의 데이터부터 500만건의 데이터까지 순차적으로 늘려 해당 처리 속도를 측정
            -> 같은호출을 100번하여 100개의 시각에 평균을 내는 방향으로 진행(오차범위를 최대한 ↓ )

코드구현 방향

데이터 건수 늘리는 코드

        for (int i = 100000; i <= 5000000; i +=10000) {// Make growing data
            for (int j = 0; j <= i; j++) {
                hashMap.put(j, 1); //input data
            }
        }

10000건식 늘려서 5백만건까지 Map에 put

            for (int x = 0; x <= 100; x++) { // average of 100
                long startTime = System.nanoTime();
                // Test Code

                long endTime = System.nanoTime();
                avgch100.add((int) (endTime - startTime));
            }

nanoTime으로 설정/ next()같은 경우는 currentTimeMillis()를 사용

  1. 각각의 메소드 코드 grid로 기재
TestCode HashMap TreeMap LinkedHashMap
add(put) hashmap.put(variable,1) treemap.put(variable,1) linkedhashmap.put(variable,1)
get hashmap.get(variable) treemap.get(variable) linkedhashmap.get(variable)
remove hashmap.clear(); treemap.clear() linkedhashmap.clear();
contains hashmap.containskey(variable); treemap.containskey(variable); linkedhashmap.containskey(variable);
next Iterateor<Integer> keys = hashmap.keySet().iterator();while(keys.hasNext()){int key = keys.next();} Iterateor<Integer> keys = treemap.keySet().iterator();while(keys.hasNext()){int key = keys.next();} Iterateor<Integer> keys = linkedhashmap.keySet().iterator();while(keys.hasNext()){int key = keys.next();}

출력 결과

코드 add(put) get remove(clear) contains next
HashMap 전체 : 26ns
10만-100만: 31ns
200만-400만: 25ns
전체 : 25ns
10만-100만 : 30ns
200만-400만: 24ns
전체 : 17083ns
10만-100만: 3427ns
200만-400만: 20965ns
전체 : 36ns
10만-100만: 44ns
200만-400만: 35ns
전체 : 14m/s
10만-100만: 2m/s
200만-400만: 17m/s
TreeMap 전체 : 103ns
10만-100만: 106ns
200만-400만: 102ns
전체 : 99ns
10만-100만: 120ns
200만-400만: 89ns
전체 : 22ns
10만-100만: 30ns
200만-400만: 18ns
전체 : 113ns
10만-100만: 122ns
200만-400만: 110ns
전체 : 15m/s
10만-100만: 2m/s
200만-400만: 18m/s
LinkedHashMap 전체 : 36ns
10만-100만: 49ns
200만-400만: 33ns
전체 : 24ns
10만-100만: 35ns
200만-400만: 21ns
전체 : 17164ns
10만-100만: 3721ns
200만-400만: 20837ns
전체 24: ns
10만-100만: 32ns
200만-400만: 21ns
전체 : 9m/s
10만-100만: 1m/s
200만-400만: 11m/s

TreeMap은 확실히 시간복잡도 측면에서 미흡하나, clear()은 속도가 가장 좋다.
나와 같은 질문을 한 링크 및 답변(https://www.quora.com/Java-Why-does-clear-on-a-HashMap-take-O-n-time-while-clear-on-a-TreeMap-takes-only-O-1-time)
또한 HashMap과 LinkedHashMap은 시간복잡도표와 같이 차이가 근소하게 나타났다

실제 코드

시간 측정

import java.util.*;

public class CollectionsBigData {
    public static void main(String[] args) {
        TreeMap<Integer, Integer> hashMap = new TreeMap<>();
        ArrayList<Integer> avgch100 = new ArrayList<>();
        Map<Integer, Integer> result = new LinkedHashMap<>();

        for (int i = 100000; i <= 5000000; i +=10000) {// Make data
            for (int j = 0; j <= i; j++) {
                hashMap.put(j, 1);
            }
            for (int x = 0; x <= 100; x++) { // average
                long startTime = System.currentTimeMillis();
                // Test Code
                Iterator<Integer> keys = hashMap.keySet().iterator();
                while(keys.hasNext()){int key = keys.next();}
                long endTime = System.currentTimeMillis();
                avgch100.add((int) (endTime - startTime));
            }
            result.put(i, avg(avgch100));
            hashMap.clear();
            avgch100.clear();
        }
        System.out.println(result);
    }
    public static int avg(ArrayList<Integer> list) {
        int sum = 0;
        for (int num : list) {
            sum += num;
        }
        return sum / list.size();
    }

}

이전에 했던 메모리 측정코드_V0.2

import java.lang.management.*;
import java.util.*;

public class CollectionsMap {

    public static void main(String[] args) {

        ArrayList avg = new ArrayList();
        HashMap<Integer,Integer> hashMap = new HashMap<>();
        int check = 0;
        for(int i = 0; i <10001; i++) {
            hashMap.put(i, 1);
        }
        MemoryUsage HeapMemorybefore = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); //a MemoryUsage object representing the heap memory usage.
        for(int i = 0; i <= 10000; i++){
            // testcode
            hashMap.containsKey(i);
            MemoryUsage HeapMemoryafter = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
//            System.out.println(i+"th >>>>>"+HeapMemorybefore.getUsed()+">>>"+HeapMemoryafter.getUsed());
            avg.add(HeapMemoryafter.getUsed()-HeapMemorybefore.getUsed());

            if(check == 0 && HeapMemoryafter.getUsed()==4194304){
                check += 1;
                System.out.println(i+"th 4194304");
            } else if (check == 1 && HeapMemoryafter.getUsed()==8388608) {
                check += 1;
                System.out.println(i+"th 8388608");
            } else if (check == 2 && HeapMemoryafter.getUsed()==12582912) {
                check += 1;
                System.out.println(i+"th 12582912");

            }else if (check == 3 && HeapMemoryafter.getUsed()==25165824) {
                check += 1;
                System.out.println(i+"th 25165824");
            }else if (check == 4 && HeapMemoryafter.getUsed()>25165825){
                System.out.println(i+"th >>>>"+HeapMemoryafter.getUsed());
            }
        }
        long result = 0;

        for (int i = 0; i <avg.size(); i++){
            long a = (long) avg.get(i);
            result += a;
        }
        System.out.println("average : " + (double)result/avg.size());
    }
}

결론 (부제 : 취지에 맞는 결론인가?)

  • Iterator함수를 사용할 경우 LinkedHashMap의 속도가 가장빠르다. 왜냐하면 LinkedHashMap은 순서를 갖고 있기에 Iterator함수를 사용할 때 바로 다음 참조값을 가지고 있기때문이다.

  • 모든 메소드에 대해서 (메모리측면에서) 최고의 효율을 내는 interface는 TreeMap이고, 그 다음은 HashMap, 그 다음은 LinkedHashMap순으로 효율적이라고 할 수 있다.

가장 많은 데이터에 효율적으로 대응한 메소드는 TreeMap의 get메소드다.

많은 양의 데이터를 삭제할 경우에는 TreeMap이 이진트리를 사용하므로 노드만 삭제하는 방향으로 진행되기에 해당 이유로 우수하다.

  • HashMap과 LinkedHaspMap에서는 속도(성능)차이가 거의 없다는 걸 확인할 수 있다.

  • 결론적으로 메모리측면에서 TreeMap이 우수한 반면 속도측면을 함께 보았을 때 HashMap이 속도와 메모리사용량을 함께 효율적으로 수행함으로 HashMap을 사용하는 것이 이번 테스트에서 가장 우수하다고 결론이 난다.

Hash가 우수한 이유 Hashmap은 말그대로 HasingMap이라는 뜻으로 Hash는 Hashing이라고 봐도 무방하다 그럼 Hasing은 우수한 이유가 된다. 그렇다면 Hashing이란 뭘까
Hashing(해싱)이란 인덱스를 구하는 것으로, 산술연산을 이용하여 키가 있는 위치를 계산해서 찾아가는 검색방식을 일컫는다.

이해가 안된다. 해싱의 구조에 대해서 짚어본다.

ex)어떤 회사에 사원수가 100명이라 사원 번호가 0~99 까지로 형성 탐색을 하려면 크기가 100인 배열을 만들면 끝
하지만 현실적으로는 사원번호(숫자)로 형성되어있지 않을 뿐더러 값이 매우 클 수 있기에 배열로는 효율적이지 못한다.
그래서 키를 그대로 사용하지 않고 작은 정수로 변경 후 관리하여 사용효율을 높인 것이다.

결론적으로 Hashing이란 어떤 항목의 키만을 가지고 바로 항목이 들어 있는 배열의 인덱스를 결정하는 기법

연관 상식으로 해시함수가 보인다.

해시함수란? 키를 입력으로 받아 해시주소를 생성하고 이 해시 주소를 해시 테이블의 인덱스로 사용

키 값이 들어오면 해쉬함수를 거치며 key값이 주소(index)로변환되어 해시 테이블에 저장된다.

즉, 키 값을 원소 위치로 변환하는 함수는 해시함수라고 한다.

해시 테이블이란 해시함수에 의해 계산된 주소에 저장할 값을 저장한 표를 말한다.
이는 버킷(배열)을 사용하여 데이터를 저장한다(※ 실제 값이 저장되는 장소 = 버킷)

결과적으로 빠른 이유는 key로 해싱함수를 통해 해당 인덱스의 위치로 이동하여 인덱스를 산출 후 데이터에 접근하기에 빈자리 찾는 다른 함수에 비해 성능이 우수하다.

해당 해싱함수를 통해 생기는 단점은 인덱스에 한계가 있다는 것이다.

해당 인덱스에 충돌이 생기면 2가지 방법으로 관리를 한다.

  1. Open Addressing : 다른 곳으로 배정
  2. Separate Chaining : 테이블의 구조를 변경 혹은 하나 이상의 키 값을 저장할 수 있도록 하는 법

Separate Chaining은 java8이후부터 데이터의 수가 많아지면 연결리스트가 아닌 트리 자료구조를 사용한다. 버킷 기본값은 16으로 일정 개수(버킷의 개수가 3/4가 되었을 때)가 지나면 2배로 늘리는 방식

Concurrent Collection

취지

실제 활용이 가능하게 concurrent collections에 대하여 설명 및 예시를 제안하여 실무에 도움이 되게 이해한다.

목적

  • 동시 수집 API Concurrent Collections가 무엇인가
  • 주요 기능은 무엇인가
  • 각각의 기능은 무엇이고 어떻게 활용하는가

Concurrent Collections가 무엇인가

concurrent collection = 동시에 수집하는(찾아오는) 이라는 뜻

동기화가 필요한 상황에서 사용할 수 있는 유틸리티 클래스를 제공

데이터를 여러부분으로 나눠어서 락을 걸어 동시에 같은 부분을 접근하면 차단하고,

다른 부분을 접근한다면 허용하여 결과적으로 여러 쓰레드가 동시에 한 객체에 접근이 가능하다.

성능

  • Map, List에 전체 lock을 걸지 않음
  • 여러 쓰레드가 한 번에 접근이 가능하기에 쓰레드 대기 시간을 줄여줌
  • 하나 이상의 쓰레드가 동시에 read, write연산을 할 수 있다.

패키지에서 제공하는 주요기능

  • Locks
  • Atomic
  • Executors
  • Queue
  • Synchronizers

Java.util.concurrent.locks

동기화를 더욱 유연하고 정교하게 처리하기 위해 만들어짐

Interface

Lock 공유 자원에 한번에 한 쓰레드만 read, write를 수행 가능하도록 한다.

ReadWriteLock Lock에서 한 단계 발전된 형태로 여러개의 쓰레드가 read를 수행할 수 있지만, write는 한번에 한 쓰레드만 수행이 가능하다

Condition Object 클래스
await() : 락을 해제하고 잠들게 만듬 그 다음에 온 스레드가 진행됨
signal() : await()으로 잠든 스레드들 중 하나를 깨움
signalAll() : await()로 잠든 모든 스레드를 동시에 깨움

구현체

ReentrantLock : Lock의 구현체, 임계영역∗의 시작과 종료 지점을 직접 명시할 수 있게 해줌

ReentrantReadWriteLock : ReadWriteLock의 구현체

  • 임계영역 : 둘 이상의 스레드가 공유 자원에 접근하는 코드의 일부(=통제가 필요한 영역 + 하나의 프로세스만 이용하게끔 보장해줘야하는 영역)

주요 메서드

lock() : Lock 인스턴스에 잠금을 걸어둔다. Lock인스턴스가 이미 잠겨있는 상태라면, 잠금을 걸어둔 쓰레드가 unlock()을 호출할 때까지 실행이 비활성화 된다.

lockInterruptibly(): 현재 쓰레드가 intterupted(중단된) 상태가 아닐 때 Lock 인스턴스에 잠금을 건다. 현재 쓰래드가 intterupted 상태면 InterruptedException을 발생시킨다.

tryLock() : 즉시 Lock 인스턴스에 잠금을 시도하고 성공 여부를 Boolean 타입으로 반환한다.

tryLock(long timeout, TimeUnit timeUnit) : tryLock()과 동일하지만, 잠금이 실패했을 때 바로 false를 반환하지 않고 인자로 주어진 시간동안 기다린다.

unlock() : Lock 인스턴스의 잠금을 해제한다.

활용예제

여러 쓰레드가 동일한 자원을 공유할 때 벌어지는 일을 확인
SharedData는 모든 쓰레드가 공유할 데이터를 정의한 클래스

public class SharedData {
    private int value;

    public void increase() {
        value += 1;
    }

    public void print() {
        System.out.println(value);
    }
}

main함수에서 10개의 TestRnnable 객체를 생성해 쓰레드별로 각각 increase()를 100번씩 호출

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

public class Main {
    public static void main(String[] args) {
        final SharedData mySharedData = new SharedData(); // shared resource

        for (int i = 0; i < 10; i++) {
            new Thread(new TestRunnable(mySharedData)).start();
        }
    }
}

class TestRunnable implements Runnable {
    private final SharedData mySharedData;

    public TestRunnable(SharedData mySharedData) {
        this.mySharedData = mySharedData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            mySharedData.increase();
        }

        mySharedData.print();
    }
}

실행 결과
image image image


10개의 쓰레드가 run()블록에 정의된 작업을 시분할 방식으로 번갈아가며 실행하기에 매번 조금씩 달라진다.

Lock을 사용해 동시성 문제를 해결한다.

How To

  • 쓰레드들이 공유할 Lock 인스턴스를 만들고, 동기화가 필요한 실행문의 앞 뒤로 lock()/unlock()을 호출하면 된다.
  • lock를 걸었다면 unlock도 빼먹지 않고 호출을 해야 임계 영역 블록이 실행이 끝나더라도 unlock()이 호출 되기 전 까지는 쓰레드의 잠금 상태가 영원히 유지되기에 어떤 예외가 발생하더라도 unlock()이 호출되도록 try-catch-finally 형태를 사용하는 것이 권장된다.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    public static void main(String[] args) {
        final SharedData mySharedData = new SharedData(); // shared resource
        final Lock lock = new ReentrantLock(); // lock instance

        for (int i = 0; i < 10; i++) {
            new Thread(new TestRunnable(mySharedData, lock)).start();
        }
    }
}

class TestRunnable implements Runnable {
    private final SharedData mySharedData;
    private final Lock lock;

    public TestRunnable(SharedData mySharedData, Lock lock) {
        this.mySharedData = mySharedData;
        this.lock = lock;
    }

    @Override
    public void run() {
        lock.lock();
        try {
            for (int i = 0; i < 100; i++) {
                mySharedData.increase();
            }

            mySharedData.print();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

실행 결과
image

synchronized vs Lock

  • synchronized는 블록 구조를 사용하기 때문에 하나의 메서드 안에서 임계 영역의 시작과 끝이 있어야 한다. Lock은 lock(), unlock()으로 시작과 끝을 명시하기 때문에 임계 영역을 여러 메서드에 나눠서 작성할 수 있다.
  • synchronized는 동기화가 필요한 블럭을 synchronized { }로 감싸 lock을 건다. 여러 쓰레드가 경쟁 상태에 있을 때 어떤 쓰레드가 진입권을 획득할지 순서를 보장하지 않는다. 이를 암시적인(implicit) lock 이라고 칭한다.
  • Lock은 lock()-unlock() 메서드를 호출함으로써 어떤 쓰레드가 먼저 락을 획득하게 될지 순서를 지정할 수 있다. 이를 명시적인(explicit) lock이라고 칭한다.
  • Lock은 인스턴스에 한 개 이상의 Condition을 지정할 수 있다. lockInterruptibly(), tryLock() 같은 편리한 제어 메서드를 사용할 수 있고, lock 획득을 기다리고 있는 쓰레드의 목록을 간편하게 확인할 수 있다.
  • synchronized는 간결한 코드로 임계 영역을 지정할 수 있다. 그리고 개발자의 실수로 lock을 해제하지 않아 문제가 생길 가능성이 없다. Lock을 사용할 경우 synchronized를 사용할 때와 달리 import 구문과 try-finally 문이 추가됨으로써 코드가 덜 간결해진다는 단점이 있다.

Java.util.concurrent.atomic

최신 데이터임을 보장 _ 중간에 멈추지 않는다.(= 완전히 수행되거나, 아무것도 수행하지 않아야 함

필요의 예시로는 재고가 1개인 상품을 사려는 고객이 2명 존재한다면 결제를 2명이 성공하게 되면 한명은 결제만 진행될 수 있음. 수강신청이나 티켓팅, 프린트 을 예시로 듬

서로 다른 작업이지만 한 세트로 진행이 되어야 함

작업 단위에 분리가 필요할 때 사용!

주요 클래스

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
  • AtomicIntegerArray
  • AtomicDoubleArray

주요 메서드

get() : 현재 값을 반환

set(newValue) : newValue로 값을 업데이트 한다.

getAndSet(newValue) : 원자적으로 값을 업데이트하고 원래의 값을 반환한다.

compareAndSet(expect, update) : 현재 값이 예상하는 값(=expect)과 동일하다면 값을 update 한 후 true를 반환한다. 예상하는 값과 같지 않다면 update는 생략하고 false를 반환한다.

Compare-And-Swap(CAS)

Atomic은 CAS 알고리즘을 기반으로 동작한다는 얘기를 했다. 이 CAS가 무엇인가

∴ 동시에 알고리즘을 설계할 때 사용되는 기술

현재 쓰레드에 저장된 값과 메인 메모리에 저장된 값을 비교하여 일치하는 경우, 새로운 값으로 교체하고, 일치하지 않는다면 실패하고 재시도. ==> CPU 캐시에서 잘못된 값을 참조하는 가시성 문제가 해결됨

활용도

변수마다 동기화를 해줄 수 있는 특징이 있다. Atomic자료형은 기본형인 int, long등의 동기화를 보장해주는 자료형.

메소드 구현 구조

image

현재 연산에서 기대하는 값과 메모리 상에서의 값이 일치하지 않는다면 중간에 다른 쓰레드가 끼어들었다고 판단해 write를 실패시키고 재시도를 하게된다. lock-free 방식으로 루프를 돌기 때문에 block<->unblock 상태 변경 처리에 쓰이는 비용을 절감할 수 있게된다.

728x90
반응형