동시성 컬렉션

황상익·2024년 10월 22일

Inflearn JAVA

목록 보기
53/61

동시성 컬렉션이 필요한 이유1 - 시작

util 패키지에 있는 컬렉션 프레임워크는 원자적인 연산을 제공??
여러 스레드가 동시에 접근해도 괜찮을까?? == Thread Safe

package chap48;

import chap35.list.ArrayList;
import chap35.list.List;

public class SimpleListMainV0 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("A");
        list.add("B");

        System.out.println(list);
    }
}
public interface SimpleList {
    int size();

    void add(Object e);

    Object get(int index);
}
package chap48;

import java.util.Arrays;

import static chap41.util.ThreadUtils.sleep;

public class BasicList implements SimpleList {

    private static final int CAPACITY = 5;

    private Object[] elements;
    private int size = 0;

    public BasicList() {
        this.elements = new Object[CAPACITY];
    }

    @Override
    public int size() {
        return size;
    }


    //원자적 연산 아니다.
    @Override
    public void add(Object e) {
        elements[size] = e;
        sleep(100); //멀티 스레드를 만드는 쉽게 확인하는 코드
        size++;
    }

    @Override
    public Object get(int index) {
        return elements[index];
    }

    @Override
    public String toString() {
        return Arrays.toString(Arrays.copyOf(elements, size)) + " size =" + size + " capacity=" + elements.length;
    }
}
package chap48;

import chap35.list.ArrayList;
import chap35.list.List;

public class SimpleListMainV1 {
    public static void main(String[] args) {
        SimpleList list = new BasicList();

        list.add("A");
        list.add("B");

        System.out.println(list);
    }
}

단일 스레드로 진행했기 때문에 문제 없음

동시성 컬렉션이 필요한 이유2 - 동시성 문제

package chap48;

import static chap41.util.MyLogger.log;

//두개를 넣었는데 하나의 값만 넣어짐
public class SimpleListMainV2 {
    public static void main(String[] args) throws InterruptedException {
       //test(new BasicList());
        //test(new SyncList());

        //target에 basicList가 들어감
        BasicList basicList = new BasicList();
        SyncProxyList syncProxyList = new SyncProxyList(basicList);
        //proxyList에 있는 add를 호출 -> synchronized를 건다
        test(syncProxyList);
    }

    private static void test(SimpleList list) throws InterruptedException {
        log(list.getClass().getSimpleName());

        Runnable addA = new Runnable() {

            //Thread A
            @Override
            public void run() {
                list.add("A");
                log("Thread-1 : List.add(A)");
            }
        };

        Runnable addB = new Runnable() {


            //Thread B
            @Override
            public void run() {
                list.add("B");
                log("Thread-2 : List.add(B)");
            }
        };

        Thread thread1 = new Thread(addA, "Thread-1");
        Thread thread2 = new Thread(addB, "Thread-2");

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        log(list);
    }
}

가정 1) 스레드 1,2가 동시에 수행 -> A 값이 들어가고 B가 들어갈때 A값을 변경
가정 2) 스레드 1,2가 동시에 연산 수행 -> 0으로 읽고 -> 최종 결과물 1

동시성 컬렉션이 필요한 이유3 - 동기화

package chap48;

import java.util.Arrays;

import static chap41.util.ThreadUtils.sleep;

public class SyncList implements SimpleList{
    private static final int CAPACITY = 5;

    private Object[] elements;
    private int size = 0;

    public SyncList() {
        elements = new Object[CAPACITY];
    }

    @Override
    public synchronized int size() {
        return size;
    }

    @Override
    public synchronized void add(Object e) {
        elements[size++] = e;
        sleep(100);
        size++;
    }

    @Override
    public synchronized Object get(int index) {
        return elements[index];
    }

    @Override
    public synchronized String toString() {
        return Arrays.toString(Arrays.copyOf(elements, size)) + " size =" + size + " capacity=" + elements.length;
    }
}
package chap48;

import static chap41.util.MyLogger.log;

