ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라.
    Java/이펙티브 자바 2023. 11. 5. 21:10

    핵심정리

    - 제네릭 가변인수 배열에 값을 저장하는 것은 안전하지 않다.

        ㆍ힙 오염이 발생할 수 있다 (컴파일 경고 발생)

        ㆍ자바7에 추가된 @SafeVarargs 애노테이션을 사용할 수 있다.

    static void dangerous(List<String>... stringLists) {
    //        List<String>[] myList = new ArrayList<String>[10];  // 컴파일러가 허용하지 않는다!
    
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList;   // 힙 오염 발생
        String s = stringLists[0].get(0);   // ClassCastException
    }
    @SafeVarargs    // 가변인자는 안전하게 사용되고 있다.
    static <T> List<T> flatten(List<? extends T>... lists) {    // 안에 아무것도 안넣고 꺼내서 쓰기만 하면 안전하다. & 밖으로 노출하지마라.
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists) {
            result.addAll(list);
        }
        return result;
    }

     

    - 제네릭 가변인수 배열의 참조를 밖으로 노출하면 힙 오염을 전달할 수 있다.

        ㆍ예외적으로, @SafeVarargs를 사용한 메서드에 넘기는 것은 안전하다.

        ㆍ예외적으로, 배열의 내용의 일부 함수를 호출하는 일반 메서드로 넘기는 것은 안전하다.

    // 미묘한 힙 오염 발생 (193-194쪽)
    public class PickTwo {
        // 코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다! (193쪽)
        static <T> T[] toArray(T... args) {
            return args;    // object[] 를 반환
        }
    
        static <T> T[] pickTwo(T a, T b,  T c) {
            switch (ThreadLocalRandom.current().nextInt(3)) {
                case 0: return toArray(a, b);
                case 1: return toArray(a, c);
                case 2: return toArray(b, c);
            }
            throw new AssertionError(); //도달할 수 없다.
        }
    
        public static void main(String[] args) {
            String[] attributes = pickTwo("좋은", "빠른", "저렴한");   // object[]를 반환하기에 ClassCastException 발생
            System.out.println(Arrays.toString(attributes));
        }
    }
    // 배열 대신 List를 이용해 안전하게 바뀐 PickTwo (196쪽)
    public class SafePickTwo {
        static <T> List<T> pickTwo(T a, T b, T c) {
            switch (ThreadLocalRandom.current().nextInt(3)) {
                case 0: return List.of(a, b);
                case 1: return List.of(a, c);
                case 2: return List.of(b, c);
            }
            throw new AssertionError(); //도달할 수 없다.
        }
    
        public static void main(String[] args) {
            List<String> attributes = pickTwo("좋은", "빠른", "저렴한");
            System.out.println(attributes);
        }
    }

     

    - 아이템 28의 조언에 따라 가변인수를 List로 바꾼다면

        ㆍ배열없이 제네릭만 사용하므로 컴파일러가 타입 안정성을 보장할 수 있다.

        ㆍ@SafeVarargs 애노테이션을 사용할 필요가 없다.

        ㆍ실수로 안전하다고 판단할 걱정도 없다.

    static <T> List<T> flatten(List<List<? extends T>> lists) { // 가변인자 대신에 List 사용 -> @SafeVarargs 사용하지 않아도 된다.
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists) {
            result.addAll(list);
        }
        return result;
    }

     

     

    완벽공략 - ThreadLocal

    쓰레드 지역 변수

     

    - 모든 멤버 변수는 기본적으로 여러 쓰레드에서 공유해서 쓰일 수 있다. 이때 쓰레드 안전성과 관련된 여러 문제가 발생할 수 있다.

        ㆍ경합 또는 경쟁조건 (Race-Condition) : 어떤 쓰레드가 먼저 실행함에 따라 다른 값이 나온다.

        ㆍ교착상태 (deadlock) : 앞에서 Lock이 걸렸을 때 서로 키를 기다리는 경우

        ㆍLivelock : 서로 락만 교환하는 형태

    - 쓰레드 지역 변수를 사용하면 동기화를 하지 않아도 한 쓰레드에서만 접근 가능한 값이기 때문에 안전하게 사용할 수 있다.

    private SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmm");
    
    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i = 0; i < 10; i++) {
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }
    
    @Override
    public void run() {
        System.out.println("Thread Name= " + Thread.currentThread().getName() + " default Formatter = " + formatter.toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter = new SimpleDateFormat();
    
        System.out.println("Thread Name= " + Thread.currentThread().getName() + " formatter = " + formatter.toPattern());
    }
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
    
    
    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i = 0; i < 10; i++) {
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }
    
    @Override
    public void run() {
        System.out.println("Thread Name= " + Thread.currentThread().getName() + " default Formatter = " + formatter.get());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        formatter.set(new SimpleDateFormat());
    
        System.out.println("Thread Name= " + Thread.currentThread().getName() + " formatter = " + formatter.get());
    }

        ㆍsynchronized 없이도 멀티 스레드 환경에서 안전하게 스레드 범위에 해당하는 변수를 만들어 쓸 수 있다.

     

    - 한 쓰레드 내에서 공유하는 데이터로, 메서드 매개변수에 매번 전달하지 않고 전역 변수처럼 사용할 수 있다.

        ㆍex) 스프링에서의 트랜잭션 관리

     

    완벽공략 - ThreadLocalRandom

    스레드 지역 랜덤값 생성기

     

    - java.util.Random은 멀티 스레드 환경에서 CAS(CompareAndSet)로 인해 실패할 가능성이 있기 때문에 성능이 좋지 않다.

    protected int next(int bist) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + added) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

        ㆍAtomicLong : optimistic Lock 을 사용

    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int readValue = value;
        if (readValue == expectedValue) {
            value = newValue;
        }
        return readValue;
    }

        ㆍ위의 코드 중 compareAndSet과 같은 형식

        ㆍ멀티스레드 환경에서 많이 호출될 시 실패하는 경우가 발생한다.

              => 둘 중 한 스레드는 재시도를 반복하다가 재시도하는 경우가 발생할 수 도 있다.

    - Random 대신 TreadLocalRandom을 사용하면 해당 스레드 전용 Random이라 간섭이 발생하지 않는다.

    댓글

Designed by Tistory.