-
아이템 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이라 간섭이 발생하지 않는다.
'Java > 이펙티브 자바' 카테고리의 다른 글
아이템33. 타입 안전 이종 컨테이너를 고려하라. (0) 2023.11.09 아이템31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) 2023.10.31 아이템29 & 아이템30 (0) 2023.09.28 아이템28. 배열보다는 리스트를 사용하라 (0) 2023.09.23 아이템 27. 비검사 경고를 제거하라 (0) 2023.09.21