//두개를 넣었는데 하나의 값만 넣어짐
public class SimpleListMainV2 {
    public static void main(String[] args) throws InterruptedException {
       //test(new BasicList());
        //test(new SyncList());

        //target에 basicList가 들어감
        BasicList basicList = new BasicList();
        SyncProxyList syncProxyList = new SyncProxyList(basicList);
        //proxyList에 있는 add를 호출 -> synchronized를 건다
        test(syncProxyList);
    }

    private static void test(SimpleList list) throws InterruptedException {
        log(list.getClass().getSimpleName());

        Runnable addA = new Runnable() {

            //Thread A
            @Override
            public void run() {
                list.add("A");
                log("Thread-1 : List.add(A)");
            }
        };

        Runnable addB = new Runnable() {


            //Thread B
            @Override
            public void run() {
                list.add("B");
                log("Thread-2 : List.add(B)");
            }
        };

        Thread thread1 = new Thread(addA, "Thread-1");
        Thread thread2 = new Thread(addB, "Thread-2");

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        log(list);
    }
}

안전한 임계영역을 설정해 줬기 때문에 하나의 스레드 add만 메서드 수행

동시성 컬렉션이 필요한 이유4 - 프록시 도입

그렇다면 모든 util에 있는 컬랙션들을 하나하나 수정??
이럴때 사용하는게 proxy! = 대신 처리해주는 자

package chap48;

public class SyncProxyList implements SimpleList{

    private SimpleList target;

    // target을 주입 -> 호출되는 대상이 target에 들어감
    public SyncProxyList(SimpleList target) {
        this.target = target;
    }

    @Override
    public synchronized int size() {
        return target.size();
    }

    //lock을 획득
    @Override
    public  void add(Object e) {
        //원본 메서드 호출
        target.add(e);
        //원본 메서드 반납
        //lock 반납
    }

    @Override
    public synchronized Object get(int index) {
        return target.get(index);
    }

    @Override
    public String toString() {
        return target.toString() + " by "  + this.getClass().getSimpleName();
    }
}

이 클래스는 생성자를 통해 SimpleList target 을 주입 받는다. 여기에 실제 호출되는 대상이 들어간다
클래스의 역할은 모든 메서드에 synchronized 를 걸어주는 일 뿐이다. 그리고나서 target 에 있는 같은 기능을 호출.

package chap48;

import static chap41.util.MyLogger.log;

//두개를 넣었는데 하나의 값만 넣어짐
public class SimpleListMainV2 {
    public static void main(String[] args) throws InterruptedException {
       //test(new BasicList());
        //test(new SyncList());

        //target에 basicList가 들어감
        BasicList basicList = new BasicList();
        SyncProxyList syncProxyList = new SyncProxyList(basicList);
        //proxyList에 있는 add를 호출 -> synchronized를 건다
        test(syncProxyList);
    }

    private static void test(SimpleList list) throws InterruptedException {
        log(list.getClass().getSimpleName());

        Runnable addA = new Runnable() {

            //Thread A
            @Override
            public void run() {
                list.add("A");
                log("Thread-1 : List.add(A)");
            }
        };

        Runnable addB = new Runnable() {


            //Thread B
            @Override
            public void run() {
                list.add("B");
                log("Thread-2 : List.add(B)");
            }
        };

        Thread thread1 = new Thread(addA, "Thread-1");
        Thread thread2 = new Thread(addB, "Thread-2");

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        log(list);
    }
}

변경 구조: 클라이언트 SyncProxyList (프록시) BasicList (서버)

정적 의존관계

simpleList 인터페이스 덕분에 BasicList, SyncList, SyncProxyList중에 어떤 것을
사용하든, 클라이언트인 test() 의 코드는 전혀 변경하지 않아도 된다.

런타임 의존관계

실제 런티임에 발생하는 인스턴스의 의존관계 = 런타임 의존관계
test(new BasicList()) 를 실행하면 BasicList(x001) 의 인스턴스가 만들어지면서 test() 메서드에 전달된다.
test() 메서드는 BasicList(x001) 인스턴스의 참조를 알고 사용하게 된다.



test() 메서드에서 스레드를 만들고, 스레드에 있는 run() 에서 list.add() 를 호출
프록시인 SyncProxyListsynchronized 를 적용한다. 그리고 나서 target 에 있는add() 를 호출
원본 대상인 BasicList(x001)add() 가 호출
SyncProxyList에 있는add()로 흐름이 돌아온다. 메서드를 반환하면서 synchronized 를 해제

Proxy 정리
프록시는 내부에 원본을 갖고 있다. 그래서 프록시가 필요한 일부의 일을 처리, 다음에 원본을 호출하는 구조 형성, 여기서 프록시는 synchronized를 통한 동기화 작용

프록시 pattern
객체지향 디자인 패턴 중 하나로 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스의 역할을 하는 객체를 제공하는 pattern. 프록시 객체는 실제 객체에 대한 참조를 유지, 그 객체에 접근 or 수행 하기 전에 추가적 처리

프록시 패턴의 주요 목적
접근 제어: 실제 객체에 대한 접근을 제한하거나 통제할 수 있다.
성능 향상: 실제 객체의 생성을 지연시키거나 캐싱하여 성능을 최적화할 수 있다.
부가 기능 제공: 실제 객체에 추가적인 기능(로깅, 인증, 동기화 등)을 투명하게 제공할 수 있다.

자바 동시성 컬렉션1 - Synchronized

자료구조 처음부터 동기화를 적용하면?? -> 성능과 tradeOff 발생
컬렉션이 항상 멀티스레드 환경에서 적용되는 것은 아니기 때문에 단일 스레드일 경우 성능 Issue 발생

자바 synchronized 프록시

package chap48;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedListMain {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        //List<String> list = new ArrayList<>(); //동기화 문제 발생
        list.add("data1");
        list.add("data2");
        list.add("data3");

        System.out.println(list.getClass());
        System.out.println("list = " + list);
    }
}

Collections 는 다음과 같이 다양한 synchronized 동기화 메서드를 지원한다. 이 메서드를 사용하면 List ,Collection , Map , Set 등 다양한 동기화 프록시를 만들어낼 수 있다.
synchronizedList()
synchronizedCollection()
synchronizedMap()
synchronizedSet()
synchronizedNavigableMap()
synchronizedNavigableSet()
synchronizedSortedMap()
synchronizedSortedSet()

자바 동시성 컬렉션2 - 동시성 컬렉션

동시성 컬렉션은 스레드 안전한 컬렉션, 고성능 멀티스레드 환경을 지원하는 다양한 동시성 컬렉션 클래스들을 제공. ex) ConcurrentHashMap , CopyOnWriteArrayList , BlockingQueue


LIST

package chap48;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ListMain {
    public static void main(String[] args) {
        List<Integer> list = new CopyOnWriteArrayList<>();

        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        System.out.println("list = " + list);
    }
}

SetMain

package chap48;

import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArraySet;

public class SetMain {
    public static void main(String[] args) {
        Set<Integer> set = new CopyOnWriteArraySet<>();
        set.add(1);
        set.add(2);
        set.add(3);
        set.add(4);
        System.out.println("set = " + set);

        Set<Integer> skip = new ConcurrentSkipListSet<>();
        skip.add(1);
        skip.add(2);
        skip.add(3);
        System.out.println("skip = " + skip);


    }
}

MapMain

package chap48;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MapMain {
    public static void main(String[] args) {
        Map<Integer, String> map1 = new ConcurrentHashMap<>();
        map1.put(1, "data1");
        map1.put(2, "data2");
        map1.put(3, "data3");
        System.out.println("map1 = " + map1);

        Map<Integer, String> map2 = new ConcurrentHashMap<>();
        map2.put(1, "data1");
        map2.put(2, "data2");
        map2.put(3, "data3");

        System.out.println("map2 = " + map2);
    }
}
profile
개발자를 향해 가는 중입니다~! 항상 겸손

0개의 댓글